diff --git a/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs b/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs index b28e47e69..44c777117 100644 --- a/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs +++ b/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs @@ -1,1961 +1,2144 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Threading.Tasks; using DisCatSharp.ApplicationCommands.Attributes; using DisCatSharp.ApplicationCommands.EventArgs; 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 _commandMethods { get; set; } = new List(); /// /// List of groups. /// private static List _groupCommands { get; set; } = new List(); /// /// List of groups with subgroups. /// private static List _subGroupCommands { get; set; } = new List(); /// /// List of context menus. /// private static List _contextMenuCommands { get; set; } = new List(); /// /// List of global commands on discords backend. /// - private static List _globalDiscordCommands { get; set; } = new List(); + private List _globalDiscordCommands { get; set; } = null; /// /// List of guild commands on discords backend. /// - private static Dictionary> _guildDiscordCommands { get; set; } = new Dictionary>(); + private Dictionary> _guildDiscordCommands { get; set; } = null; /// /// Singleton modules. /// private static List _singletonModules { get; set; } = new List(); /// /// List of modules to register. /// private List> _updateList { get; set; } = new List>(); /// /// Configuration for Discord. /// private readonly ApplicationCommandsConfiguration _configuration; /// /// Set to true if anything fails when registering. /// private static bool _errored { get; set; } = false; /// /// Gets a list of registered commands. The key is the guild id (null if global). /// public IReadOnlyList>> RegisteredCommands => _registeredCommands; private static List>> _registeredCommands = new(); /// /// Gets a list of registered global commands. /// public IReadOnlyList GlobalCommands => _globalCommands; private static readonly List _globalCommands = new(); /// /// Gets a list of registered guild commands mapped by guild id. /// public IReadOnlyDictionary> GuildCommands => _guildCommands; private static readonly Dictionary> _guildCommands = new(); /// /// Gets the registration count. /// private static int RegistrationCount { get; set; } = 0; /// /// Gets the expected count. /// private static int ExpectedCount { get; set; } = 0; /// /// Gets the guild ids where the applications.commands scope is missing. /// private IReadOnlyList MissingScopeGuildIds { get; set; } /// /// Initializes a new instance of the class. /// /// The configuration. internal ApplicationCommandsExtension(ApplicationCommandsConfiguration configuration) { this._configuration = configuration; } /// /// 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.Ready += this.UpdateAsync; this.Client.InteractionCreated += this.CatchInteractionsOnStartup; this.Client.ContextMenuInteractionCreated += this.CatchContextMenuInteractionsOnStartup; } private async Task CatchInteractionsOnStartup(DiscordClient sender, InteractionCreateEventArgs e) => await e.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().AsEphemeral(true).WithContent("Attention: This application is still starting up. Application commands are unavailable for now.")); private async Task CatchContextMenuInteractionsOnStartup(DiscordClient sender, ContextMenuInteractionCreateEventArgs e) => await e.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().AsEphemeral(true).WithContent("Attention: This application is still starting up. Context menu commands are unavailable for now.")); private void FinishedRegistration() { this.Client.InteractionCreated -= this.CatchInteractionsOnStartup; this.Client.ContextMenuInteractionCreated -= this.CatchContextMenuInteractionsOnStartup; this.Client.InteractionCreated += this.InteractionHandler; this.Client.ContextMenuInteractionCreated += this.ContextMenuHandler; } /// /// Registers a command class. /// /// The command class to register. /// The guild id to register it on. If you want global commands, leave it null. public void RegisterCommands(ulong? guildId = null) where T : ApplicationCommandsModule { if (this.Client.ShardId == 0) this._updateList.Add(new KeyValuePair(guildId, new ApplicationCommandsModuleConfiguration(typeof(T)))); } /// /// Registers a command class. /// /// The of the command class to register. /// The guild id to register it on. If you want global commands, leave it null. public void RegisterCommands(Type type, ulong? guildId = null) { if (!typeof(ApplicationCommandsModule).IsAssignableFrom(type)) throw new ArgumentException("Command classes have to inherit from ApplicationCommandsModule", nameof(type)); //If sharding, only register for shard 0 if (this.Client.ShardId == 0) this._updateList.Add(new KeyValuePair(guildId, new ApplicationCommandsModuleConfiguration(type))); } /// /// Cleans all guild application commands. /// public async Task CleanGuildCommandsAsync() { foreach(var guild in this.Client.Guilds.Values) { await this.Client.BulkOverwriteGuildApplicationCommandsAsync(guild.Id, Array.Empty()); } } /// /// Cleans the global application commands. /// public async Task CleanGlobalCommandsAsync() => await this.Client.BulkOverwriteGlobalApplicationCommandsAsync(Array.Empty()); /// /// Registers a command class with permission and translation setup. /// /// The command class to register. /// The guild id to register it on. /// A callback to setup permissions with. /// A callback to setup translations with. public void RegisterCommands(ulong guildId, Action permissionSetup = null, Action translationSetup = null) where T : ApplicationCommandsModule { if (this.Client.ShardId == 0) this._updateList.Add(new KeyValuePair(guildId, new ApplicationCommandsModuleConfiguration(typeof(T), permissionSetup, translationSetup))); } /// /// Registers a command class with permission and translation setup. /// /// The of the command class to register. /// The guild id to register it on. /// A callback to setup permissions with. /// A callback to setup translations with. public void RegisterCommands(Type type, ulong guildId, Action permissionSetup = null, Action translationSetup = null) { if (!typeof(ApplicationCommandsModule).IsAssignableFrom(type)) throw new ArgumentException("Command classes have to inherit from ApplicationCommandsModule", nameof(type)); //If sharding, only register for shard 0 if (this.Client.ShardId == 0) this._updateList.Add(new KeyValuePair(guildId, new ApplicationCommandsModuleConfiguration(type, permissionSetup, translationSetup))); } /* /// /// Registers a command class with permission setup but without a guild id. /// /// The command class to register. /// A callback to setup permissions with. public void RegisterCommands(Action permissionSetup = null) where T : ApplicationCommandsModule { if (this.Client.ShardId == 0) this._updateList.Add(new KeyValuePair(null, new ApplicationCommandsModuleConfiguration(typeof(T), permissionSetup))); } /// /// Registers a command class with permission setup but without a guild id. /// /// The of the command class to register. /// A callback to setup permissions with. public void RegisterCommands(Type type, Action permissionSetup = null) { if (!typeof(ApplicationCommandsModule).IsAssignableFrom(type)) throw new ArgumentException("Command classes have to inherit from ApplicationCommandsModule", nameof(type)); //If sharding, only register for shard 0 if (this.Client.ShardId == 0) this._updateList.Add(new KeyValuePair(null, new ApplicationCommandsModuleConfiguration(type, permissionSetup))); } */ /// /// Fired when the application commands module is ready. /// public event AsyncEventHandler ApplicationCommandsModuleReady { add { this._applicationCommandsModuleReady.Register(value); } remove { this._applicationCommandsModuleReady.Unregister(value); } } private AsyncEvent _applicationCommandsModuleReady; /// /// Fired when the application commands modules startup is finished. /// public event AsyncEventHandler ApplicationCommandsModuleStartupFinished { add { this._applicationCommandsModuleStartupFinished.Register(value); } remove { this._applicationCommandsModuleStartupFinished.Unregister(value); } } private AsyncEvent _applicationCommandsModuleStartupFinished; /// /// Fired when guild commands are registered on a guild. /// public event AsyncEventHandler GuildApplicationCommandsRegistered { add { this._guildApplicationCommandsRegistered.Register(value); } remove { this._guildApplicationCommandsRegistered.Unregister(value); } } private AsyncEvent _guildApplicationCommandsRegistered; /// /// Fired when the global commands are registered. /// public event AsyncEventHandler GlobalApplicationCommandsRegistered { add { this._globalApplicationCommandsRegistered.Register(value); } remove { this._globalApplicationCommandsRegistered.Unregister(value); } } private AsyncEvent _globalApplicationCommandsRegistered; /// /// To be run on ready. /// /// The client. /// The ready event args. internal async Task UpdateAsync(DiscordClient client, ReadyEventArgs e) => await this.UpdateAsync(); /// /// Actual method for registering, used for RegisterCommands and on Ready. /// internal async Task UpdateAsync() { //Only update for shard 0 if (this.Client.ShardId == 0) { + this._globalDiscordCommands = new(); + this._guildDiscordCommands = new(); + var commands_pending = this._updateList.Select(x => x.Key).Distinct(); ExpectedCount = commands_pending.Count(); List FailedGuilds = new(); - _globalDiscordCommands = this.Client.GetGlobalApplicationCommandsAsync().Result.ToList(); + IEnumerable GlobalCommands = null; + GlobalCommands = await this.Client.GetGlobalApplicationCommandsAsync() ?? null; foreach(var guild in this.Client.Guilds.Keys) { IEnumerable Commands = null; var unauthorized = false; try { - Commands = await this.Client.GetGuildApplicationCommandsAsync(guild); + Commands = await this.Client.GetGuildApplicationCommandsAsync(guild) ?? null; } catch (UnauthorizedException) { unauthorized = true; } finally { - if (!unauthorized) - _guildDiscordCommands.Add(guild, Commands.ToList()); + if (!unauthorized && Commands != null && Commands.Any()) + this._guildDiscordCommands.Add(guild, Commands.ToList()); + else if (!unauthorized) + this._guildDiscordCommands.Add(guild, null); else FailedGuilds.Add(guild); } } + if (GlobalCommands != null && GlobalCommands.Any()) + this._globalDiscordCommands.AddRange(GlobalCommands); + foreach (var key in commands_pending) { this.Client.Logger.LogDebug(key.HasValue ? $"Registering commands in guild {key.Value}" : "Registering global commands."); this.RegisterCommands(this._updateList.Where(x => x.Key == key).Select(x => x.Value), key); } this.MissingScopeGuildIds = FailedGuilds; await this._applicationCommandsModuleReady.InvokeAsync(this, new ApplicationCommandsModuleReadyEventArgs(this._configuration?.ServiceProvider) { Handled = true, GuildsWithoutScope = FailedGuilds }); } } /// /// Method for registering commands for a target from modules. /// /// The types. /// The optional guild id. private void RegisterCommands(IEnumerable types, ulong? guildid) { //Initialize empty lists to be added to the global ones at the end var commandMethods = new List(); var groupCommands = new List(); var subGroupCommands = new List(); var contextMenuCommands = new List(); var updateList = new List(); var commandTypeSources = new List>(); _ = Task.Run(async () => { //Iterates over all the modules foreach (var config in types) { var type = config.Type; try { var module = type.GetTypeInfo(); var classes = new List(); //Add module to classes list if it's a group if (module.GetCustomAttribute() != null) { classes.Add(module); } else { //Otherwise add the nested groups classes = module.DeclaredNestedTypes.Where(x => x.GetCustomAttribute() != null).ToList(); } //Handles groups foreach (var subclassinfo in classes) { var ctx = new ApplicationCommandsTranslationContext(type, module.FullName); config.Translations?.Invoke(ctx); List translations = null; if (!string.IsNullOrEmpty(ctx.Translations)) { translations = JsonConvert.DeserializeObject>(ctx.Translations); } //Gets the attribute and methods in the group var groupAttribute = subclassinfo.GetCustomAttribute(); var submethods = subclassinfo.DeclaredMethods.Where(x => x.GetCustomAttribute() != null); var subclasses = subclassinfo.DeclaredNestedTypes.Where(x => x.GetCustomAttribute() != null); if (subclasses.Any() && submethods.Any()) { throw new ArgumentException("Slash command groups cannot have both subcommands and subgroups!"); } DiscordApplicationCommandLocalization NameLocalizations = null; DiscordApplicationCommandLocalization DescriptionLocalizations = null; if (translations != null) { var command_translation = translations.Single(c => c.Name == groupAttribute.Name); if (command_translation != null) { NameLocalizations = command_translation.NameTranslations; DescriptionLocalizations = command_translation.DescriptionTranslations; } } //Initializes the command var payload = new DiscordApplicationCommand(groupAttribute.Name, groupAttribute.Description, default_permission: groupAttribute.DefaultPermission, nameLocalizations: NameLocalizations, descriptionLocalizations: DescriptionLocalizations); commandTypeSources.Add(new KeyValuePair(type, type)); var commandmethods = new List>(); //Handles commands in the group foreach (var submethod in submethods) { var commandAttribute = submethod.GetCustomAttribute(); //Gets the paramaters and accounts for InteractionContext var parameters = submethod.GetParameters(); if (parameters.Length == 0 || parameters == null || !ReferenceEquals(parameters.First().ParameterType, typeof(InteractionContext))) throw new ArgumentException($"The first argument must be an InteractionContext!"); parameters = parameters.Skip(1).ToArray(); var options = await this.ParseParameters(parameters, guildid); DiscordApplicationCommandLocalization SubNameLocalizations = null; DiscordApplicationCommandLocalization SubDescriptionLocalizations = null; List LocalizisedOptions = null; if (translations != null) { var command_translation = translations.Single(c => c.Name == payload.Name); if (command_translation.Commands != null) { var sub_command_translation = command_translation.Commands.Single(sc => sc.Name == commandAttribute.Name); if (sub_command_translation.Options != null) { LocalizisedOptions = new(options.Count); foreach (var option in options) { List choices = option.Choices != null ? new(option.Choices.Count()) : null; if (option.Choices != null) { foreach (var choice in option.Choices) { choices.Add(new DiscordApplicationCommandOptionChoice(choice.Name, choice.Value, sub_command_translation.Options.Single(o => o.Name == option.Name).Choices.Single(c => c.Name == choice.Name).NameTranslations)); } } LocalizisedOptions.Add(new DiscordApplicationCommandOption(option.Name, option.Description, option.Type, option.Required, choices, option.Options, option.ChannelTypes, option.AutoComplete, option.MinimumValue, option.MaximumValue, sub_command_translation.Options.Single(o => o.Name == option.Name).NameTranslations, sub_command_translation.Options.Single(o => o.Name == option.Name).DescriptionTranslations )); } } SubNameLocalizations = sub_command_translation.NameTranslations; SubDescriptionLocalizations = sub_command_translation.DescriptionTranslations; } } //Creates the subcommand and adds it to the main command var subpayload = new DiscordApplicationCommandOption(commandAttribute.Name, commandAttribute.Description, ApplicationCommandOptionType.SubCommand, null, null, LocalizisedOptions ?? options, nameLocalizations: SubNameLocalizations, descriptionLocalizations: SubDescriptionLocalizations); payload = new DiscordApplicationCommand(payload.Name, payload.Description, payload.Options?.Append(subpayload) ?? new[] { subpayload }, payload.DefaultPermission, nameLocalizations: payload.NameLocalizations, descriptionLocalizations: payload.DescriptionLocalizations); commandTypeSources.Add(new KeyValuePair(subclassinfo, type)); //Adds it to the method lists commandmethods.Add(new KeyValuePair(commandAttribute.Name, submethod)); groupCommands.Add(new GroupCommand { Name = groupAttribute.Name, Methods = commandmethods }); } var command = new SubGroupCommand { Name = groupAttribute.Name }; //Handles subgroups foreach (var subclass in subclasses) { var subGroupAttribute = subclass.GetCustomAttribute(); var subsubmethods = subclass.DeclaredMethods.Where(x => x.GetCustomAttribute() != null); var options = new List(); var currentMethods = new List>(); DiscordApplicationCommandLocalization SubNameLocalizations = null; DiscordApplicationCommandLocalization SubDescriptionLocalizations = null; if (translations != null) { var command_translation = translations.Single(c => c.Name == payload.Name); if (command_translation != null && command_translation.SubGroups != null) { var sub_command_translation = command_translation.SubGroups.Single(sc => sc.Name == subGroupAttribute.Name); if (sub_command_translation != null) { SubNameLocalizations = sub_command_translation.NameTranslations; SubDescriptionLocalizations = sub_command_translation.DescriptionTranslations; } } } //Similar to the one for regular groups foreach (var subsubmethod in subsubmethods) { var suboptions = new List(); var commatt = subsubmethod.GetCustomAttribute(); var parameters = subsubmethod.GetParameters(); if (parameters.Length == 0 || parameters == null || !ReferenceEquals(parameters.First().ParameterType, typeof(InteractionContext))) throw new ArgumentException($"The first argument must be an InteractionContext!"); parameters = parameters.Skip(1).ToArray(); suboptions = suboptions.Concat(await this.ParseParameters(parameters, guildid)).ToList(); DiscordApplicationCommandLocalization SubSubNameLocalizations = null; DiscordApplicationCommandLocalization SubSubDescriptionLocalizations = null; List LocalizisedOptions = null; if (translations != null) { var command_translation = translations.Single(c => c.Name == payload.Name); if (command_translation.SubGroups != null) { var sub_command_translation = command_translation.SubGroups.Single(sc => sc.Name == commatt.Name); if (sub_command_translation != null) { var sub_sub_command_translation = sub_command_translation.Commands.Single(sc => sc.Name == commatt.Name); if(sub_sub_command_translation != null) { if(sub_sub_command_translation.Options != null) { LocalizisedOptions = new(suboptions.Count); foreach (var option in suboptions) { List choices = option.Choices != null ? new(option.Choices.Count()) : null; if (option.Choices != null) { foreach (var choice in option.Choices) { choices.Add(new DiscordApplicationCommandOptionChoice(choice.Name, choice.Value, sub_sub_command_translation.Options.Single(o => o.Name == option.Name).Choices.Single(c => c.Name == choice.Name).NameTranslations)); } } LocalizisedOptions.Add(new DiscordApplicationCommandOption(option.Name, option.Description, option.Type, option.Required, choices, option.Options, option.ChannelTypes, option.AutoComplete, option.MinimumValue, option.MaximumValue, sub_sub_command_translation.Options.Single(o => o.Name == option.Name).NameTranslations, sub_sub_command_translation.Options.Single(o => o.Name == option.Name).DescriptionTranslations )); } } SubSubNameLocalizations = sub_sub_command_translation.NameTranslations; SubSubDescriptionLocalizations = sub_sub_command_translation.DescriptionTranslations; } } } } var subsubpayload = new DiscordApplicationCommandOption(commatt.Name, commatt.Description, ApplicationCommandOptionType.SubCommand, null, null, LocalizisedOptions ?? suboptions, nameLocalizations: SubSubNameLocalizations, descriptionLocalizations: SubSubDescriptionLocalizations); options.Add(subsubpayload); commandmethods.Add(new KeyValuePair(commatt.Name, subsubmethod)); currentMethods.Add(new KeyValuePair(commatt.Name, subsubmethod)); } //Adds the group to the command and method lists var subpayload = new DiscordApplicationCommandOption(subGroupAttribute.Name, subGroupAttribute.Description, ApplicationCommandOptionType.SubCommandGroup, null, null, options, nameLocalizations: SubNameLocalizations, descriptionLocalizations: SubDescriptionLocalizations); command.SubCommands.Add(new GroupCommand { Name = subGroupAttribute.Name, Methods = currentMethods }); payload = new DiscordApplicationCommand(payload.Name, payload.Description, payload.Options?.Append(subpayload) ?? new[] { subpayload }, payload.DefaultPermission, nameLocalizations: payload.NameLocalizations, descriptionLocalizations: payload.DescriptionLocalizations); commandTypeSources.Add(new KeyValuePair(subclass, type)); //Accounts for lifespans for the sub group if (subclass.GetCustomAttribute() != null) { if (subclass.GetCustomAttribute().Lifespan == ApplicationCommandModuleLifespan.Singleton) { _singletonModules.Add(this.CreateInstance(subclass, this._configuration?.ServiceProvider)); } } } if (command.SubCommands.Any()) subGroupCommands.Add(command); updateList.Add(payload); //Accounts for lifespans if (subclassinfo.GetCustomAttribute() != null) { if (subclassinfo.GetCustomAttribute().Lifespan == ApplicationCommandModuleLifespan.Singleton) { _singletonModules.Add(this.CreateInstance(subclassinfo, this._configuration?.ServiceProvider)); } } } //Handles methods and context menus, only if the module isn't a group itself if (module.GetCustomAttribute() == null) { var ctx = new ApplicationCommandsTranslationContext(type, module.FullName); config.Translations?.Invoke(ctx); List translations = null; if (!string.IsNullOrEmpty(ctx.Translations)) { translations = JsonConvert.DeserializeObject>(ctx.Translations); } //Slash commands (again, similar to the one for groups) var methods = module.DeclaredMethods.Where(x => x.GetCustomAttribute() != null); foreach (var method in methods) { var commandattribute = method.GetCustomAttribute(); var parameters = method.GetParameters(); if (parameters.Length == 0 || parameters == null || !ReferenceEquals(parameters.FirstOrDefault()?.ParameterType, typeof(InteractionContext))) throw new ArgumentException($"The first argument must be an InteractionContext!"); parameters = parameters.Skip(1).ToArray(); var options = await this.ParseParameters(parameters, guildid); commandMethods.Add(new CommandMethod { Method = method, Name = commandattribute.Name }); DiscordApplicationCommandLocalization NameLocalizations = null; DiscordApplicationCommandLocalization DescriptionLocalizations = null; List LocalizisedOptions = null; if (translations != null) { var command_translation = translations.Single(c => c.Name == commandattribute.Name && c.Type == ApplicationCommandType.ChatInput); if (command_translation != null) { if (command_translation.Options != null) { LocalizisedOptions = new(options.Count); foreach (var option in options) { List choices = option.Choices != null ? new(option.Choices.Count()) : null; if (option.Choices != null) { foreach (var choice in option.Choices) { choices.Add(new DiscordApplicationCommandOptionChoice(choice.Name, choice.Value, command_translation.Options.Single(o => o.Name == option.Name).Choices.Single(c => c.Name == choice.Name).NameTranslations)); } } LocalizisedOptions.Add(new DiscordApplicationCommandOption(option.Name, option.Description, option.Type, option.Required, choices, option.Options, option.ChannelTypes, option.AutoComplete, option.MinimumValue, option.MaximumValue, command_translation.Options.Single(o => o.Name == option.Name).NameTranslations, command_translation.Options.Single(o => o.Name == option.Name).DescriptionTranslations )); } } NameLocalizations = command_translation.NameTranslations; DescriptionLocalizations = command_translation.DescriptionTranslations; } } var payload = new DiscordApplicationCommand(commandattribute.Name, commandattribute.Description, LocalizisedOptions ?? options, commandattribute.DefaultPermission, ApplicationCommandType.ChatInput, NameLocalizations, DescriptionLocalizations); updateList.Add(payload); commandTypeSources.Add(new KeyValuePair(type, type)); } //Context Menus var contextMethods = module.DeclaredMethods.Where(x => x.GetCustomAttribute() != null); foreach (var contextMethod in contextMethods) { var contextAttribute = contextMethod.GetCustomAttribute(); DiscordApplicationCommandLocalization NameLocalizations = null; if (translations != null) { var command_translation = translations.Single(c => c.Name == contextAttribute.Name && c.Type == contextAttribute.Type); if (command_translation != null) NameLocalizations = command_translation.NameTranslations; } var command = new DiscordApplicationCommand(contextAttribute.Name, null, null, contextAttribute.DefaultPermission, contextAttribute.Type, NameLocalizations); var parameters = contextMethod.GetParameters(); if (parameters.Length == 0 || parameters == null || !ReferenceEquals(parameters.FirstOrDefault()?.ParameterType, typeof(ContextMenuContext))) throw new ArgumentException($"The first argument must be a ContextMenuContext!"); if (parameters.Length > 1) throw new ArgumentException($"A context menu cannot have parameters!"); contextMenuCommands.Add(new ContextMenuCommand { Method = contextMethod, Name = contextAttribute.Name }); updateList.Add(command); commandTypeSources.Add(new KeyValuePair(type, type)); } //Accounts for lifespans if (module.GetCustomAttribute() != null) { if (module.GetCustomAttribute().Lifespan == ApplicationCommandModuleLifespan.Singleton) { _singletonModules.Add(this.CreateInstance(module, this._configuration?.ServiceProvider)); } } } } catch (Exception ex) { //This isn't really much more descriptive but I added a separate case for it anyway if (ex is BadRequestException brex) this.Client.Logger.LogCritical(brex, $"There was an error registering application commands: {brex.JsonMessage}"); else this.Client.Logger.LogCritical(ex, $"There was an error registering application commands"); _errored = true; } } if (!_errored) { try { async Task UpdateCommandPermission(ulong commandId, string commandName, Type commandDeclaringType, Type commandRootType) { if (!guildid.HasValue) { this.Client.Logger.LogTrace("You can't set global permissions till yet. See https://discord.com/developers/docs/interactions/application-commands#permissions"); } else { var ctx = new ApplicationCommandsPermissionContext(commandDeclaringType, commandName); var conf = types.First(t => t.Type == commandRootType); conf.Setup?.Invoke(ctx); if (ctx.Permissions.Count == 0) return; await this.Client.OverwriteGuildApplicationCommandPermissionsAsync(guildid.Value, commandId, ctx.Permissions); } } async Task UpdateCommandPermissionGroup(GroupCommand groupCommand) { foreach (var com in groupCommand.Methods) { var source = commandTypeSources.FirstOrDefault(f => f.Key == com.Value.DeclaringType); await UpdateCommandPermission(groupCommand.CommandId, com.Key, source.Key, source.Value); } } List Commands = new(); try { if (guildid == null) { - var GlobalCommandsOverwriteList = this.BuildGlobalOverwriteList(updateList); - var GlobalCommandsCreateList = this.BuildGlobalCreateList(updateList); - var GlobalCommandsDeleteList = this.BuildGlobalDeleteList(updateList); - - if (GlobalCommandsCreateList.Any() && !GlobalCommandsOverwriteList.Any()) - { - var cmds = await this.Client.BulkOverwriteGlobalApplicationCommandsAsync(GlobalCommandsCreateList); - Commands.AddRange(cmds); - } - else if (!GlobalCommandsCreateList.Any() && GlobalCommandsOverwriteList.Any()) + if (updateList != null && updateList.Any()) { - List OverwriteList = new(); - foreach (var overwrite in GlobalCommandsOverwriteList) + var GlobalCommandsOverwriteList = this.BuildGlobalOverwriteList(updateList); + var GlobalCommandsCreateList = this.BuildGlobalCreateList(updateList); + var GlobalCommandsDeleteList = this.BuildGlobalDeleteList(updateList); + + if (CheckAnyAndNotNull(GlobalCommandsCreateList) && CheckNotAnyOrNull(GlobalCommandsOverwriteList)) { - var cmd = overwrite.Value; - cmd.Id = overwrite.Key; - OverwriteList.Add(cmd); + var cmds = await this.Client.BulkOverwriteGlobalApplicationCommandsAsync(GlobalCommandsCreateList); + Commands.AddRange(cmds); } - var discord_backend_commands = await this.Client.BulkOverwriteGlobalApplicationCommandsAsync(OverwriteList); - Commands.AddRange(discord_backend_commands); - } - else if (GlobalCommandsCreateList.Any() && GlobalCommandsOverwriteList.Any()) - { - foreach (var cmd in GlobalCommandsCreateList) + else if (CheckNotAnyOrNull(GlobalCommandsCreateList) && CheckAnyAndNotNull(GlobalCommandsOverwriteList)) { - var discord_backend_command = await this.Client.CreateGlobalApplicationCommandAsync(cmd); - Commands.Add(discord_backend_command); + List OverwriteList = new(); + foreach (var overwrite in GlobalCommandsOverwriteList) + { + var cmd = overwrite.Value; + cmd.Id = overwrite.Key; + OverwriteList.Add(cmd); + } + var discord_backend_commands = await this.Client.BulkOverwriteGlobalApplicationCommandsAsync(OverwriteList); + Commands.AddRange(discord_backend_commands); + } + else if (CheckAnyAndNotNull(GlobalCommandsCreateList) && CheckAnyAndNotNull(GlobalCommandsOverwriteList)) + { + foreach (var cmd in GlobalCommandsCreateList) + { + var discord_backend_command = await this.Client.CreateGlobalApplicationCommandAsync(cmd); + Commands.Add(discord_backend_command); + } + + foreach (var cmd in GlobalCommandsOverwriteList) + { + var command = cmd.Value; + + var discord_backend_command = await this.Client.EditGlobalApplicationCommandAsync(cmd.Key, action => + { + action.Name = command.Name; + action.NameLocalizations = command.NameLocalizations; + action.Description = command.Description; + action.DescriptionLocalizations = command.DescriptionLocalizations; + if(command.Options != null && command.Options.Any()) + action.Options = Optional.FromValue(command.Options); + action.DefaultPermission = command.DefaultPermission; + }); + Commands.Add(discord_backend_command); + } } - foreach (var cmd in GlobalCommandsOverwriteList) + if (CheckAnyAndNotNull(GlobalCommandsDeleteList)) { - var command = cmd.Value; - var discord_backend_command = await this.Client.ApiClient.EditGlobalApplicationCommandAsync(this.Client.CurrentApplication.Id, - cmd.Key, command.Name, command.Description, command.Options.Any() ? Optional.FromValue(command.Options) : null, command.DefaultPermission, - command.NameLocalizations, command.DescriptionLocalizations - ); - Commands.Add(discord_backend_command); + foreach (var cmdId in GlobalCommandsDeleteList) + { + await this.Client.DeleteGlobalApplicationCommandAsync(cmdId); + } } - } - foreach (var cmdId in GlobalCommandsDeleteList) - { - await this.Client.DeleteGlobalApplicationCommandAsync(cmdId); + if (CheckAnyAndNotNull(Commands)) + _globalCommands.AddRange(Commands); + else + return; } - _globalCommands.AddRange(Commands); } else { - var GuildCommandsOverwriteList = this.BuildGuildOverwriteList(guildid.Value, updateList); - var GuildCommandsCreateList = this.BuildGuildCreateList(guildid.Value, updateList); - var GuildCommandsDeleteList = this.BuildGuildDeleteList(guildid.Value, updateList); - - if (GuildCommandsCreateList.Any() && !GuildCommandsOverwriteList.Any()) - { - var cmds = await this.Client.BulkOverwriteGuildApplicationCommandsAsync(guildid.Value, GuildCommandsCreateList); - Commands.AddRange(cmds); - } - else if (!GuildCommandsCreateList.Any() && GuildCommandsOverwriteList.Any()) + if (updateList != null && updateList.Any()) { - List OverwriteList = new(); - foreach (var overwrite in GuildCommandsOverwriteList) + var GuildCommandsOverwriteList = this.BuildGuildOverwriteList(guildid.Value, updateList); + var GuildCommandsCreateList = this.BuildGuildCreateList(guildid.Value, updateList); + var GuildCommandsDeleteList = this.BuildGuildDeleteList(guildid.Value, updateList); + + if (CheckAnyAndNotNull(GuildCommandsCreateList) && CheckNotAnyOrNull(GuildCommandsOverwriteList)) { - var cmd = overwrite.Value; - cmd.Id = overwrite.Key; - OverwriteList.Add(cmd); + var cmds = await this.Client.BulkOverwriteGuildApplicationCommandsAsync(guildid.Value, GuildCommandsCreateList); + Commands.AddRange(cmds); } - var discord_backend_commands = await this.Client.BulkOverwriteGuildApplicationCommandsAsync(guildid.Value, OverwriteList); - Commands.AddRange(discord_backend_commands); - } - else if (GuildCommandsCreateList.Any() && GuildCommandsOverwriteList.Any()) - { - foreach (var cmd in GuildCommandsCreateList) + else if (CheckNotAnyOrNull(GuildCommandsCreateList) && CheckAnyAndNotNull(GuildCommandsOverwriteList)) { - var discord_backend_command = await this.Client.CreateGuildApplicationCommandAsync(guildid.Value, cmd); - Commands.Add(discord_backend_command); + List OverwriteList = new(); + foreach (var overwrite in GuildCommandsOverwriteList) + { + var cmd = overwrite.Value; + cmd.Id = overwrite.Key; + OverwriteList.Add(cmd); + } + var discord_backend_commands = await this.Client.BulkOverwriteGuildApplicationCommandsAsync(guildid.Value, OverwriteList); + Commands.AddRange(discord_backend_commands); } + else if (CheckAnyAndNotNull(GuildCommandsCreateList) && CheckAnyAndNotNull(GuildCommandsOverwriteList)) + { + foreach (var cmd in GuildCommandsCreateList) + { + var discord_backend_command = await this.Client.CreateGuildApplicationCommandAsync(guildid.Value, cmd); + Commands.Add(discord_backend_command); + } - foreach (var cmd in GuildCommandsOverwriteList) + foreach (var cmd in GuildCommandsOverwriteList) + { + var command = cmd.Value; + var discord_backend_command = await this.Client.EditGuildApplicationCommandAsync(guildid.Value, cmd.Key, action => + { + action.Name = command.Name; + action.NameLocalizations = command.NameLocalizations; + action.Description = command.Description; + action.DescriptionLocalizations = command.DescriptionLocalizations; + if(command.Options != null && command.Options.Any()) + action.Options = Optional.FromValue(command.Options); + action.DefaultPermission = command.DefaultPermission; + }); + + Commands.Add(discord_backend_command); + } + } + + if(CheckAnyAndNotNull(GuildCommandsDeleteList)) { - var command = cmd.Value; - var discord_backend_command = await this.Client.ApiClient.EditGuildApplicationCommandAsync(this.Client.CurrentApplication.Id, guildid.Value, - cmd.Key, command.Name, command.Description, command.Options.Any() ? Optional.FromValue(command.Options) : null, command.DefaultPermission, - command.NameLocalizations, command.DescriptionLocalizations - ); - Commands.Add(discord_backend_command); + foreach (var cmdId in GuildCommandsDeleteList) + { + await this.Client.DeleteGuildApplicationCommandAsync(guildid.Value, cmdId); + } } - } - foreach (var cmdId in GuildCommandsDeleteList) - { - await this.Client.DeleteGuildApplicationCommandAsync(guildid.Value, cmdId); + if (CheckAnyAndNotNull(Commands)) + _guildCommands.Add(guildid.Value, Commands); + else + return; } - - _guildCommands.Add(guildid.Value, Commands); } } catch (UnauthorizedException ex) { this.Client.Logger.LogError($"Could not register application commands for guild {guildid}.\nError: {ex.JsonMessage}"); return; } //Creates a guild command if a guild id is specified, otherwise global //Checks against the ids and adds them to the command method lists foreach (var command in Commands) { if (commandMethods.Any(x => x.Name == command.Name)) { var com = commandMethods.First(x => x.Name == command.Name); com.CommandId = command.Id; var source = commandTypeSources.FirstOrDefault(f => f.Key == com.Method.DeclaringType); await UpdateCommandPermission(command.Id, com.Name, source.Value, source.Key); } else if (groupCommands.Any(x => x.Name == command.Name)) { var com = groupCommands.First(x => x.Name == command.Name); com.CommandId = command.Id; await UpdateCommandPermissionGroup(com); } else if (subGroupCommands.Any(x => x.Name == command.Name)) { var com = subGroupCommands.First(x => x.Name == command.Name); com.CommandId = command.Id; foreach (var groupComs in com.SubCommands) await UpdateCommandPermissionGroup(groupComs); } else if (contextMenuCommands.Any(x => x.Name == command.Name)) { var com = contextMenuCommands.First(x => x.Name == command.Name); com.CommandId = command.Id; var source = commandTypeSources.First(f => f.Key == com.Method.DeclaringType); await UpdateCommandPermission(command.Id, com.Name, source.Value, source.Key); } } //Adds to the global lists finally _commandMethods.AddRange(commandMethods); _groupCommands.AddRange(groupCommands); _subGroupCommands.AddRange(subGroupCommands); _contextMenuCommands.AddRange(contextMenuCommands); _registeredCommands.Add(new KeyValuePair>(guildid, Commands.ToList())); foreach (var command in commandMethods) { var app = types.First(t => t.Type == command.Method.DeclaringType); } if (guildid.HasValue) { await this._guildApplicationCommandsRegistered.InvokeAsync(this, new GuildApplicationCommandsRegisteredEventArgs(this._configuration?.ServiceProvider) { Handled = true, GuildId = guildid.Value, RegisteredCommands = _guildCommands.Single(c => c.Key == guildid.Value).Value }); } else { await this._globalApplicationCommandsRegistered.InvokeAsync(this, new GlobalApplicationCommandsRegisteredEventArgs(this._configuration?.ServiceProvider) { Handled = true, RegisteredCommands = _globalCommands }); } RegistrationCount++; this.CheckRegistrationStartup(); } catch (Exception ex) { if (ex is BadRequestException brex) this.Client.Logger.LogCritical(brex, $"There was an error registering application commands: {brex.JsonMessage}"); else this.Client.Logger.LogCritical(ex, $"There was an error registering application commands"); _errored = true; } } }); } + internal static bool CheckNotAnyOrNull(List list) + => list == null ? true : !list.Any(); + + internal static bool CheckNotAnyOrNull(List list) + => list == null ? true : !list.Any(); + + internal static bool CheckNotAnyOrNull(Dictionary dict) + => dict == null ? true : !dict.Any() ? true : dict.Keys == null ? true : !dict.Keys.Any(); + + internal static bool CheckAnyAndNotNull(List list) + => list == null ? false : list.Any(); + + internal static bool CheckAnyAndNotNull(List list) + => list == null ? false : list.Any(); + + internal static bool CheckAnyAndNotNull(Dictionary dict) + => dict == null ? false : !dict.Any() ? false : dict.Keys == null ? false : !dict.Keys.Any() ? false : true; + private async void CheckRegistrationStartup() { if (RegistrationCount == ExpectedCount) { await this._applicationCommandsModuleStartupFinished.InvokeAsync(this, new ApplicationCommandsModuleStartupFinishedEventArgs(this._configuration?.ServiceProvider) { RegisteredGlobalCommands = _globalCommands, RegisteredGuildCommands = _guildCommands, GuildsWithoutScope = MissingScopeGuildIds }); this.FinishedRegistration(); } } private List BuildGuildDeleteList(ulong guildId, List updateList) { - var discord = _guildDiscordCommands.Where(l => l.Key == guildId).First(); - - List InvalidCommandIds = new(); - foreach (var cmd in discord.Value) + if(this._guildDiscordCommands == null || !this._guildDiscordCommands.Any()) { - if (!updateList.Where(ul => ul.Name == cmd.Name).Any()) - InvalidCommandIds.Add(cmd.Id); + return null; } + else if (updateList == null) + { + var discord = this._guildDiscordCommands.Where(l => l.Key == guildId).First().Value; + + if (discord != null) + { + List InvalidCommandIds = new(); + foreach (var cmd in discord) + { + InvalidCommandIds.Add(cmd.Id); + } + + return InvalidCommandIds; + } + else + { + return null; + } + } + else + { + var discord = this._guildDiscordCommands.Where(l => l.Key == guildId).First().Value; - return InvalidCommandIds; + if (discord != null) { + List InvalidCommandIds = new(); + foreach (var cmd in discord) + { + if (!updateList.Any(ul => ul.Name == cmd.Name)) + InvalidCommandIds.Add(cmd.Id); + } + + return InvalidCommandIds; + } + else + { + return null; + } + } } private List BuildGuildCreateList(ulong guildId, List updateList) { - var discord = _guildDiscordCommands.Where(l => l.Key == guildId).First().Value; - List NewCommands = new(); - foreach(var cmd in updateList) + if (this._guildDiscordCommands == null || !this._guildDiscordCommands.Any()) { - if(!discord.Where(d => d.Name == cmd.Name).Any()) + return updateList; + } + else if (updateList == null) + { + return null; + } + else + { + var discord = this._guildDiscordCommands.Where(l => l.Key == guildId).First().Value; + List NewCommands = new(); + foreach (var cmd in updateList) { - NewCommands.Add(cmd); + if (discord == null || !discord.Any(d => d.Name == cmd.Name)) + { + NewCommands.Add(cmd); + } } - } - return NewCommands; + return NewCommands; + } } private Dictionary BuildGuildOverwriteList(ulong guildId, List updateList) { - var discord = _guildDiscordCommands.Where(l => l.Key == guildId).First().Value; - Dictionary UpdateCommands = new(); - foreach (var cmd in updateList) + if (this._guildDiscordCommands == null || !this._guildDiscordCommands.Any() || !this._guildDiscordCommands.Any(l => l.Key == guildId)) + { + return null; + } + else if (updateList == null) + { + return null; + } + else { - var dc = discord.Where(d => d.Name == cmd.Name); - if (dc.Any()) + var discord = this._guildDiscordCommands.Where(l => l.Key == guildId).First().Value; + if (discord != null) { - UpdateCommands.Add(dc.First().Id, cmd); + Dictionary UpdateCommands = new(); + foreach (var cmd in updateList) + { + if (discord.Any(d => d.Name == cmd.Name)) + { + UpdateCommands.Add(discord.Where(d => d.Name == cmd.Name).First().Id, cmd); + } + } + return UpdateCommands; + } + else + { + return null; } } - - return UpdateCommands; } - private List BuildGlobalDeleteList(List updateList) + private List BuildGlobalDeleteList(List updateList = null) { - var discord = _globalDiscordCommands; - List InvalidCommandIds = new(); - foreach(var cmd in discord) + if (this._globalDiscordCommands == null || !this._globalDiscordCommands.Any()) + { + return null; + } + else if (updateList == null) { - if (!updateList.Where(ul => ul.Name == cmd.Name).Any()) - InvalidCommandIds.Add(cmd.Id); + var discord = this._globalDiscordCommands; + + if (discord != null) { + List InvalidCommandIds = new(); + foreach (var cmd in discord) + { + InvalidCommandIds.Add(cmd.Id); + } + + return InvalidCommandIds; + } + else + { + return null; + } } + else + { + var discord = this._globalDiscordCommands; - return InvalidCommandIds; + if (discord != null) + { + List InvalidCommandIds = new(); + foreach (var cmd in discord) + { + if (!updateList.Any(ul => ul.Name == cmd.Name)) + InvalidCommandIds.Add(cmd.Id); + } + + return InvalidCommandIds; + } + else + { + return null; + } + } } private List BuildGlobalCreateList(List updateList) { - var discord = _globalDiscordCommands; - List NewCommands = new(); - foreach (var cmd in updateList) + if (this._globalDiscordCommands == null || !this._globalDiscordCommands.Any()) + { + return updateList; + } + else if(updateList == null) { - if (!discord.Where(d => d.Name == cmd.Name).Any()) + return null; + } + else + { + var discord = this._globalDiscordCommands; + List NewCommands = new(); + foreach (var cmd in updateList) { - NewCommands.Add(cmd); + if (discord == null || !discord.Any(d => d.Name == cmd.Name)) + { + NewCommands.Add(cmd); + } } - } - return NewCommands; + return NewCommands; + } } private Dictionary BuildGlobalOverwriteList(List updateList) { - var discord = _globalDiscordCommands; - Dictionary UpdateCommands = new(); - foreach (var cmd in updateList) + if (this._globalDiscordCommands == null || !this._globalDiscordCommands.Any()) + { + return null; + } + else if (updateList == null) { - var dc = discord.Where(d => d.Name == cmd.Name); - if (dc.Any()) + return null; + } + else + { + var discord = this._globalDiscordCommands; + if (discord != null) + { + Dictionary UpdateCommands = new(); + foreach (var cmd in updateList) + { + if (discord.Any(d => d.Name == cmd.Name)) + { + UpdateCommands.Add(discord.Where(d => d.Name == cmd.Name).First().Id, cmd); + } + } + + return UpdateCommands; + } + else { - UpdateCommands.Add(dc.First().Id, cmd); + return null; } } - - return UpdateCommands; } /// /// Interaction handler. /// /// The client. /// The event args. private Task InteractionHandler(DiscordClient client, InteractionCreateEventArgs e) { _ = Task.Run(async () => { if (e.Interaction.Type == InteractionType.ApplicationCommand) { //Creates the context var context = new InteractionContext { Interaction = e.Interaction, Channel = e.Interaction.Channel, Guild = e.Interaction.Guild, User = e.Interaction.User, Client = client, ApplicationCommandsExtension = this, CommandName = e.Interaction.Data.Name, InteractionId = e.Interaction.Id, Token = e.Interaction.Token, Services = this._configuration?.ServiceProvider, ResolvedUserMentions = e.Interaction.Data.Resolved?.Users?.Values.ToList(), ResolvedRoleMentions = e.Interaction.Data.Resolved?.Roles?.Values.ToList(), ResolvedChannelMentions = e.Interaction.Data.Resolved?.Channels?.Values.ToList(), ResolvedAttachments = e.Interaction.Data.Resolved?.Attachments?.Values.ToList(), Type = ApplicationCommandType.ChatInput, Locale = e.Interaction.Locale, GuildLocale = e.Interaction.GuildLocale }; try { if (_errored) throw new InvalidOperationException("Slash commands failed to register properly on startup."); var methods = _commandMethods.Where(x => x.CommandId == e.Interaction.Data.Id); var groups = _groupCommands.Where(x => x.CommandId == e.Interaction.Data.Id); var subgroups = _subGroupCommands.Where(x => x.CommandId == e.Interaction.Data.Id); if (!methods.Any() && !groups.Any() && !subgroups.Any()) throw new InvalidOperationException("A slash command was executed, but no command was registered for it."); if (methods.Any()) { var method = methods.First().Method; var args = await this.ResolveInteractionCommandParameters(e, context, method, e.Interaction.Data.Options); await this.RunCommandAsync(context, method, args); } else if (groups.Any()) { var command = e.Interaction.Data.Options.First(); var method = groups.First().Methods.First(x => x.Key == command.Name).Value; var args = await this.ResolveInteractionCommandParameters(e, context, method, e.Interaction.Data.Options.First().Options); await this.RunCommandAsync(context, method, args); } else if (subgroups.Any()) { var command = e.Interaction.Data.Options.First(); var group = subgroups.First().SubCommands.First(x => x.Name == command.Name); var method = group.Methods.First(x => x.Key == command.Options.First().Name).Value; var args = await this.ResolveInteractionCommandParameters(e, context, method, e.Interaction.Data.Options.First().Options.First().Options); await this.RunCommandAsync(context, method, args); } await this._slashExecuted.InvokeAsync(this, new SlashCommandExecutedEventArgs(this.Client.ServiceProvider) { Context = context }); } catch (Exception ex) { await this._slashError.InvokeAsync(this, new SlashCommandErrorEventArgs(this.Client.ServiceProvider) { Context = context, Exception = ex }); } } else if (e.Interaction.Type == InteractionType.AutoComplete) { if (_errored) throw new InvalidOperationException("Slash commands failed to register properly on startup."); var methods = _commandMethods.Where(x => x.CommandId == e.Interaction.Data.Id); var groups = _groupCommands.Where(x => x.CommandId == e.Interaction.Data.Id); var subgroups = _subGroupCommands.Where(x => x.CommandId == e.Interaction.Data.Id); if (!methods.Any() && !groups.Any() && !subgroups.Any()) throw new InvalidOperationException("An autocomplete interaction was created, but no command was registered for it."); try { if (methods.Any()) { var focusedOption = e.Interaction.Data.Options.First(o => o.Focused); var method = methods.First().Method; var option = method.GetParameters().Skip(1).First(p => p.GetCustomAttribute().Name == focusedOption.Name); var provider = option.GetCustomAttribute().ProviderType; var providerMethod = provider.GetMethod(nameof(IAutocompleteProvider.Provider)); var providerInstance = Activator.CreateInstance(provider); var context = new AutocompleteContext { Interaction = e.Interaction, Client = this.Client, Services = this._configuration?.ServiceProvider, ApplicationCommandsExtension = this, Guild = e.Interaction.Guild, Channel = e.Interaction.Channel, User = e.Interaction.User, Options = e.Interaction.Data.Options.ToList(), FocusedOption = focusedOption, Locale = e.Interaction.Locale, GuildLocale = e.Interaction.GuildLocale }; var choices = await (Task>) providerMethod.Invoke(providerInstance, new[] { context }); await e.Interaction.CreateResponseAsync(InteractionResponseType.AutoCompleteResult, new DiscordInteractionResponseBuilder().AddAutoCompleteChoices(choices)); } else if (groups.Any()) { var command = e.Interaction.Data.Options.First(); var group = groups.First().Methods.First(x => x.Key == command.Name).Value; var focusedOption = command.Options.First(o => o.Focused); var option = group.GetParameters().Skip(1).First(p => p.GetCustomAttribute().Name == focusedOption.Name); var provider = option.GetCustomAttribute().ProviderType; var providerMethod = provider.GetMethod(nameof(IAutocompleteProvider.Provider)); var providerInstance = Activator.CreateInstance(provider); var context = new AutocompleteContext { Interaction = e.Interaction, Services = this._configuration?.ServiceProvider, ApplicationCommandsExtension = this, Guild = e.Interaction.Guild, Channel = e.Interaction.Channel, User = e.Interaction.User, Options = command.Options.ToList(), FocusedOption = focusedOption, Locale = e.Interaction.Locale, GuildLocale = e.Interaction.GuildLocale }; var choices = await (Task>) providerMethod.Invoke(providerInstance, new[] { context }); await e.Interaction.CreateResponseAsync(InteractionResponseType.AutoCompleteResult, new DiscordInteractionResponseBuilder().AddAutoCompleteChoices(choices)); } /*else if (subgroups.Any()) { var command = e.Interaction.Data.Options.First(); var method = methods.First().Method; var group = subgroups.First().SubCommands.First(x => x.Name == command.Name); var focusedOption = command.Options.First(x => x.Name == group.Name).Options.First(o => o.Focused); this.Client.Logger.LogDebug("SUBGROUP::" + focusedOption.Name + ": " + focusedOption.RawValue); var option = group.Methods.First(p => p.Value.GetCustomAttribute().Name == focusedOption.Name).Value; var provider = option.GetCustomAttribute().ProviderType; var providerMethod = provider.GetMethod(nameof(IAutocompleteProvider.Provider)); var providerInstance = Activator.CreateInstance(provider); var context = new AutocompleteContext { Interaction = e.Interaction, Services = this._configuration?.Services, ApplicationCommandsExtension = this, Guild = e.Interaction.Guild, Channel = e.Interaction.Channel, User = e.Interaction.User, Options = command.Options.First(x => x.Name == group.Name).Options.ToList(), FocusedOption = focusedOption }; var choices = await (Task>) providerMethod.Invoke(providerInstance, new[] { context }); await e.Interaction.CreateResponseAsync(InteractionResponseType.AutoCompleteResult, new DiscordInteractionResponseBuilder().AddAutoCompleteChoices(choices)); }*/ } catch (Exception ex) { this.Client.Logger.LogError(ex, "Error in autocomplete interaction"); } } }); return Task.CompletedTask; } /// /// Context menu handler. /// /// The client. /// The event args. private Task ContextMenuHandler(DiscordClient client, ContextMenuInteractionCreateEventArgs e) { _ = Task.Run(async () => { //Creates the context var context = new ContextMenuContext { Interaction = e.Interaction, Channel = e.Interaction.Channel, Client = client, Services = this._configuration?.ServiceProvider, CommandName = e.Interaction.Data.Name, ApplicationCommandsExtension = this, Guild = e.Interaction.Guild, InteractionId = e.Interaction.Id, User = e.Interaction.User, Token = e.Interaction.Token, TargetUser = e.TargetUser, TargetMessage = e.TargetMessage, Type = e.Type, Locale = e.Interaction.Locale, GuildLocale = e.Interaction.GuildLocale }; try { if (_errored) throw new InvalidOperationException("Context menus failed to register properly on startup."); //Gets the method for the command var method = _contextMenuCommands.FirstOrDefault(x => x.CommandId == e.Interaction.Data.Id); if (method == null) throw new InvalidOperationException("A context menu was executed, but no command was registered for it."); await this.RunCommandAsync(context, method.Method, new[] { context }); await this._contextMenuExecuted.InvokeAsync(this, new ContextMenuExecutedEventArgs(this.Client.ServiceProvider) { Context = context }); } catch (Exception ex) { await this._contextMenuErrored.InvokeAsync(this, new ContextMenuErrorEventArgs(this.Client.ServiceProvider) { Context = context, Exception = ex }); } }); return Task.CompletedTask; } /// /// Runs a command. /// /// The base context. /// The method info. /// The arguments. [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0066:Convert switch statement to expression", Justification = "")] internal async Task RunCommandAsync(BaseContext context, MethodInfo method, IEnumerable args) { object classInstance; //Accounts for lifespans var moduleLifespan = (method.DeclaringType.GetCustomAttribute() != null ? method.DeclaringType.GetCustomAttribute()?.Lifespan : ApplicationCommandModuleLifespan.Transient) ?? ApplicationCommandModuleLifespan.Transient; switch (moduleLifespan) { case ApplicationCommandModuleLifespan.Scoped: //Accounts for static methods and adds DI classInstance = method.IsStatic ? ActivatorUtilities.CreateInstance(this._configuration?.ServiceProvider.CreateScope().ServiceProvider, method.DeclaringType) : this.CreateInstance(method.DeclaringType, this._configuration?.ServiceProvider.CreateScope().ServiceProvider); break; case ApplicationCommandModuleLifespan.Transient: //Accounts for static methods and adds DI classInstance = method.IsStatic ? ActivatorUtilities.CreateInstance(this._configuration?.ServiceProvider, method.DeclaringType) : this.CreateInstance(method.DeclaringType, this._configuration?.ServiceProvider); break; //If singleton, gets it from the singleton list case ApplicationCommandModuleLifespan.Singleton: classInstance = _singletonModules.First(x => ReferenceEquals(x.GetType(), method.DeclaringType)); break; default: throw new Exception($"An unknown {nameof(ApplicationCommandModuleLifespanAttribute)} scope was specified on command {context.CommandName}"); } ApplicationCommandsModule module = null; if (classInstance is ApplicationCommandsModule mod) module = mod; // Slash commands if (context is InteractionContext slashContext) { await this.RunPreexecutionChecksAsync(method, slashContext); var shouldExecute = await (module?.BeforeSlashExecutionAsync(slashContext) ?? Task.FromResult(true)); if (shouldExecute) { await (Task)method.Invoke(classInstance, args.ToArray()); await (module?.AfterSlashExecutionAsync(slashContext) ?? Task.CompletedTask); } } // Context menus if (context is ContextMenuContext contextMenuContext) { await this.RunPreexecutionChecksAsync(method, contextMenuContext); var shouldExecute = await (module?.BeforeContextMenuExecutionAsync(contextMenuContext) ?? Task.FromResult(true)); if (shouldExecute) { await (Task)method.Invoke(classInstance, args.ToArray()); await (module?.AfterContextMenuExecutionAsync(contextMenuContext) ?? Task.CompletedTask); } } } /// - /// Property injection copied over from CommandsNext + /// Property injection /// /// The type. /// The services. internal object CreateInstance(Type t, IServiceProvider services) { var ti = t.GetTypeInfo(); var constructors = ti.DeclaredConstructors .Where(xci => xci.IsPublic) .ToArray(); if (constructors.Length != 1) throw new ArgumentException("Specified type does not contain a public constructor or contains more than one public constructor."); var constructor = constructors[0]; var constructorArgs = constructor.GetParameters(); var args = new object[constructorArgs.Length]; if (constructorArgs.Length != 0 && services == null) throw new InvalidOperationException("Dependency collection needs to be specified for parameterized constructors."); // inject via constructor if (constructorArgs.Length != 0) for (var i = 0; i < args.Length; i++) args[i] = services.GetRequiredService(constructorArgs[i].ParameterType); var moduleInstance = Activator.CreateInstance(t, args); // inject into properties var props = t.GetRuntimeProperties().Where(xp => xp.CanWrite && xp.SetMethod != null && !xp.SetMethod.IsStatic && xp.SetMethod.IsPublic); foreach (var prop in props) { if (prop.GetCustomAttribute() != null) continue; var service = services.GetService(prop.PropertyType); if (service == null) continue; prop.SetValue(moduleInstance, service); } // inject into fields var fields = t.GetRuntimeFields().Where(xf => !xf.IsInitOnly && !xf.IsStatic && xf.IsPublic); foreach (var field in fields) { if (field.GetCustomAttribute() != null) continue; var service = services.GetService(field.FieldType); if (service == null) continue; field.SetValue(moduleInstance, service); } return moduleInstance; } /// /// Resolves the slash command parameters. /// /// The event arguments. /// The interaction context. /// The method info. /// The options. private async Task> ResolveInteractionCommandParameters(InteractionCreateEventArgs e, InteractionContext context, MethodInfo method, IEnumerable options) { var args = new List { context }; var parameters = method.GetParameters().Skip(1); for (var i = 0; i < parameters.Count(); i++) { var parameter = parameters.ElementAt(i); //Accounts for optional arguments without values given - if (parameter.IsOptional && (options == null || - (!options?.Any(x => x.Name == parameter.GetCustomAttribute().Name.ToLower()) ?? true))) + if (parameter.IsOptional && (options == null || (!options?.Any(x => x.Name == parameter.GetCustomAttribute().Name.ToLower()) ?? true))) args.Add(parameter.DefaultValue); else { var option = options.Single(x => x.Name == parameter.GetCustomAttribute().Name.ToLower()); if (parameter.ParameterType == typeof(string)) args.Add(option.Value.ToString()); else if (parameter.ParameterType.IsEnum) args.Add(Enum.Parse(parameter.ParameterType, (string)option.Value)); else if (parameter.ParameterType == typeof(long) || parameter.ParameterType == typeof(long?)) args.Add((long?)option.Value); else if (parameter.ParameterType == typeof(bool) || parameter.ParameterType == typeof(bool?)) args.Add((bool?)option.Value); else if (parameter.ParameterType == typeof(double) || parameter.ParameterType == typeof(double?)) args.Add((double?)option.Value); else if(parameter.ParameterType == typeof(int) || parameter.ParameterType == typeof(int?)) args.Add((int?)option.Value); else if (parameter.ParameterType == typeof(DiscordAttachment)) { //Checks through resolved if (e.Interaction.Data.Resolved.Attachments != null && e.Interaction.Data.Resolved.Attachments.TryGetValue((ulong)option.Value, out var attachment)) args.Add(attachment); + else + args.Add(new DiscordAttachment() { Id = (ulong)option.Value, Discord = this.Client.ApiClient.Discord }); } else if (parameter.ParameterType == typeof(DiscordUser)) { //Checks through resolved if (e.Interaction.Data.Resolved.Members != null && e.Interaction.Data.Resolved.Members.TryGetValue((ulong)option.Value, out var member)) args.Add(member); else if (e.Interaction.Data.Resolved.Users != null && e.Interaction.Data.Resolved.Users.TryGetValue((ulong)option.Value, out var user)) args.Add(user); else args.Add(await this.Client.GetUserAsync((ulong)option.Value)); } else if (parameter.ParameterType == typeof(DiscordChannel)) { //Checks through resolved if (e.Interaction.Data.Resolved.Channels != null && e.Interaction.Data.Resolved.Channels.TryGetValue((ulong)option.Value, out var channel)) args.Add(channel); else args.Add(e.Interaction.Guild.GetChannel((ulong)option.Value)); } else if (parameter.ParameterType == typeof(DiscordRole)) { //Checks through resolved if (e.Interaction.Data.Resolved.Roles != null && e.Interaction.Data.Resolved.Roles.TryGetValue((ulong)option.Value, out var role)) args.Add(role); else args.Add(e.Interaction.Guild.GetRole((ulong)option.Value)); } else if (parameter.ParameterType == typeof(SnowflakeObject)) { //Checks through resolved if (e.Interaction.Data.Resolved.Roles != null && e.Interaction.Data.Resolved.Roles.TryGetValue((ulong)option.Value, out var role)) args.Add(role); else if (e.Interaction.Data.Resolved.Members != null && e.Interaction.Data.Resolved.Members.TryGetValue((ulong)option.Value, out var member)) args.Add(member); else if (e.Interaction.Data.Resolved.Users != null && e.Interaction.Data.Resolved.Users.TryGetValue((ulong)option.Value, out var user)) args.Add(user); else if (e.Interaction.Data.Resolved.Attachments != null && e.Interaction.Data.Resolved.Attachments.TryGetValue((ulong)option.Value, out var attachment)) args.Add(attachment); else throw new ArgumentException("Error resolving mentionable option."); } else throw new ArgumentException($"Error resolving interaction."); } } return args; } /// /// Runs the preexecution checks. /// /// The method info. /// The base context. private async Task RunPreexecutionChecksAsync(MethodInfo method, BaseContext context) { if (context is InteractionContext ctx) { //Gets all attributes from parent classes as well and stuff var attributes = new List(); attributes.AddRange(method.GetCustomAttributes(true)); attributes.AddRange(method.DeclaringType.GetCustomAttributes()); if (method.DeclaringType.DeclaringType != null) { attributes.AddRange(method.DeclaringType.DeclaringType.GetCustomAttributes()); if (method.DeclaringType.DeclaringType.DeclaringType != null) { attributes.AddRange(method.DeclaringType.DeclaringType.DeclaringType.GetCustomAttributes()); } } var dict = new Dictionary(); foreach (var att in attributes) { //Runs the check and adds the result to a list var result = await att.ExecuteChecksAsync(ctx); dict.Add(att, result); } //Checks if any failed, and throws an exception if (dict.Any(x => x.Value == false)) throw new SlashExecutionChecksFailedException { FailedChecks = dict.Where(x => x.Value == false).Select(x => x.Key).ToList() }; } if (context is ContextMenuContext CMctx) { var attributes = new List(); attributes.AddRange(method.GetCustomAttributes(true)); attributes.AddRange(method.DeclaringType.GetCustomAttributes()); if (method.DeclaringType.DeclaringType != null) { attributes.AddRange(method.DeclaringType.DeclaringType.GetCustomAttributes()); if (method.DeclaringType.DeclaringType.DeclaringType != null) { attributes.AddRange(method.DeclaringType.DeclaringType.DeclaringType.GetCustomAttributes()); } } var dict = new Dictionary(); foreach (var att in attributes) { //Runs the check and adds the result to a list var result = await att.ExecuteChecksAsync(CMctx); dict.Add(att, result); } //Checks if any failed, and throws an exception if (dict.Any(x => x.Value == false)) throw new ContextMenuExecutionChecksFailedException { FailedChecks = dict.Where(x => x.Value == false).Select(x => x.Key).ToList() }; } } /// /// Gets the choice attributes from choice provider. /// /// The custom attributes. /// The optional guild id private 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 ApplicationCommandOptionType GetParameterType(Type type) { var parametertype = type == typeof(string) ? ApplicationCommandOptionType.String : type == typeof(long) || type == typeof(long?) ? ApplicationCommandOptionType.Integer : type == typeof(int) || type == typeof(int?) ? ApplicationCommandOptionType.Integer : type == typeof(bool) || type == typeof(bool?) ? ApplicationCommandOptionType.Boolean : type == typeof(double) || type == typeof(double?) ? ApplicationCommandOptionType.Number : type == typeof(DiscordChannel) ? ApplicationCommandOptionType.Channel : type == typeof(DiscordUser) ? ApplicationCommandOptionType.User : type == typeof(DiscordRole) ? ApplicationCommandOptionType.Role : type == typeof(SnowflakeObject) ? ApplicationCommandOptionType.Mentionable : type == typeof(DiscordAttachment) ? ApplicationCommandOptionType.Attachment : type.IsEnum ? ApplicationCommandOptionType.String : throw new ArgumentException("Cannot convert type! Argument types must be string, int, long, bool, double, DiscordChannel, DiscordUser, DiscordRole, SnowflakeObject, DiscordAttachment or an Enum."); return parametertype; } /// /// Gets the choice attributes from parameter. /// /// The choice attributes. private List GetChoiceAttributesFromParameter(IEnumerable choiceattributes) { return !choiceattributes.Any() ? null : choiceattributes.Select(att => new DiscordApplicationCommandOptionChoice(att.Name, att.Value)).ToList(); } /// /// Parses the parameters. /// /// The parameters. /// The optional guild id. private async Task> ParseParameters(ParameterInfo[] parameters, ulong? guildId) { var options = new List(); foreach (var parameter in parameters) { //Gets the attribute var optionattribute = parameter.GetCustomAttribute(); if (optionattribute == null) throw new ArgumentException("Arguments must have the Option attribute!"); var minimumValue = parameter.GetCustomAttribute()?.Value ?? null; var maximumValue = parameter.GetCustomAttribute()?.Value ?? null; var autocompleteAttribute = parameter.GetCustomAttribute(); if (optionattribute.Autocomplete && autocompleteAttribute == null) throw new ArgumentException("Autocomplete options must have the Autocomplete attribute!"); if (!optionattribute.Autocomplete && autocompleteAttribute != null) throw new ArgumentException("Setting an autocomplete provider requires the option to have autocomplete set to true!"); //Sets the type var type = parameter.ParameterType; var parametertype = this.GetParameterType(type); //Handles choices //From attributes var choices = this.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 this.GetChoiceAttributesFromProvider(choiceProviders, guildId); } var channelTypes = parameter.GetCustomAttribute()?.ChannelTypes ?? null; options.Add(new DiscordApplicationCommandOption(optionattribute.Name, optionattribute.Description, parametertype, !parameter.IsOptional, choices, null, channelTypes, optionattribute.Autocomplete, minimumValue, maximumValue)); } return options; } /// /// Refreshes your commands, used for refreshing choice providers or applying commands registered after the ready event on the discord client. /// Should only be run on the slash command extension linked to shard 0 if sharding. /// Not recommended and should be avoided since it can make slash commands be unresponsive for a while. /// public async Task RefreshCommandsAsync() { _commandMethods.Clear(); _groupCommands.Clear(); _subGroupCommands.Clear(); _registeredCommands.Clear(); _contextMenuCommands.Clear(); - _globalDiscordCommands.Clear(); - _guildDiscordCommands.Clear(); + this._globalDiscordCommands.Clear(); + this._guildDiscordCommands.Clear(); + this._globalDiscordCommands = null; + this._guildDiscordCommands = null; _guildCommands.Clear(); _globalCommands.Clear(); await this.UpdateAsync(); } /// /// Fires when the execution of a slash command fails. /// public event AsyncEventHandler SlashCommandErrored { add { this._slashError.Register(value); } remove { this._slashError.Unregister(value); } } private AsyncEvent _slashError; /// /// Fires when the execution of a slash command is successful. /// public event AsyncEventHandler SlashCommandExecuted { add { this._slashExecuted.Register(value); } remove { this._slashExecuted.Unregister(value); } } private AsyncEvent _slashExecuted; /// /// Fires when the execution of a context menu fails. /// public event AsyncEventHandler ContextMenuErrored { add { this._contextMenuErrored.Register(value); } remove { this._contextMenuErrored.Unregister(value); } } private AsyncEvent _contextMenuErrored; /// /// Fire when the execution of a context menu is successful. /// public event AsyncEventHandler ContextMenuExecuted { add { this._contextMenuExecuted.Register(value); } remove { this._contextMenuExecuted.Unregister(value); } } private AsyncEvent _contextMenuExecuted; } /// /// Holds configuration data for setting up an application command. /// internal class ApplicationCommandsModuleConfiguration { /// /// The type of the command module. /// public Type Type { get; } /// /// The permission setup. /// public Action Setup { get; } public Action Translations { get; } /// /// Creates a new command configuration. /// /// The type of the command module. /// The permission setup callback. /// The translation setup callback. public ApplicationCommandsModuleConfiguration(Type type, Action setup = null, Action translations = null) { this.Type = type; this.Setup = setup; this.Translations = translations; } } /// /// Links a command to its original command module. /// internal class ApplicationCommandSourceLink { /// /// The command. /// public DiscordApplicationCommand ApplicationCommand { get; set; } /// /// The base/root module the command is contained in. /// public Type RootCommandContainerType { get; set; } /// /// The direct group the command is contained in. /// public Type CommandContainerType { get; set; } } /// /// The command method. /// internal class CommandMethod { /// /// Gets or sets the command id. /// public ulong CommandId { get; set; } /// /// Gets or sets the name. /// public string Name { get; set; } /// /// Gets or sets the method. /// public MethodInfo Method { get; set; } } /// /// The group command. /// internal class GroupCommand { /// /// Gets or sets the command id. /// public ulong CommandId { get; set; } /// /// Gets or sets the name. /// public string Name { get; set; } /// /// Gets or sets the methods. /// public List> Methods { get; set; } = null; } /// /// The sub group command. /// internal class SubGroupCommand { /// /// Gets or sets the command id. /// public ulong CommandId { get; set; } /// /// Gets or sets the name. /// public string Name { get; set; } /// /// Gets or sets the sub commands. /// public List SubCommands { get; set; } = new List(); } /// /// The context menu command. /// internal class ContextMenuCommand { /// /// Gets or sets the command id. /// public ulong CommandId { get; set; } /// /// Gets or sets the name. /// public string Name { get; set; } /// /// Gets or sets the method. /// public MethodInfo Method { get; set; } } } diff --git a/DisCatSharp.ApplicationCommands/Attributes/SlashCommand/SlashCommandAttribute.cs b/DisCatSharp.ApplicationCommands/Attributes/SlashCommand/SlashCommandAttribute.cs index 29a221f6b..d4d38969c 100644 --- a/DisCatSharp.ApplicationCommands/Attributes/SlashCommand/SlashCommandAttribute.cs +++ b/DisCatSharp.ApplicationCommands/Attributes/SlashCommand/SlashCommandAttribute.cs @@ -1,61 +1,71 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; namespace DisCatSharp.ApplicationCommands { /// /// Marks this method as a slash command /// [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] public class SlashCommandAttribute : Attribute { /// /// Gets the name of this command /// - public string Name { get; } + public string Name { get; set; } /// /// Gets the description of this command /// - public string Description { get; } + public string Description { get; set; } /// /// Gets the default permission of this command /// public bool DefaultPermission { get; } + /// + /// Gets the needed permission of this command + /// + public Permissions? Permission { get; set; } + + /// + /// Gets the dm permission of this command + /// + public bool? DmPermission { get; set; } + /// /// Marks this method as a slash command /// /// The name of this slash command. /// The description of this slash command. /// Whether everyone can execute this command. public SlashCommandAttribute(string name, string description, bool default_permission = true) { this.Name = name.ToLower(); this.Description = description; this.DefaultPermission = default_permission; } } } diff --git a/DisCatSharp.ApplicationCommands/Attributes/SlashCommand/SlashCommandGroupAttribute.cs b/DisCatSharp.ApplicationCommands/Attributes/SlashCommand/SlashCommandGroupAttribute.cs index aea710da2..b1fab1d93 100644 --- a/DisCatSharp.ApplicationCommands/Attributes/SlashCommand/SlashCommandGroupAttribute.cs +++ b/DisCatSharp.ApplicationCommands/Attributes/SlashCommand/SlashCommandGroupAttribute.cs @@ -1,61 +1,71 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; namespace DisCatSharp.ApplicationCommands { /// /// Marks this class a slash command group /// [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] public class SlashCommandGroupAttribute : Attribute { /// /// Gets the name of this slash command group /// public string Name { get; set; } /// /// Gets the description of this slash command group /// public string Description { get; set; } /// /// Gets the default permission of this slash command group /// public bool DefaultPermission { get; set; } + /// + /// Gets the needed permission of this slash command group + /// + public Permissions? Permission { get; set; } + + /// + /// Gets the dm permission of this slash command group + /// + public bool? DmPermission { get; set; } + /// /// Marks this class as a slash command group /// /// The name of this slash command group. /// The description of this slash command group. /// Whether everyone can execute this command. public SlashCommandGroupAttribute(string name, string description, bool default_permission = true) { this.Name = name.ToLower(); this.Description = description; this.DefaultPermission = default_permission; } } } diff --git a/DisCatSharp.Interactivity/EventHandling/Components/ComponentPaginator.cs b/DisCatSharp.Interactivity/EventHandling/Components/ComponentPaginator.cs index 7dd22f096..bf618c967 100644 --- a/DisCatSharp.Interactivity/EventHandling/Components/ComponentPaginator.cs +++ b/DisCatSharp.Interactivity/EventHandling/Components/ComponentPaginator.cs @@ -1,178 +1,181 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Generic; using System.Threading.Tasks; using DisCatSharp.Entities; using DisCatSharp.EventArgs; using DisCatSharp.Interactivity.Enums; using Microsoft.Extensions.Logging; namespace DisCatSharp.Interactivity.EventHandling { /// /// The component paginator. /// internal class ComponentPaginator : IPaginator { private readonly DiscordClient _client; private readonly InteractivityConfiguration _config; private readonly DiscordMessageBuilder _builder = new(); private readonly Dictionary _requests = new(); /// /// Initializes a new instance of the class. /// /// The client. /// The config. public ComponentPaginator(DiscordClient client, InteractivityConfiguration config) { this._client = client; this._client.ComponentInteractionCreated += this.Handle; this._config = config; } /// /// Does the pagination async. /// /// The request. public async Task DoPaginationAsync(IPaginationRequest request) { var id = (await request.GetMessageAsync().ConfigureAwait(false)).Id; this._requests.Add(id, request); try { var tcs = await request.GetTaskCompletionSourceAsync().ConfigureAwait(false); await tcs.Task.ConfigureAwait(false); } catch (Exception ex) { this._client.Logger.LogError(InteractivityEvents.InteractivityPaginationError, ex, "There was an exception while paginating."); } finally { this._requests.Remove(id); try { await request.DoCleanupAsync().ConfigureAwait(false); } catch (Exception ex) { this._client.Logger.LogError(InteractivityEvents.InteractivityPaginationError, ex, "There was an exception while cleaning up pagination."); } } } /// /// Disposes the paginator. /// public void Dispose() => this._client.ComponentInteractionCreated -= this.Handle; /// /// Handles the pagination event. /// /// The client. /// The event arguments. private async Task Handle(DiscordClient _, ComponentInteractionCreateEventArgs e) { + if (e.Interaction.Type == InteractionType.ModalSubmit) + return; + if (!this._requests.TryGetValue(e.Message.Id, out var req)) return; if (this._config.AckPaginationButtons) { e.Handled = true; await e.Interaction.CreateResponseAsync(InteractionResponseType.DeferredMessageUpdate).ConfigureAwait(false); } if (await req.GetUserAsync().ConfigureAwait(false) != e.User) { if (this._config.ResponseBehavior is InteractionResponseBehavior.Respond) await e.Interaction.CreateFollowupMessageAsync(new() { Content = this._config.ResponseMessage, IsEphemeral = true }).ConfigureAwait(false); return; } if (req is InteractionPaginationRequest ipr) ipr.RegenerateCTS(e.Interaction); // Necessary to ensure we don't prematurely yeet the CTS // await this.HandlePaginationAsync(req, e).ConfigureAwait(false); } /// /// Handles the pagination async. /// /// The request. /// The arguments. private async Task HandlePaginationAsync(IPaginationRequest request, ComponentInteractionCreateEventArgs args) { var buttons = this._config.PaginationButtons; var msg = await request.GetMessageAsync().ConfigureAwait(false); var id = args.Id; var tcs = await request.GetTaskCompletionSourceAsync().ConfigureAwait(false); #pragma warning disable CS8846 // The switch expression does not handle all possible values of its input type (it is not exhaustive). var paginationTask = id switch #pragma warning restore CS8846 // The switch expression does not handle all possible values of its input type (it is not exhaustive). { _ when id == buttons.SkipLeft.CustomId => request.SkipLeftAsync(), _ when id == buttons.SkipRight.CustomId => request.SkipRightAsync(), _ when id == buttons.Stop.CustomId => Task.FromResult(tcs.TrySetResult(true)), _ when id == buttons.Left.CustomId => request.PreviousPageAsync(), _ when id == buttons.Right.CustomId => request.NextPageAsync(), }; await paginationTask.ConfigureAwait(false); if (id == buttons.Stop.CustomId) return; var page = await request.GetPageAsync().ConfigureAwait(false); var bts = await request.GetButtonsAsync().ConfigureAwait(false); if (request is InteractionPaginationRequest ipr) { var builder = new DiscordWebhookBuilder() .WithContent(page.Content) .AddEmbed(page.Embed) .AddComponents(bts); await args.Interaction.EditOriginalResponseAsync(builder).ConfigureAwait(false); return; } this._builder.Clear(); this._builder .WithContent(page.Content) .AddEmbed(page.Embed) .AddComponents(bts); await this._builder.ModifyAsync(msg).ConfigureAwait(false); } } } diff --git a/DisCatSharp.Interactivity/EventHandling/Components/ModalEventWaiter.cs b/DisCatSharp.Interactivity/EventHandling/Components/ModalEventWaiter.cs new file mode 100644 index 000000000..dcb990f83 --- /dev/null +++ b/DisCatSharp.Interactivity/EventHandling/Components/ModalEventWaiter.cs @@ -0,0 +1,112 @@ +// This file is part of the DisCatSharp project. +// +// Copyright (c) 2021 AITSYS +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; +using ConcurrentCollections; +using DisCatSharp.Entities; +using DisCatSharp.EventArgs; +using DisCatSharp.Interactivity.Enums; +using Microsoft.Extensions.Logging; + +namespace DisCatSharp.Interactivity.EventHandling +{ + /// + /// A modal-based version of + /// + internal class ModalEventWaiter : IDisposable + { + private readonly DiscordClient _client; + private readonly ConcurrentHashSet _modalMatchRequests = new(); + + private readonly DiscordFollowupMessageBuilder _message; + private readonly InteractivityConfiguration _config; + + /// + /// Initializes a new instance of the class. + /// + /// The client. + /// The config. + public ModalEventWaiter(DiscordClient client, InteractivityConfiguration config) + { + this._client = client; + this._client.ComponentInteractionCreated += this.Handle; + this._config = config; + + this._message = new() { Content = config.ResponseMessage ?? "This modal was not meant for you.", IsEphemeral = true }; + } + + /// + /// Waits for a specified 's predicate to be fufilled. + /// + /// The request to wait for. + /// The returned args, or null if it timed out. + public async Task WaitForModalMatchAsync(ModalMatchRequest request) + { + this._modalMatchRequests.Add(request); + + try + { + return await request.Tcs.Task.ConfigureAwait(false); + } + catch (Exception e) + { + this._client.Logger.LogError(InteractivityEvents.InteractivityWaitError, e, "An exception was thrown while waiting for modals."); + return null; + } + finally + { + this._modalMatchRequests.TryRemove(request); + } + } + + /// + /// Handles the waiter. + /// + /// The client. + /// The args. + private async Task Handle(DiscordClient _, ComponentInteractionCreateEventArgs args) + { + foreach (var mreq in this._modalMatchRequests.ToArray()) + { + if (mreq.CustomId == args.Interaction.Data.CustomId && mreq.IsMatch(args)) + mreq.Tcs.TrySetResult(args); + + else if (this._config.ResponseBehavior is InteractionResponseBehavior.Respond) + await args.Interaction.CreateFollowupMessageAsync(this._message).ConfigureAwait(false); + } + } + + /// + /// Disposes the waiter. + /// + public void Dispose() + { + this._modalMatchRequests.Clear(); + this._client.ComponentInteractionCreated -= this.Handle; + } + } +} diff --git a/DisCatSharp.Interactivity/EventHandling/Components/Requests/ModalMatchRequest.cs b/DisCatSharp.Interactivity/EventHandling/Components/Requests/ModalMatchRequest.cs new file mode 100644 index 000000000..ee3a69787 --- /dev/null +++ b/DisCatSharp.Interactivity/EventHandling/Components/Requests/ModalMatchRequest.cs @@ -0,0 +1,69 @@ +// This file is part of the DisCatSharp project. +// +// Copyright (c) 2021 AITSYS +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using System; +using System.Threading; +using System.Threading.Tasks; +using DisCatSharp.Entities; +using DisCatSharp.EventArgs; + +namespace DisCatSharp.Interactivity.EventHandling +{ + /// + /// Represents a match that is being waited for. + /// + internal class ModalMatchRequest + { + /// + /// The id to wait on. This should be uniquely formatted to avoid collisions. + /// + public string CustomId { get; private set; } + + /// + /// The completion source that represents the result of the match. + /// + public TaskCompletionSource Tcs { get; private set; } = new(); + + protected readonly CancellationToken _cancellation; + protected readonly Func _predicate; + + /// + /// Initializes a new instance of the class. + /// + /// The custom id. + /// The predicate. + /// The cancellation token. + public ModalMatchRequest(string custom_id, Func predicate, CancellationToken cancellation) + { + this.CustomId = custom_id; + this._predicate = predicate; + this._cancellation = cancellation; + this._cancellation.Register(() => this.Tcs.TrySetResult(null)); // TrySetCancelled would probably be better but I digress ~Velvet // + } + + /// + /// Whether it is a match. + /// + /// The arguments. + public bool IsMatch(ComponentInteractionCreateEventArgs args) => this._predicate(args); + } +} diff --git a/DisCatSharp.Interactivity/InteractivityExtension.cs b/DisCatSharp.Interactivity/InteractivityExtension.cs index fc93fcfaf..73b64abef 100644 --- a/DisCatSharp.Interactivity/InteractivityExtension.cs +++ b/DisCatSharp.Interactivity/InteractivityExtension.cs @@ -1,930 +1,962 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Threading.Tasks; using DisCatSharp.Entities; using DisCatSharp.EventArgs; using DisCatSharp.Interactivity.Enums; using DisCatSharp.Interactivity.EventHandling; using DisCatSharp.Common.Utilities; using DisCatSharp.Enums; using System.Threading; using System.Globalization; namespace DisCatSharp.Interactivity { /// /// Extension class for DisCatSharp.Interactivity /// public class InteractivityExtension : BaseExtension { #pragma warning disable IDE1006 // Naming Styles /// /// Gets the config. /// internal InteractivityConfiguration Config { get; } private EventWaiter MessageCreatedWaiter; private EventWaiter MessageReactionAddWaiter; private EventWaiter TypingStartWaiter; + private EventWaiter ModalInteractionWaiter; + private EventWaiter ComponentInteractionWaiter; private ComponentEventWaiter ComponentEventWaiter; + private ModalEventWaiter ModalEventWaiter; + private ReactionCollector ReactionCollector; private Poller Poller; private Paginator Paginator; private ComponentPaginator _compPaginator; #pragma warning restore IDE1006 // Naming Styles /// /// Initializes a new instance of the class. /// /// The configuration. internal InteractivityExtension(InteractivityConfiguration cfg) { this.Config = new InteractivityConfiguration(cfg); } /// /// Setups the Interactivity Extension. /// /// Discord client. protected internal override void Setup(DiscordClient client) { this.Client = client; this.MessageCreatedWaiter = new EventWaiter(this.Client); this.MessageReactionAddWaiter = new EventWaiter(this.Client); this.ComponentInteractionWaiter = new EventWaiter(this.Client); + this.ModalInteractionWaiter = new EventWaiter(this.Client); this.TypingStartWaiter = new EventWaiter(this.Client); this.Poller = new Poller(this.Client); this.ReactionCollector = new ReactionCollector(this.Client); this.Paginator = new Paginator(this.Client); this._compPaginator = new(this.Client, this.Config); this.ComponentEventWaiter = new(this.Client, this.Config); + this.ModalEventWaiter = new(this.Client, this.Config); } /// /// Makes a poll and returns poll results. /// /// Message to create poll on. /// Emojis to use for this poll. /// What to do when the poll ends. /// override timeout period. /// public async Task> DoPollAsync(DiscordMessage m, IEnumerable emojis, PollBehaviour? behaviour = default, TimeSpan? timeout = null) { if (!Utilities.HasReactionIntents(this.Client.Configuration.Intents)) throw new InvalidOperationException("No reaction intents are enabled."); if (!emojis.Any()) throw new ArgumentException("You need to provide at least one emoji for a poll!"); foreach (var em in emojis) await m.CreateReactionAsync(em).ConfigureAwait(false); var res = await this.Poller.DoPollAsync(new PollRequest(m, timeout ?? this.Config.Timeout, emojis)).ConfigureAwait(false); var pollbehaviour = behaviour ?? this.Config.PollBehaviour; var thismember = await m.Channel.Guild.GetMemberAsync(this.Client.CurrentUser.Id).ConfigureAwait(false); if (pollbehaviour == PollBehaviour.DeleteEmojis && m.Channel.PermissionsFor(thismember).HasPermission(Permissions.ManageMessages)) await m.DeleteAllReactionsAsync().ConfigureAwait(false); return new ReadOnlyCollection(res.ToList()); } /// /// Waits for any button in the specified collection to be pressed. /// /// The message to wait on. /// A collection of buttons to listen for. /// Override the timeout period in . /// A with the result of button that was pressed, if any. /// Thrown when attempting to wait for a message that is not authored by the current user. /// Thrown when the message does not contain a button with the specified Id, or any buttons at all. public Task> WaitForButtonAsync(DiscordMessage message, IEnumerable buttons, TimeSpan? timeoutOverride = null) => this.WaitForButtonAsync(message, buttons, this.GetCancellationToken(timeoutOverride)); /// /// Waits for any button in the specified collection to be pressed. /// /// The message to wait on. /// A collection of buttons to listen for. /// A custom cancellation token that can be cancelled at any point. /// A with the result of button that was pressed, if any. /// Thrown when attempting to wait for a message that is not authored by the current user. /// Thrown when the message does not contain a button with the specified Id, or any buttons at all. public async Task> WaitForButtonAsync(DiscordMessage message, IEnumerable buttons, CancellationToken token) { if (message.Author != this.Client.CurrentUser) throw new InvalidOperationException("Interaction events are only sent to the application that created them."); if (!buttons.Any()) throw new ArgumentException("You must specify at least one button to listen for."); if (!message.Components.Any()) throw new ArgumentException("Provided message does not contain any components."); if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type is ComponentType.Button)) throw new ArgumentException("Provided Message does not contain any button components."); var res = await this.ComponentEventWaiter .WaitForMatchAsync(new(message, c => c.Interaction.Data.ComponentType == ComponentType.Button && buttons.Any(b => b.CustomId == c.Id), token)).ConfigureAwait(false); return new(res is null, res); } + /// + /// Waits for a user modal submit. + /// + /// The custom id of the modal to wait for. + /// Override the timeout period specified in . + /// A with the result of the modal. + public Task> WaitForModalAsync(string custom_id, TimeSpan? timeoutOverride = null) + => this.WaitForModalAsync(custom_id, this.GetCancellationToken(timeoutOverride)); + + /// + /// Waits for a user modal submit. + /// + /// The custom id of the modal to wait for. + /// A custom cancellation token that can be cancelled at any point. + /// A with the result of the modal. + public async Task> WaitForModalAsync(string custom_id, CancellationToken token) + { + var result = + await this + .ModalEventWaiter + .WaitForModalMatchAsync(new(custom_id, c => c.Interaction.Type == InteractionType.ModalSubmit, token)) + .ConfigureAwait(false); + + return new(result is null, result); + } + /// /// Waits for any button on the specified message to be pressed. /// /// The message to wait for the button on. /// Override the timeout period specified in . /// A with the result of button that was pressed, if any. /// Thrown when attempting to wait for a message that is not authored by the current user. /// Thrown when the message does not contain a button with the specified Id, or any buttons at all. public Task> WaitForButtonAsync(DiscordMessage message, TimeSpan? timeoutOverride = null) => this.WaitForButtonAsync(message, this.GetCancellationToken(timeoutOverride)); /// /// Waits for any button on the specified message to be pressed. /// /// The message to wait for the button on. /// A custom cancellation token that can be cancelled at any point. /// A with the result of button that was pressed, if any. /// Thrown when attempting to wait for a message that is not authored by the current user. /// Thrown when the message does not contain a button with the specified Id, or any buttons at all. public async Task> WaitForButtonAsync(DiscordMessage message, CancellationToken token) { if (message.Author != this.Client.CurrentUser) throw new InvalidOperationException("Interaction events are only sent to the application that created them."); if (!message.Components.Any()) throw new ArgumentException("Provided message does not contain any components."); if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type is ComponentType.Button)) throw new ArgumentException("Message does not contain any button components."); var ids = message.Components.SelectMany(m => m.Components).Select(c => c.CustomId); var result = await this .ComponentEventWaiter .WaitForMatchAsync(new(message, c => c.Interaction.Data.ComponentType == ComponentType.Button && ids.Contains(c.Id), token)) .ConfigureAwait(false); return new(result is null, result); } /// /// Waits for any button on the specified message to be pressed by the specified user. /// /// The message to wait for the button on. /// The user to wait for the button press from. /// Override the timeout period specified in . /// A with the result of button that was pressed, if any. /// Thrown when attempting to wait for a message that is not authored by the current user. /// Thrown when the message does not contain a button with the specified Id, or any buttons at all. public Task> WaitForButtonAsync(DiscordMessage message, DiscordUser user, TimeSpan? timeoutOverride = null) => this.WaitForButtonAsync(message, user, this.GetCancellationToken(timeoutOverride)); /// /// Waits for any button on the specified message to be pressed by the specified user. /// /// The message to wait for the button on. /// The user to wait for the button press from. /// A custom cancellation token that can be cancelled at any point. /// A with the result of button that was pressed, if any. /// Thrown when attempting to wait for a message that is not authored by the current user. /// Thrown when the message does not contain a button with the specified Id, or any buttons at all. public async Task> WaitForButtonAsync(DiscordMessage message, DiscordUser user, CancellationToken token) { if (message.Author != this.Client.CurrentUser) throw new InvalidOperationException("Interaction events are only sent to the application that created them."); if (!message.Components.Any()) throw new ArgumentException("Provided message does not contain any components."); if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type is ComponentType.Button)) throw new ArgumentException("Message does not contain any button components."); var result = await this .ComponentEventWaiter .WaitForMatchAsync(new(message, (c) => c.Interaction.Data.ComponentType is ComponentType.Button && c.User == user, token)) .ConfigureAwait(false); return new(result is null, result); } /// /// Waits for a button with the specified Id to be pressed. /// /// The message to wait for the button on. /// The Id of the button to wait for. /// Override the timeout period specified in . /// A with the result of the operation. /// Thrown when attempting to wait for a message that is not authored by the current user. /// Thrown when the message does not contain a button with the specified Id, or any buttons at all. public Task> WaitForButtonAsync(DiscordMessage message, string id, TimeSpan? timeoutOverride = null) => this.WaitForButtonAsync(message, id, this.GetCancellationToken(timeoutOverride)); /// /// Waits for a button with the specified Id to be pressed. /// /// The message to wait for the button on. /// The Id of the button to wait for. /// Override the timeout period specified in . /// A with the result of the operation. /// Thrown when attempting to wait for a message that is not authored by the current user. /// Thrown when the message does not contain a button with the specified Id, or any buttons at all. public async Task> WaitForButtonAsync(DiscordMessage message, string id, CancellationToken token) { if (message.Author != this.Client.CurrentUser) throw new InvalidOperationException("Interaction events are only sent to the application that created them."); if (!message.Components.Any()) throw new ArgumentException("Provided message does not contain any components."); if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type is ComponentType.Button)) throw new ArgumentException("Message does not contain any button components."); if (!message.Components.SelectMany(c => c.Components).OfType().Any(c => c.CustomId == id)) throw new ArgumentException($"Message does not contain button with Id of '{id}'."); var result = await this .ComponentEventWaiter .WaitForMatchAsync(new(message, (c) => c.Interaction.Data.ComponentType is ComponentType.Button && c.Id == id, token)) .ConfigureAwait(false); return new(result is null, result); } /// /// Waits for any button to be interacted with. /// /// The message to wait on. /// The predicate to filter interactions by. /// Override the timeout specified in public Task> WaitForButtonAsync(DiscordMessage message, Func predicate, TimeSpan? timeoutOverride = null) => this.WaitForButtonAsync(message, predicate, this.GetCancellationToken(timeoutOverride)); /// /// Waits for any button to be interacted with. /// /// The message to wait on. /// The predicate to filter interactions by. /// A token to cancel interactivity with at any time. Pass to wait indefinitely. public async Task> WaitForButtonAsync(DiscordMessage message, Func predicate, CancellationToken token) { if (message.Author != this.Client.CurrentUser) throw new InvalidOperationException("Interaction events are only sent to the application that created them."); if (!message.Components.Any()) throw new ArgumentException("Provided message does not contain any components."); if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type is ComponentType.Button)) throw new ArgumentException("Message does not contain any button components."); var result = await this .ComponentEventWaiter .WaitForMatchAsync(new(message, c => c.Interaction.Data.ComponentType is ComponentType.Button && predicate(c), token)) .ConfigureAwait(false); return new(result is null, result); } /// /// Waits for any dropdown to be interacted with. /// /// The message to wait for. /// A filter predicate. /// Override the timeout period specified in . /// Thrown when the Provided message does not contain any dropdowns public Task> WaitForSelectAsync(DiscordMessage message, Func predicate, TimeSpan? timeoutOverride = null) => this.WaitForSelectAsync(message, predicate, this.GetCancellationToken(timeoutOverride)); /// /// Waits for any dropdown to be interacted with. /// /// The message to wait for. /// A filter predicate. /// A token that can be used to cancel interactivity. Pass to wait indefinitely. /// Thrown when the Provided message does not contain any dropdowns public async Task> WaitForSelectAsync(DiscordMessage message, Func predicate, CancellationToken token) { if (message.Author != this.Client.CurrentUser) throw new InvalidOperationException("Interaction events are only sent to the application that created them."); if (!message.Components.Any()) throw new ArgumentException("Provided message does not contain any components."); if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type is ComponentType.Select)) throw new ArgumentException("Message does not contain any select components."); var result = await this .ComponentEventWaiter .WaitForMatchAsync(new(message, c => c.Interaction.Data.ComponentType is ComponentType.Select && predicate(c), token)) .ConfigureAwait(false); return new(result is null, result); } /// /// Waits for a dropdown to be interacted with. /// /// This is here for backwards-compatibility and will internally create a cancellation token. /// The message to wait on. /// The Id of the dropdown to wait on. /// Override the timeout period specified in . /// Thrown when the message does not have any dropdowns or any dropdown with the specified Id. public Task> WaitForSelectAsync(DiscordMessage message, string id, TimeSpan? timeoutOverride = null) => this.WaitForSelectAsync(message, id, this.GetCancellationToken(timeoutOverride)); /// /// Waits for a dropdown to be interacted with. /// /// The message to wait on. /// The Id of the dropdown to wait on. /// A custom cancellation token that can be cancelled at any point. /// Thrown when the message does not have any dropdowns or any dropdown with the specified Id. public async Task> WaitForSelectAsync(DiscordMessage message, string id, CancellationToken token) { if (message.Author != this.Client.CurrentUser) throw new InvalidOperationException("Interaction events are only sent to the application that created them."); if (!message.Components.Any()) throw new ArgumentException("Provided message does not contain any components."); if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type is ComponentType.Select)) throw new ArgumentException("Message does not contain any select components."); if (message.Components.SelectMany(c => c.Components).OfType().All(c => c.CustomId != id)) throw new ArgumentException($"Message does not contain select component with Id of '{id}'."); var result = await this .ComponentEventWaiter .WaitForMatchAsync(new(message, (c) => c.Interaction.Data.ComponentType is ComponentType.Select && c.Id == id, token)) .ConfigureAwait(false); return new(result is null, result); } /// /// Waits for a dropdown to be interacted with by a specific user. /// /// The message to wait on. /// The user to wait on. /// The Id of the dropdown to wait on. /// Override the timeout period specified in . /// Thrown when the message does not have any dropdowns or any dropdown with the specified Id. public Task> WaitForSelectAsync(DiscordMessage message, DiscordUser user, string id, TimeSpan? timeoutOverride = null) => this.WaitForSelectAsync(message, user, id, this.GetCancellationToken(timeoutOverride)); /// /// Waits for a dropdown to be interacted with by a specific user. /// /// The message to wait on. /// The user to wait on. /// The Id of the dropdown to wait on. /// A custom cancellation token that can be cancelled at any point. /// Thrown when the message does not have any dropdowns or any dropdown with the specified Id. public async Task> WaitForSelectAsync(DiscordMessage message, DiscordUser user, string id, CancellationToken token) { if (message.Author != this.Client.CurrentUser) throw new InvalidOperationException("Interaction events are only sent to the application that created them."); if (!message.Components.Any()) throw new ArgumentException("Provided message does not contain any components."); if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type is ComponentType.Select)) throw new ArgumentException("Message does not contain any select components."); if (message.Components.SelectMany(c => c.Components).OfType().All(c => c.CustomId != id)) throw new ArgumentException($"Message does not contain select with Id of '{id}'."); var result = await this .ComponentEventWaiter .WaitForMatchAsync(new(message, (c) => c.Id == id && c.User == user, token)).ConfigureAwait(false); return new(result is null, result); } /// /// Waits for a specific message. /// /// Predicate to match. /// override timeout period. public async Task> WaitForMessageAsync(Func predicate, TimeSpan? timeoutoverride = null) { if (!Utilities.HasMessageIntents(this.Client.Configuration.Intents)) throw new InvalidOperationException("No message intents are enabled."); var timeout = timeoutoverride ?? this.Config.Timeout; var returns = await this.MessageCreatedWaiter.WaitForMatchAsync(new MatchRequest(x => predicate(x.Message), timeout)).ConfigureAwait(false); return new InteractivityResult(returns == null, returns?.Message); } /// /// Wait for a specific reaction. /// /// Predicate to match. /// override timeout period. public async Task> WaitForReactionAsync(Func predicate, TimeSpan? timeoutoverride = null) { if (!Utilities.HasReactionIntents(this.Client.Configuration.Intents)) throw new InvalidOperationException("No reaction intents are enabled."); var timeout = timeoutoverride ?? this.Config.Timeout; var returns = await this.MessageReactionAddWaiter.WaitForMatchAsync(new MatchRequest(predicate, timeout)).ConfigureAwait(false); return new InteractivityResult(returns == null, returns); } /// /// Wait for a specific reaction. /// For this Event you need the intent specified in /// /// Message reaction was added to. /// User that made the reaction. /// override timeout period. public async Task> WaitForReactionAsync(DiscordMessage message, DiscordUser user, TimeSpan? timeoutoverride = null) => await this.WaitForReactionAsync(x => x.User.Id == user.Id && x.Message.Id == message.Id, timeoutoverride).ConfigureAwait(false); /// /// Waits for a specific reaction. /// For this Event you need the intent specified in /// /// Predicate to match. /// Message reaction was added to. /// User that made the reaction. /// override timeout period. public async Task> WaitForReactionAsync(Func predicate, DiscordMessage message, DiscordUser user, TimeSpan? timeoutoverride = null) => await this.WaitForReactionAsync(x => predicate(x) && x.User.Id == user.Id && x.Message.Id == message.Id, timeoutoverride).ConfigureAwait(false); /// /// Waits for a specific reaction. /// For this Event you need the intent specified in /// /// predicate to match. /// User that made the reaction. /// Override timeout period. public async Task> WaitForReactionAsync(Func predicate, DiscordUser user, TimeSpan? timeoutoverride = null) => await this.WaitForReactionAsync(x => predicate(x) && x.User.Id == user.Id, timeoutoverride).ConfigureAwait(false); /// /// Waits for a user to start typing. /// /// User that starts typing. /// Channel the user is typing in. /// Override timeout period. public async Task> WaitForUserTypingAsync(DiscordUser user, DiscordChannel channel, TimeSpan? timeoutoverride = null) { if (!Utilities.HasTypingIntents(this.Client.Configuration.Intents)) throw new InvalidOperationException("No typing intents are enabled."); var timeout = timeoutoverride ?? this.Config.Timeout; var returns = await this.TypingStartWaiter.WaitForMatchAsync( new MatchRequest(x => x.User.Id == user.Id && x.Channel.Id == channel.Id, timeout)) .ConfigureAwait(false); return new InteractivityResult(returns == null, returns); } /// /// Waits for a user to start typing. /// /// User that starts typing. /// Override timeout period. public async Task> WaitForUserTypingAsync(DiscordUser user, TimeSpan? timeoutoverride = null) { if (!Utilities.HasTypingIntents(this.Client.Configuration.Intents)) throw new InvalidOperationException("No typing intents are enabled."); var timeout = timeoutoverride ?? this.Config.Timeout; var returns = await this.TypingStartWaiter.WaitForMatchAsync( new MatchRequest(x => x.User.Id == user.Id, timeout)) .ConfigureAwait(false); return new InteractivityResult(returns == null, returns); } /// /// Waits for any user to start typing. /// /// Channel to type in. /// Override timeout period. public async Task> WaitForTypingAsync(DiscordChannel channel, TimeSpan? timeoutoverride = null) { if (!Utilities.HasTypingIntents(this.Client.Configuration.Intents)) throw new InvalidOperationException("No typing intents are enabled."); var timeout = timeoutoverride ?? this.Config.Timeout; var returns = await this.TypingStartWaiter.WaitForMatchAsync( new MatchRequest(x => x.Channel.Id == channel.Id, timeout)) .ConfigureAwait(false); return new InteractivityResult(returns == null, returns); } /// /// Collects reactions on a specific message. /// /// Message to collect reactions on. /// Override timeout period. public async Task> CollectReactionsAsync(DiscordMessage m, TimeSpan? timeoutoverride = null) { if (!Utilities.HasReactionIntents(this.Client.Configuration.Intents)) throw new InvalidOperationException("No reaction intents are enabled."); var timeout = timeoutoverride ?? this.Config.Timeout; var collection = await this.ReactionCollector.CollectAsync(new ReactionCollectRequest(m, timeout)).ConfigureAwait(false); return collection; } /// /// Waits for specific event args to be received. Make sure the appropriate are registered, if needed. /// /// /// The predicate. /// Override timeout period. public async Task> WaitForEventArgsAsync(Func predicate, TimeSpan? timeoutoverride = null) where T : AsyncEventArgs { var timeout = timeoutoverride ?? this.Config.Timeout; using var waiter = new EventWaiter(this.Client); var res = await waiter.WaitForMatchAsync(new MatchRequest(predicate, timeout)).ConfigureAwait(false); return new InteractivityResult(res == null, res); } /// /// Collects the event arguments. /// /// The predicate. /// Override timeout period. public async Task> CollectEventArgsAsync(Func predicate, TimeSpan? timeoutoverride = null) where T : AsyncEventArgs { var timeout = timeoutoverride ?? this.Config.Timeout; using var waiter = new EventWaiter(this.Client); var res = await waiter.CollectMatchesAsync(new CollectRequest(predicate, timeout)).ConfigureAwait(false); return res; } /// /// Sends a paginated message with buttons. /// /// The channel to send it on. /// User to give control. /// The pages. /// Pagination buttons (pass null to use buttons defined in ). /// Pagination behaviour. /// Deletion behaviour /// A custom cancellation token that can be cancelled at any point. public async Task SendPaginatedMessageAsync( DiscordChannel channel, DiscordUser user, IEnumerable pages, PaginationButtons buttons, PaginationBehaviour? behaviour = default, ButtonPaginationBehavior? deletion = default, CancellationToken token = default) { var bhv = behaviour ?? this.Config.PaginationBehaviour; var del = deletion ?? this.Config.ButtonBehavior; var bts = buttons ?? this.Config.PaginationButtons; bts = new(bts); if (bhv is PaginationBehaviour.Ignore) { bts.SkipLeft.Disable(); bts.Left.Disable(); } var builder = new DiscordMessageBuilder() .WithContent(pages.First().Content) .WithEmbed(pages.First().Embed) .AddComponents(bts.ButtonArray); var message = await builder.SendAsync(channel).ConfigureAwait(false); var req = new ButtonPaginationRequest(message, user, bhv, del, bts, pages.ToArray(), token == default ? this.GetCancellationToken() : token); await this._compPaginator.DoPaginationAsync(req).ConfigureAwait(false); } /// /// Sends a paginated message with buttons. /// /// The channel to send it on. /// User to give control. /// The pages. /// Pagination buttons (pass null to use buttons defined in ). /// Pagination behaviour. /// Deletion behaviour /// Override timeout period. public Task SendPaginatedMessageAsync( DiscordChannel channel, DiscordUser user, IEnumerable pages, PaginationButtons buttons, TimeSpan? timeoutoverride, PaginationBehaviour? behaviour = default, ButtonPaginationBehavior? deletion = default) => this.SendPaginatedMessageAsync(channel, user, pages, buttons, behaviour, deletion, this.GetCancellationToken(timeoutoverride)); /// /// Sends the paginated message. /// /// The channel. /// The user. /// The pages. /// The behaviour. /// The deletion. /// The token. /// A Task. public Task SendPaginatedMessageAsync(DiscordChannel channel, DiscordUser user, IEnumerable pages, PaginationBehaviour? behaviour = default, ButtonPaginationBehavior? deletion = default, CancellationToken token = default) => this.SendPaginatedMessageAsync(channel, user, pages, default, behaviour, deletion, token); /// /// Sends the paginated message. /// /// The channel. /// The user. /// The pages. /// The timeoutoverride. /// The behaviour. /// The deletion. /// A Task. public Task SendPaginatedMessageAsync(DiscordChannel channel, DiscordUser user, IEnumerable pages, TimeSpan? timeoutoverride, PaginationBehaviour? behaviour = default, ButtonPaginationBehavior? deletion = default) => this.SendPaginatedMessageAsync(channel, user, pages, timeoutoverride, behaviour, deletion); /// /// Sends a paginated message. /// For this Event you need the intent specified in /// /// Channel to send paginated message in. /// User to give control. /// Pages. /// Pagination emojis. /// Pagination behaviour (when hitting max and min indices). /// Deletion behaviour. /// Override timeout period. public async Task SendPaginatedMessageAsync(DiscordChannel channel, DiscordUser user, IEnumerable pages, PaginationEmojis emojis, PaginationBehaviour? behaviour = default, PaginationDeletion? deletion = default, TimeSpan? timeoutoverride = null) { var builder = new DiscordMessageBuilder() .WithContent(pages.First().Content) .WithEmbed(pages.First().Embed); var m = await builder.SendAsync(channel).ConfigureAwait(false); var timeout = timeoutoverride ?? this.Config.Timeout; var bhv = behaviour ?? this.Config.PaginationBehaviour; var del = deletion ?? this.Config.PaginationDeletion; var ems = emojis ?? this.Config.PaginationEmojis; var prequest = new PaginationRequest(m, user, bhv, del, ems, timeout, pages.ToArray()); await this.Paginator.DoPaginationAsync(prequest).ConfigureAwait(false); } /// /// Sends a paginated message in response to an interaction. /// /// Pass the interaction directly. Interactivity will ACK it. /// /// /// The interaction to create a response to. /// Whether the response should be ephemeral. /// The user to listen for button presses from. /// The pages to paginate. /// Optional: custom buttons /// Pagination behaviour. /// Deletion behaviour /// A custom cancellation token that can be cancelled at any point. public async Task SendPaginatedResponseAsync(DiscordInteraction interaction, bool ephemeral, DiscordUser user, IEnumerable pages, PaginationButtons buttons = null, PaginationBehaviour? behaviour = default, ButtonPaginationBehavior? deletion = default, CancellationToken token = default) { var bhv = behaviour ?? this.Config.PaginationBehaviour; var del = deletion ?? this.Config.ButtonBehavior; var bts = buttons ?? this.Config.PaginationButtons; bts = new(bts); if (bhv is PaginationBehaviour.Ignore) { bts.SkipLeft.Disable(); bts.Left.Disable(); } var builder = new DiscordInteractionResponseBuilder() .WithContent(pages.First().Content) .AddEmbed(pages.First().Embed) .AsEphemeral(ephemeral) .AddComponents(bts.ButtonArray); await interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, builder).ConfigureAwait(false); var message = await interaction.GetOriginalResponseAsync().ConfigureAwait(false); var req = new InteractionPaginationRequest(interaction, message, user, bhv, del, bts, pages, token); await this._compPaginator.DoPaginationAsync(req).ConfigureAwait(false); } /// /// Waits for a custom pagination request to finish. /// This does NOT handle removing emojis after finishing for you. /// /// /// public async Task WaitForCustomPaginationAsync(IPaginationRequest request) => await this.Paginator.DoPaginationAsync(request).ConfigureAwait(false); /// /// Waits for custom button-based pagination request to finish. ///
/// This does not invoke . ///
/// The request to wait for. public async Task WaitForCustomComponentPaginationAsync(IPaginationRequest request) => await this._compPaginator.DoPaginationAsync(request).ConfigureAwait(false); /// /// Generates pages from a string, and puts them in message content. /// /// Input string. /// How to split input string. /// public IEnumerable GeneratePagesInContent(string input, SplitType splittype = SplitType.Character) { if (string.IsNullOrEmpty(input)) throw new ArgumentException("You must provide a string that is not null or empty!"); var result = new List(); List split; switch (splittype) { default: case SplitType.Character: split = this.SplitString(input, 500).ToList(); break; case SplitType.Line: var subsplit = input.Split('\n'); split = new List(); var s = ""; for (var i = 0; i < subsplit.Length; i++) { s += subsplit[i]; if (i >= 15 && i % 15 == 0) { split.Add(s); s = ""; } } if (split.All(x => x != s)) split.Add(s); break; } var page = 1; foreach (var s in split) { result.Add(new Page($"Page {page}:\n{s}")); page++; } return result; } /// /// Generates pages from a string, and puts them in message embeds. /// /// Input string. /// How to split input string. /// Base embed for output embeds. /// public IEnumerable GeneratePagesInEmbed(string input, SplitType splittype = SplitType.Character, DiscordEmbedBuilder embedbase = null) { if (string.IsNullOrEmpty(input)) throw new ArgumentException("You must provide a string that is not null or empty!"); var embed = embedbase ?? new DiscordEmbedBuilder(); var result = new List(); List split; switch (splittype) { default: case SplitType.Character: split = this.SplitString(input, 500).ToList(); break; case SplitType.Line: var subsplit = input.Split('\n'); split = new List(); var s = ""; for (var i = 0; i < subsplit.Length; i++) { s += $"{subsplit[i]}\n"; if (i % 15 == 0 && i != 0) { split.Add(s); s = ""; } } if (!split.Any(x => x == s)) split.Add(s); break; } var page = 1; foreach (var s in split) { result.Add(new Page("", new DiscordEmbedBuilder(embed).WithDescription(s).WithFooter($"Page {page}/{split.Count}"))); page++; } return result; } /// /// Splits the string. /// /// The string. /// The chunk size. private List SplitString(string str, int chunkSize) { var res = new List(); var len = str.Length; var i = 0; while (i < len) { var size = Math.Min(len - i, chunkSize); res.Add(str.Substring(i, size)); i += size; } return res; } /// /// Gets the cancellation token. /// /// The timeout. private CancellationToken GetCancellationToken(TimeSpan? timeout = null) => new CancellationTokenSource(timeout ?? this.Config.Timeout).Token; /// /// Handles an invalid interaction. /// /// The interaction. private async Task HandleInvalidInteraction(DiscordInteraction interaction) { var at = this.Config.ResponseBehavior switch { InteractionResponseBehavior.Ack => interaction.CreateResponseAsync(InteractionResponseType.DeferredMessageUpdate), InteractionResponseBehavior.Respond => interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new() { Content = this.Config.ResponseMessage, IsEphemeral = true}), InteractionResponseBehavior.Ignore => Task.CompletedTask, _ => throw new ArgumentException("Unknown enum value.") }; await at; } } } diff --git a/DisCatSharp/Clients/DiscordClient.Dispatch.cs b/DisCatSharp/Clients/DiscordClient.Dispatch.cs index 3bd2f46d6..6ab174f14 100644 --- a/DisCatSharp/Clients/DiscordClient.Dispatch.cs +++ b/DisCatSharp/Clients/DiscordClient.Dispatch.cs @@ -1,3224 +1,3226 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; using DisCatSharp.Common.Utilities; using DisCatSharp.Entities; using DisCatSharp.Enums; using DisCatSharp.EventArgs; using DisCatSharp.Exceptions; using DisCatSharp.Net.Abstractions; using DisCatSharp.Net.Serialization; using Microsoft.Extensions.Logging; using Newtonsoft.Json.Linq; namespace DisCatSharp { /// /// Represents a discord client. /// public sealed partial class DiscordClient { #region Private Fields private string _sessionId; private bool _guildDownloadCompleted = false; #endregion #region Dispatch Handler /// /// Handles the dispatch. /// /// The payload. internal async Task HandleDispatchAsync(GatewayPayload payload) { if (payload.Data is not JObject dat) { this.Logger.LogWarning(LoggerEvents.WebSocketReceive, "Invalid payload body (this message is probably safe to ignore); opcode: {0} event: {1}; payload: {2}", payload.OpCode, payload.EventName, payload.Data); return; } await this._payloadReceived.InvokeAsync(this, new(this.ServiceProvider) { EventName = payload.EventName, PayloadObject = dat }).ConfigureAwait(false); DiscordChannel chn; ulong gid; ulong cid; ulong uid; DiscordStageInstance stg = default; DiscordIntegration itg = default; DiscordThreadChannel trd = default; DiscordThreadChannelMember trdm = default; DiscordScheduledEvent gse = default; TransportUser usr = default; TransportMember mbr = default; TransportUser refUsr = default; TransportMember refMbr = default; JToken rawMbr = default; var rawRefMsg = dat["referenced_message"]; switch (payload.EventName.ToLowerInvariant()) { #region Gateway Status case "ready": var glds = (JArray)dat["guilds"]; await this.OnReadyEventAsync(dat.ToObject(), glds).ConfigureAwait(false); break; case "resumed": await this.OnResumedAsync().ConfigureAwait(false); break; #endregion #region Channel case "channel_create": chn = dat.ToObject(); await this.OnChannelCreateEventAsync(chn).ConfigureAwait(false); break; case "channel_update": await this.OnChannelUpdateEventAsync(dat.ToObject()).ConfigureAwait(false); break; case "channel_delete": chn = dat.ToObject(); await this.OnChannelDeleteEventAsync(chn.IsPrivate ? dat.ToObject() : chn).ConfigureAwait(false); break; case "channel_pins_update": cid = (ulong)dat["channel_id"]; var ts = (string)dat["last_pin_timestamp"]; await this.OnChannelPinsUpdateAsync((ulong?)dat["guild_id"], cid, ts != null ? DateTimeOffset.Parse(ts, CultureInfo.InvariantCulture) : default(DateTimeOffset?)).ConfigureAwait(false); break; #endregion #region Guild case "guild_create": await this.OnGuildCreateEventAsync(dat.ToDiscordObject(), (JArray)dat["members"], dat["presences"].ToDiscordObject>()).ConfigureAwait(false); break; case "guild_update": await this.OnGuildUpdateEventAsync(dat.ToDiscordObject(), (JArray)dat["members"]).ConfigureAwait(false); break; case "guild_delete": await this.OnGuildDeleteEventAsync(dat.ToDiscordObject()).ConfigureAwait(false); break; case "guild_sync": gid = (ulong)dat["id"]; await this.OnGuildSyncEventAsync(this._guilds[gid], (bool)dat["large"], (JArray)dat["members"], dat["presences"].ToDiscordObject>()).ConfigureAwait(false); break; case "guild_emojis_update": gid = (ulong)dat["guild_id"]; var ems = dat["emojis"].ToObject>(); await this.OnGuildEmojisUpdateEventAsync(this._guilds[gid], ems).ConfigureAwait(false); break; case "guild_stickers_update": var strs = dat["stickers"].ToDiscordObject>(); await this.OnStickersUpdatedAsync(strs, dat).ConfigureAwait(false); break; case "guild_integrations_update": gid = (ulong)dat["guild_id"]; // discord fires this event inconsistently if the current user leaves a guild. if (!this._guilds.ContainsKey(gid)) return; await this.OnGuildIntegrationsUpdateEventAsync(this._guilds[gid]).ConfigureAwait(false); break; /* Ok soooo.. this isn't documented yet It seems to be part of the next version of membership screening (https://discord.com/channels/641574644578648068/689591708962652289/845836910991507486) advaith said the following (https://discord.com/channels/641574644578648068/689591708962652289/845838160047112202): > iirc it happens when a user leaves a server where they havent completed screening yet We have to wait till it's documented, but the fields are: { "user_id": "snowflake_user", "guild_id": "snowflake_guild" } We could handle it rn, but due to the fact that it isn't documented, it's not an good idea. */ case "guild_join_request_delete": break; #endregion #region Guild Ban case "guild_ban_add": usr = dat["user"].ToObject(); gid = (ulong)dat["guild_id"]; await this.OnGuildBanAddEventAsync(usr, this._guilds[gid]).ConfigureAwait(false); break; case "guild_ban_remove": usr = dat["user"].ToObject(); gid = (ulong)dat["guild_id"]; await this.OnGuildBanRemoveEventAsync(usr, this._guilds[gid]).ConfigureAwait(false); break; #endregion #region Guild Event case "guild_scheduled_event_create": gse = dat.ToObject(); gid = (ulong)dat["guild_id"]; await this.OnGuildScheduledEventCreateEventAsync(gse, this._guilds[gid]).ConfigureAwait(false); break; case "guild_scheduled_event_update": gse = dat.ToObject(); gid = (ulong)dat["guild_id"]; await this.OnGuildScheduledEventUpdateEventAsync(gse, this._guilds[gid]).ConfigureAwait(false); break; case "guild_scheduled_event_delete": gse = dat.ToObject(); gid = (ulong)dat["guild_id"]; await this.OnGuildScheduledEventDeleteEventAsync(gse, this._guilds[gid]).ConfigureAwait(false); break; case "guild_scheduled_event_user_add": gid = (ulong)dat["guild_id"]; uid = (ulong)dat["user_id"]; await this.OnGuildScheduledEventUserAddedEventAsync((ulong)dat["guild_scheduled_event_id"], uid, this._guilds[gid]).ConfigureAwait(false); break; case "guild_scheduled_event_user_remove": gid = (ulong)dat["guild_id"]; uid = (ulong)dat["user_id"]; await this.OnGuildScheduledEventUserRemovedEventAsync((ulong)dat["guild_scheduled_event_id"], uid, this._guilds[gid]).ConfigureAwait(false); break; #endregion #region Guild Integration case "integration_create": gid = (ulong)dat["guild_id"]; itg = dat.ToObject(); // discord fires this event inconsistently if the current user leaves a guild. if (!this._guilds.ContainsKey(gid)) return; await this.OnGuildIntegrationCreateEventAsync(this._guilds[gid], itg).ConfigureAwait(false); break; case "integration_update": gid = (ulong)dat["guild_id"]; itg = dat.ToObject(); // discord fires this event inconsistently if the current user leaves a guild. if (!this._guilds.ContainsKey(gid)) return; await this.OnGuildIntegrationUpdateEventAsync(this._guilds[gid], itg).ConfigureAwait(false); break; case "integration_delete": gid = (ulong)dat["guild_id"]; // discord fires this event inconsistently if the current user leaves a guild. if (!this._guilds.ContainsKey(gid)) return; await this.OnGuildIntegrationDeleteEventAsync(this._guilds[gid], (ulong)dat["id"], (ulong?)dat["application_id"]).ConfigureAwait(false); break; #endregion #region Guild Member case "guild_member_add": gid = (ulong)dat["guild_id"]; await this.OnGuildMemberAddEventAsync(dat.ToObject(), this._guilds[gid]).ConfigureAwait(false); break; case "guild_member_remove": gid = (ulong)dat["guild_id"]; usr = dat["user"].ToObject(); if (!this._guilds.ContainsKey(gid)) { // discord fires this event inconsistently if the current user leaves a guild. if (usr.Id != this.CurrentUser.Id) this.Logger.LogError(LoggerEvents.WebSocketReceive, "Could not find {0} in guild cache", gid); return; } await this.OnGuildMemberRemoveEventAsync(usr, this._guilds[gid]).ConfigureAwait(false); break; case "guild_member_update": gid = (ulong)dat["guild_id"]; await this.OnGuildMemberUpdateEventAsync(dat.ToDiscordObject(), this._guilds[gid], dat["roles"].ToObject>(), (string)dat["nick"], (bool?)dat["pending"]).ConfigureAwait(false); break; case "guild_members_chunk": await this.OnGuildMembersChunkEventAsync(dat).ConfigureAwait(false); break; #endregion #region Guild Role case "guild_role_create": gid = (ulong)dat["guild_id"]; await this.OnGuildRoleCreateEventAsync(dat["role"].ToObject(), this._guilds[gid]).ConfigureAwait(false); break; case "guild_role_update": gid = (ulong)dat["guild_id"]; await this.OnGuildRoleUpdateEventAsync(dat["role"].ToObject(), this._guilds[gid]).ConfigureAwait(false); break; case "guild_role_delete": gid = (ulong)dat["guild_id"]; await this.OnGuildRoleDeleteEventAsync((ulong)dat["role_id"], this._guilds[gid]).ConfigureAwait(false); break; #endregion #region Invite case "invite_create": gid = (ulong)dat["guild_id"]; cid = (ulong)dat["channel_id"]; await this.OnInviteCreateEventAsync(cid, gid, dat.ToObject()).ConfigureAwait(false); break; case "invite_delete": gid = (ulong)dat["guild_id"]; cid = (ulong)dat["channel_id"]; await this.OnInviteDeleteEventAsync(cid, gid, dat).ConfigureAwait(false); break; #endregion #region Message case "message_ack": cid = (ulong)dat["channel_id"]; var mid = (ulong)dat["message_id"]; await this.OnMessageAckEventAsync(this.InternalGetCachedChannel(cid), mid).ConfigureAwait(false); break; case "message_create": rawMbr = dat["member"]; if (rawMbr != null) mbr = rawMbr.ToObject(); if (rawRefMsg != null && rawRefMsg.HasValues) { if (rawRefMsg.SelectToken("author") != null) { refUsr = rawRefMsg.SelectToken("author").ToObject(); } if (rawRefMsg.SelectToken("member") != null) { refMbr = rawRefMsg.SelectToken("member").ToObject(); } } await this.OnMessageCreateEventAsync(dat.ToDiscordObject(), dat["author"].ToObject(), mbr, refUsr, refMbr).ConfigureAwait(false); break; case "message_update": rawMbr = dat["member"]; if (rawMbr != null) mbr = rawMbr.ToObject(); if (rawRefMsg != null && rawRefMsg.HasValues) { if (rawRefMsg.SelectToken("author") != null) { refUsr = rawRefMsg.SelectToken("author").ToObject(); } if (rawRefMsg.SelectToken("member") != null) { refMbr = rawRefMsg.SelectToken("member").ToObject(); } } await this.OnMessageUpdateEventAsync(dat.ToDiscordObject(), dat["author"]?.ToObject(), mbr, refUsr, refMbr).ConfigureAwait(false); break; // delete event does *not* include message object case "message_delete": await this.OnMessageDeleteEventAsync((ulong)dat["id"], (ulong)dat["channel_id"], (ulong?)dat["guild_id"]).ConfigureAwait(false); break; case "message_delete_bulk": await this.OnMessageBulkDeleteEventAsync(dat["ids"].ToObject(), (ulong)dat["channel_id"], (ulong?)dat["guild_id"]).ConfigureAwait(false); break; #endregion #region Message Reaction case "message_reaction_add": rawMbr = dat["member"]; if (rawMbr != null) mbr = rawMbr.ToObject(); await this.OnMessageReactionAddAsync((ulong)dat["user_id"], (ulong)dat["message_id"], (ulong)dat["channel_id"], (ulong?)dat["guild_id"], mbr, dat["emoji"].ToObject()).ConfigureAwait(false); break; case "message_reaction_remove": await this.OnMessageReactionRemoveAsync((ulong)dat["user_id"], (ulong)dat["message_id"], (ulong)dat["channel_id"], (ulong?)dat["guild_id"], dat["emoji"].ToObject()).ConfigureAwait(false); break; case "message_reaction_remove_all": await this.OnMessageReactionRemoveAllAsync((ulong)dat["message_id"], (ulong)dat["channel_id"], (ulong?)dat["guild_id"]).ConfigureAwait(false); break; case "message_reaction_remove_emoji": await this.OnMessageReactionRemoveEmojiAsync((ulong)dat["message_id"], (ulong)dat["channel_id"], (ulong)dat["guild_id"], dat["emoji"]).ConfigureAwait(false); break; #endregion #region Stage Instance case "stage_instance_create": stg = dat.ToObject(); await this.OnStageInstanceCreateEventAsync(stg).ConfigureAwait(false); break; case "stage_instance_update": stg = dat.ToObject(); await this.OnStageInstanceUpdateEventAsync(stg).ConfigureAwait(false); break; case "stage_instance_delete": stg = dat.ToObject(); await this.OnStageInstanceDeleteEventAsync(stg).ConfigureAwait(false); break; #endregion #region Thread case "thread_create": trd = dat.ToObject(); await this.OnThreadCreateEventAsync(trd).ConfigureAwait(false); break; case "thread_update": trd = dat.ToObject(); await this.OnThreadUpdateEventAsync(trd).ConfigureAwait(false); break; case "thread_delete": trd = dat.ToObject(); await this.OnThreadDeleteEventAsync(trd).ConfigureAwait(false); break; case "thread_list_sync": gid = (ulong)dat["guild_id"]; //get guild await this.OnThreadListSyncEventAsync(this._guilds[gid], dat["channel_ids"].ToObject>(), dat["threads"].ToObject>(), dat["members"].ToObject>()).ConfigureAwait(false); break; case "thread_member_update": trdm = dat.ToObject(); await this.OnThreadMemberUpdateEventAsync(trdm).ConfigureAwait(false); break; case "thread_members_update": gid = (ulong)dat["guild_id"]; await this.OnThreadMembersUpdateEventAsync(this._guilds[gid], (ulong)dat["id"], (JArray)dat["added_members"], (JArray)dat["removed_member_ids"], (int)dat["member_count"]).ConfigureAwait(false); break; #endregion #region Activities case "embedded_activity_update": gid = (ulong)dat["guild_id"]; cid = (ulong)dat["channel_id"]; await this.OnEmbeddedActivityUpdateAsync((JObject)dat["embedded_activity"], this._guilds[gid], cid, (JArray)dat["users"], (ulong)dat["embedded_activity"]["application_id"]).ConfigureAwait(false); break; #endregion #region User/Presence Update case "presence_update": await this.OnPresenceUpdateEventAsync(dat, (JObject)dat["user"]).ConfigureAwait(false); break; case "user_settings_update": await this.OnUserSettingsUpdateEventAsync(dat.ToObject()).ConfigureAwait(false); break; case "user_update": await this.OnUserUpdateEventAsync(dat.ToObject()).ConfigureAwait(false); break; #endregion #region Voice case "voice_state_update": await this.OnVoiceStateUpdateEventAsync(dat).ConfigureAwait(false); break; case "voice_server_update": gid = (ulong)dat["guild_id"]; await this.OnVoiceServerUpdateEventAsync((string)dat["endpoint"], (string)dat["token"], this._guilds[gid]).ConfigureAwait(false); break; #endregion #region Interaction/Integration/Application case "interaction_create": rawMbr = dat["member"]; if (rawMbr != null) { mbr = dat["member"].ToObject(); usr = mbr.User; } else { usr = dat["user"].ToObject(); } cid = (ulong)dat["channel_id"]; await this.OnInteractionCreateAsync((ulong?)dat["guild_id"], cid, usr, mbr, dat.ToDiscordObject()).ConfigureAwait(false); break; case "application_command_create": await this.OnApplicationCommandCreateAsync(dat.ToObject(), (ulong?)dat["guild_id"]).ConfigureAwait(false); break; case "application_command_update": await this.OnApplicationCommandUpdateAsync(dat.ToObject(), (ulong?)dat["guild_id"]).ConfigureAwait(false); break; case "application_command_delete": await this.OnApplicationCommandDeleteAsync(dat.ToObject(), (ulong?)dat["guild_id"]).ConfigureAwait(false); break; case "guild_application_command_counts_update": var counts = dat["application_command_counts"]; await this.OnGuildApplicationCommandCountsUpdateAsync((int)counts["1"], (int)counts["2"], (int)counts["3"], (ulong)dat["guild_id"]).ConfigureAwait(false); break; case "application_command_permissions_update": var aid = (ulong)dat["application_id"]; if (aid != this.CurrentApplication.Id) return; var pms = dat["permissions"].ToObject>(); gid = (ulong)dat["guild_id"]; await this.OnApplicationCommandPermissionsUpdateAsync(pms, (ulong)dat["id"], gid, aid).ConfigureAwait(false); break; #endregion #region Misc case "gift_code_update": //Not supposed to be dispatched to bots break; case "typing_start": cid = (ulong)dat["channel_id"]; rawMbr = dat["member"]; if (rawMbr != null) mbr = rawMbr.ToObject(); await this.OnTypingStartEventAsync((ulong)dat["user_id"], cid, this.InternalGetCachedChannel(cid), (ulong?)dat["guild_id"], Utilities.GetDateTimeOffset((long)dat["timestamp"]), mbr).ConfigureAwait(false); break; case "webhooks_update": gid = (ulong)dat["guild_id"]; cid = (ulong)dat["channel_id"]; await this.OnWebhooksUpdateAsync(this._guilds[gid].GetChannel(cid), this._guilds[gid]).ConfigureAwait(false); break; default: await this.OnUnknownEventAsync(payload).ConfigureAwait(false); this.Logger.LogWarning(LoggerEvents.WebSocketReceive, "Unknown event: {0}\npayload: {1}", payload.EventName, payload.Data); break; #endregion } } #endregion #region Events #region Gateway /// /// Handles the ready event. /// /// The ready. /// The raw guilds. internal async Task OnReadyEventAsync(ReadyPayload ready, JArray rawGuilds) { //ready.CurrentUser.Discord = this; var rusr = ready.CurrentUser; this.CurrentUser.Username = rusr.Username; this.CurrentUser.Discriminator = rusr.Discriminator; this.CurrentUser.AvatarHash = rusr.AvatarHash; this.CurrentUser.MfaEnabled = rusr.MfaEnabled; this.CurrentUser.Verified = rusr.Verified; this.CurrentUser.IsBot = rusr.IsBot; this.GatewayVersion = ready.GatewayVersion; this._sessionId = ready.SessionId; var raw_guild_index = rawGuilds.ToDictionary(xt => (ulong)xt["id"], xt => (JObject)xt); this._guilds.Clear(); foreach (var guild in ready.Guilds) { guild.Discord = this; if (guild._channels == null) guild._channels = new ConcurrentDictionary(); foreach (var xc in guild.Channels.Values) { xc.GuildId = guild.Id; xc.Discord = this; foreach (var xo in xc._permissionOverwrites) { xo.Discord = this; xo._channel_id = xc.Id; } } if (guild._roles == null) guild._roles = new ConcurrentDictionary(); foreach (var xr in guild.Roles.Values) { xr.Discord = this; xr._guild_id = guild.Id; } var raw_guild = raw_guild_index[guild.Id]; var raw_members = (JArray)raw_guild["members"]; if (guild._members != null) guild._members.Clear(); else guild._members = new ConcurrentDictionary(); if (raw_members != null) { foreach (var xj in raw_members) { var xtm = xj.ToObject(); var xu = new DiscordUser(xtm.User) { Discord = this }; xu = this.UserCache.AddOrUpdate(xtm.User.Id, xu, (id, old) => { old.Username = xu.Username; old.Discriminator = xu.Discriminator; old.AvatarHash = xu.AvatarHash; return old; }); guild._members[xtm.User.Id] = new DiscordMember(xtm) { Discord = this, _guild_id = guild.Id }; } } if (guild._emojis == null) guild._emojis = new ConcurrentDictionary(); foreach (var xe in guild.Emojis.Values) xe.Discord = this; if (guild._voiceStates == null) guild._voiceStates = new ConcurrentDictionary(); foreach (var xvs in guild.VoiceStates.Values) xvs.Discord = this; this._guilds[guild.Id] = guild; } await this._ready.InvokeAsync(this, new ReadyEventArgs(this.ServiceProvider)).ConfigureAwait(false); } /// /// Handles the resumed. /// internal Task OnResumedAsync() { this.Logger.LogInformation(LoggerEvents.SessionUpdate, "Session resumed"); return this._resumed.InvokeAsync(this, new ReadyEventArgs(this.ServiceProvider)); } #endregion #region Channel /// /// Handles the channel create event. /// /// The channel. internal async Task OnChannelCreateEventAsync(DiscordChannel channel) { channel.Discord = this; foreach (var xo in channel._permissionOverwrites) { xo.Discord = this; xo._channel_id = channel.Id; } this._guilds[channel.GuildId.Value]._channels[channel.Id] = channel; /*if (this.Configuration.AutoRefreshChannelCache) { await this.RefreshChannelsAsync(channel.Guild.Id); }*/ await this._channelCreated.InvokeAsync(this, new ChannelCreateEventArgs(this.ServiceProvider) { Channel = channel, Guild = channel.Guild }).ConfigureAwait(false); } /// /// Handles the channel update event. /// /// The channel. internal async Task OnChannelUpdateEventAsync(DiscordChannel channel) { if (channel == null) return; channel.Discord = this; var gld = channel.Guild; var channel_new = this.InternalGetCachedChannel(channel.Id); DiscordChannel channel_old = null; if (channel_new != null) { channel_old = new DiscordChannel { Bitrate = channel_new.Bitrate, Discord = this, GuildId = channel_new.GuildId, Id = channel_new.Id, //IsPrivate = channel_new.IsPrivate, LastMessageId = channel_new.LastMessageId, Name = channel_new.Name, _permissionOverwrites = new List(channel_new._permissionOverwrites), Position = channel_new.Position, Topic = channel_new.Topic, Type = channel_new.Type, UserLimit = channel_new.UserLimit, ParentId = channel_new.ParentId, IsNSFW = channel_new.IsNSFW, PerUserRateLimit = channel_new.PerUserRateLimit, RtcRegionId = channel_new.RtcRegionId, QualityMode = channel_new.QualityMode, DefaultAutoArchiveDuration = channel_new.DefaultAutoArchiveDuration }; channel_new.Bitrate = channel.Bitrate; channel_new.Name = channel.Name; channel_new.Position = channel.Position; channel_new.Topic = channel.Topic; channel_new.UserLimit = channel.UserLimit; channel_new.ParentId = channel.ParentId; channel_new.IsNSFW = channel.IsNSFW; channel_new.PerUserRateLimit = channel.PerUserRateLimit; channel_new.Type = channel.Type; channel_new.RtcRegionId = channel.RtcRegionId; channel_new.QualityMode = channel.QualityMode; channel_new.DefaultAutoArchiveDuration = channel.DefaultAutoArchiveDuration; channel_new._permissionOverwrites.Clear(); foreach (var po in channel._permissionOverwrites) { po.Discord = this; po._channel_id = channel.Id; } channel_new._permissionOverwrites.AddRange(channel._permissionOverwrites); if (this.Configuration.AutoRefreshChannelCache && gld != null) { await this.RefreshChannelsAsync(channel.Guild.Id); } } else if (gld != null) { gld._channels[channel.Id] = channel; if (this.Configuration.AutoRefreshChannelCache) { await this.RefreshChannelsAsync(channel.Guild.Id); } } await this._channelUpdated.InvokeAsync(this, new ChannelUpdateEventArgs(this.ServiceProvider) { ChannelAfter = channel_new, Guild = gld, ChannelBefore = channel_old }).ConfigureAwait(false); } /// /// Handles the channel delete event. /// /// The channel. internal async Task OnChannelDeleteEventAsync(DiscordChannel channel) { if (channel == null) return; channel.Discord = this; //if (channel.IsPrivate) if (channel.Type == ChannelType.Group || channel.Type == ChannelType.Private) { var dmChannel = channel as DiscordDmChannel; await this._dmChannelDeleted.InvokeAsync(this, new DmChannelDeleteEventArgs(this.ServiceProvider) { Channel = dmChannel }).ConfigureAwait(false); } else { var gld = channel.Guild; if (gld._channels.TryRemove(channel.Id, out var cachedChannel)) channel = cachedChannel; if(this.Configuration.AutoRefreshChannelCache) { await this.RefreshChannelsAsync(channel.Guild.Id); } await this._channelDeleted.InvokeAsync(this, new ChannelDeleteEventArgs(this.ServiceProvider) { Channel = channel, Guild = gld }).ConfigureAwait(false); } } /// /// Refreshes the channels. /// /// The guild id. internal async Task RefreshChannelsAsync(ulong guildId) { var guild = this.InternalGetCachedGuild(guildId); var channels = await this.ApiClient.GetGuildChannelsAsync(guildId); guild._channels.Clear(); foreach (var channel in channels.ToList()) { channel.Discord = this; foreach (var xo in channel._permissionOverwrites) { xo.Discord = this; xo._channel_id = channel.Id; } guild._channels[channel.Id] = channel; } } /// /// Handles the channel pins update. /// /// The guild id. /// The channel id. /// The last pin timestamp. internal async Task OnChannelPinsUpdateAsync(ulong? guildId, ulong channelId, DateTimeOffset? lastPinTimestamp) { var guild = this.InternalGetCachedGuild(guildId); var channel = this.InternalGetCachedChannel(channelId) ?? this.InternalGetCachedThread(channelId); var ea = new ChannelPinsUpdateEventArgs(this.ServiceProvider) { Guild = guild, Channel = channel, LastPinTimestamp = lastPinTimestamp }; await this._channelPinsUpdated.InvokeAsync(this, ea).ConfigureAwait(false); } #endregion #region Guild /// /// Handles the guild create event. /// /// The guild. /// The raw members. /// The presences. internal async Task OnGuildCreateEventAsync(DiscordGuild guild, JArray rawMembers, IEnumerable presences) { if (presences != null) { foreach (var xp in presences) { xp.Discord = this; xp.GuildId = guild.Id; xp.Activity = new DiscordActivity(xp.RawActivity); if (xp.RawActivities != null) { xp._internalActivities = new DiscordActivity[xp.RawActivities.Length]; for (var i = 0; i < xp.RawActivities.Length; i++) xp._internalActivities[i] = new DiscordActivity(xp.RawActivities[i]); } this._presences[xp.InternalUser.Id] = xp; } } var exists = this._guilds.TryGetValue(guild.Id, out var foundGuild); guild.Discord = this; guild.IsUnavailable = false; var eventGuild = guild; if (exists) guild = foundGuild; if (guild._channels == null) guild._channels = new ConcurrentDictionary(); if(guild._threads == null) guild._threads = new ConcurrentDictionary(); if (guild._roles == null) guild._roles = new ConcurrentDictionary(); if (guild._threads == null) guild._threads = new ConcurrentDictionary(); if (guild._stickers == null) guild._stickers = new ConcurrentDictionary(); if (guild._emojis == null) guild._emojis = new ConcurrentDictionary(); if (guild._voiceStates == null) guild._voiceStates = new ConcurrentDictionary(); if (guild._members == null) guild._members = new ConcurrentDictionary(); if (guild._scheduledEvents == null) guild._scheduledEvents = new ConcurrentDictionary(); this.UpdateCachedGuild(eventGuild, rawMembers); guild.JoinedAt = eventGuild.JoinedAt; guild.IsLarge = eventGuild.IsLarge; guild.MemberCount = Math.Max(eventGuild.MemberCount, guild._members.Count); guild.IsUnavailable = eventGuild.IsUnavailable; guild.PremiumSubscriptionCount = eventGuild.PremiumSubscriptionCount; guild.PremiumTier = eventGuild.PremiumTier; guild.BannerHash = eventGuild.BannerHash; guild.VanityUrlCode = eventGuild.VanityUrlCode; guild.Description = eventGuild.Description; guild.IsNSFW = eventGuild.IsNSFW; foreach (var kvp in eventGuild._voiceStates) guild._voiceStates[kvp.Key] = kvp.Value; foreach (var kvp in eventGuild._channels) guild._channels[kvp.Key] = kvp.Value; foreach (var kvp in eventGuild._roles) guild._roles[kvp.Key] = kvp.Value; foreach (var kvp in eventGuild._emojis) guild._emojis[kvp.Key] = kvp.Value; foreach (var kvp in eventGuild._threads) guild._threads[kvp.Key] = kvp.Value; foreach (var kvp in eventGuild._stickers) guild._stickers[kvp.Key] = kvp.Value; foreach (var kvp in eventGuild._stageInstances) guild._stageInstances[kvp.Key] = kvp.Value; foreach (var kvp in eventGuild._scheduledEvents) guild._scheduledEvents[kvp.Key] = kvp.Value; foreach (var xc in guild._channels.Values) { xc.GuildId = guild.Id; xc.Discord = this; foreach (var xo in xc._permissionOverwrites) { xo.Discord = this; xo._channel_id = xc.Id; } } foreach(var xt in guild._threads.Values) { xt.GuildId = guild.Id; xt.Discord = this; } foreach (var xe in guild._emojis.Values) xe.Discord = this; foreach (var xs in guild._stickers.Values) xs.Discord = this; foreach (var xvs in guild._voiceStates.Values) xvs.Discord = this; foreach (var xsi in guild._stageInstances.Values) { xsi.Discord = this; xsi.GuildId = guild.Id; } foreach (var xr in guild._roles.Values) { xr.Discord = this; xr._guild_id = guild.Id; } foreach (var xse in guild._scheduledEvents.Values) { xse.Discord = this; xse.GuildId = guild.Id; if (xse.Creator != null) xse.Creator.Discord = this; } var old = Volatile.Read(ref this._guildDownloadCompleted); var dcompl = this._guilds.Values.All(xg => !xg.IsUnavailable); Volatile.Write(ref this._guildDownloadCompleted, dcompl); if (exists) await this._guildAvailable.InvokeAsync(this, new GuildCreateEventArgs(this.ServiceProvider) { Guild = guild }).ConfigureAwait(false); else await this._guildCreated.InvokeAsync(this, new GuildCreateEventArgs(this.ServiceProvider) { Guild = guild }).ConfigureAwait(false); if (dcompl && !old) await this._guildDownloadCompletedEv.InvokeAsync(this, new GuildDownloadCompletedEventArgs(this.Guilds, this.ServiceProvider)).ConfigureAwait(false); } /// /// Handles the guild update event. /// /// The guild. /// The raw members. internal async Task OnGuildUpdateEventAsync(DiscordGuild guild, JArray rawMembers) { DiscordGuild oldGuild; if (!this._guilds.ContainsKey(guild.Id)) { this._guilds[guild.Id] = guild; oldGuild = null; } else { var gld = this._guilds[guild.Id]; oldGuild = new DiscordGuild { Discord = gld.Discord, Name = gld.Name, AfkChannelId = gld.AfkChannelId, AfkTimeout = gld.AfkTimeout, ApplicationId = gld.ApplicationId, DefaultMessageNotifications = gld.DefaultMessageNotifications, ExplicitContentFilter = gld.ExplicitContentFilter, RawFeatures = gld.RawFeatures, IconHash = gld.IconHash, Id = gld.Id, IsLarge = gld.IsLarge, IsSynced = gld.IsSynced, IsUnavailable = gld.IsUnavailable, JoinedAt = gld.JoinedAt, MemberCount = gld.MemberCount, MaxMembers = gld.MaxMembers, MaxPresences = gld.MaxPresences, ApproximateMemberCount = gld.ApproximateMemberCount, ApproximatePresenceCount = gld.ApproximatePresenceCount, MaxVideoChannelUsers = gld.MaxVideoChannelUsers, DiscoverySplashHash = gld.DiscoverySplashHash, PreferredLocale = gld.PreferredLocale, MfaLevel = gld.MfaLevel, OwnerId = gld.OwnerId, SplashHash = gld.SplashHash, SystemChannelId = gld.SystemChannelId, SystemChannelFlags = gld.SystemChannelFlags, Description = gld.Description, WidgetEnabled = gld.WidgetEnabled, WidgetChannelId = gld.WidgetChannelId, VerificationLevel = gld.VerificationLevel, RulesChannelId = gld.RulesChannelId, PublicUpdatesChannelId = gld.PublicUpdatesChannelId, VoiceRegionId = gld.VoiceRegionId, IsNSFW = gld.IsNSFW, PremiumProgressBarEnabled = gld.PremiumProgressBarEnabled, PremiumSubscriptionCount = gld.PremiumSubscriptionCount, PremiumTier = gld.PremiumTier, _channels = new ConcurrentDictionary(), _threads = new ConcurrentDictionary(), _emojis = new ConcurrentDictionary(), _stickers = new ConcurrentDictionary(), _members = new ConcurrentDictionary(), _roles = new ConcurrentDictionary(), _stageInstances = new ConcurrentDictionary(), _voiceStates = new ConcurrentDictionary(), _scheduledEvents = new ConcurrentDictionary() }; foreach (var kvp in gld._channels) oldGuild._channels[kvp.Key] = kvp.Value; foreach (var kvp in gld._threads) oldGuild._threads[kvp.Key] = kvp.Value; foreach (var kvp in gld._emojis) oldGuild._emojis[kvp.Key] = kvp.Value; foreach (var kvp in gld._stickers) oldGuild._stickers[kvp.Key] = kvp.Value; foreach (var kvp in gld._roles) oldGuild._roles[kvp.Key] = kvp.Value; foreach (var kvp in gld._voiceStates) oldGuild._voiceStates[kvp.Key] = kvp.Value; foreach (var kvp in gld._members) oldGuild._members[kvp.Key] = kvp.Value; foreach (var kvp in gld._stageInstances) oldGuild._stageInstances[kvp.Key] = kvp.Value; foreach (var kvp in gld._scheduledEvents) oldGuild._scheduledEvents[kvp.Key] = kvp.Value; } guild.Discord = this; guild.IsUnavailable = false; var eventGuild = guild; guild = this._guilds[eventGuild.Id]; if (guild._channels == null) guild._channels = new ConcurrentDictionary(); if (guild._threads == null) guild._threads = new ConcurrentDictionary(); if (guild._roles == null) guild._roles = new ConcurrentDictionary(); if (guild._emojis == null) guild._emojis = new ConcurrentDictionary(); if (guild._stickers == null) guild._stickers = new ConcurrentDictionary(); if (guild._voiceStates == null) guild._voiceStates = new ConcurrentDictionary(); if (guild._stageInstances == null) guild._stageInstances = new ConcurrentDictionary(); if (guild._members == null) guild._members = new ConcurrentDictionary(); if (guild._scheduledEvents == null) guild._scheduledEvents = new ConcurrentDictionary(); this.UpdateCachedGuild(eventGuild, rawMembers); foreach (var xc in guild._channels.Values) { xc.GuildId = guild.Id; xc.Discord = this; foreach (var xo in xc._permissionOverwrites) { xo.Discord = this; xo._channel_id = xc.Id; } } foreach (var xc in guild._threads.Values) { xc.GuildId = guild.Id; xc.Discord = this; } foreach (var xe in guild._emojis.Values) xe.Discord = this; foreach (var xs in guild._stickers.Values) xs.Discord = this; foreach (var xvs in guild._voiceStates.Values) xvs.Discord = this; foreach (var xr in guild._roles.Values) { xr.Discord = this; xr._guild_id = guild.Id; } foreach (var xsi in guild._stageInstances.Values) { xsi.Discord = this; xsi.GuildId = guild.Id; } foreach (var xse in guild._scheduledEvents.Values) { xse.Discord = this; xse.GuildId = guild.Id; if (xse.Creator != null) xse.Creator.Discord = this; } await this._guildUpdated.InvokeAsync(this, new GuildUpdateEventArgs(this.ServiceProvider) { GuildBefore = oldGuild, GuildAfter = guild }).ConfigureAwait(false); } /// /// Handles the guild delete event. /// /// The guild. internal async Task OnGuildDeleteEventAsync(DiscordGuild guild) { if (guild.IsUnavailable) { if (!this._guilds.TryGetValue(guild.Id, out var gld)) return; gld.IsUnavailable = true; await this._guildUnavailable.InvokeAsync(this, new GuildDeleteEventArgs(this.ServiceProvider) { Guild = guild, Unavailable = true }).ConfigureAwait(false); } else { if (!this._guilds.TryRemove(guild.Id, out var gld)) return; await this._guildDeleted.InvokeAsync(this, new GuildDeleteEventArgs(this.ServiceProvider) { Guild = gld }).ConfigureAwait(false); } } /// /// Handles the guild sync event. /// /// The guild. /// If true, is large. /// The raw members. /// The presences. internal async Task OnGuildSyncEventAsync(DiscordGuild guild, bool isLarge, JArray rawMembers, IEnumerable presences) { presences = presences.Select(xp => { xp.Discord = this; xp.Activity = new DiscordActivity(xp.RawActivity); return xp; }); foreach (var xp in presences) this._presences[xp.InternalUser.Id] = xp; guild.IsSynced = true; guild.IsLarge = isLarge; this.UpdateCachedGuild(guild, rawMembers); await this._guildAvailable.InvokeAsync(this, new GuildCreateEventArgs(this.ServiceProvider) { Guild = guild }).ConfigureAwait(false); } /// /// Handles the guild emojis update event. /// /// The guild. /// The new emojis. internal async Task OnGuildEmojisUpdateEventAsync(DiscordGuild guild, IEnumerable newEmojis) { var oldEmojis = new ConcurrentDictionary(guild._emojis); guild._emojis.Clear(); foreach (var emoji in newEmojis) { emoji.Discord = this; guild._emojis[emoji.Id] = emoji; } var ea = new GuildEmojisUpdateEventArgs(this.ServiceProvider) { Guild = guild, EmojisAfter = guild.Emojis, EmojisBefore = new ReadOnlyConcurrentDictionary(oldEmojis) }; await this._guildEmojisUpdated.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the stickers updated. /// /// The new stickers. /// The raw. internal async Task OnStickersUpdatedAsync(IEnumerable newStickers, JObject raw) { var guild = this.InternalGetCachedGuild((ulong)raw["guild_id"]); var oldStickers = new ConcurrentDictionary(guild._stickers); guild._stickers.Clear(); foreach (var nst in newStickers) { if (nst.User is not null) { nst.User.Discord = this; this.UserCache.AddOrUpdate(nst.User.Id, nst.User, (old, @new) => @new); } nst.Discord = this; guild._stickers[nst.Id] = nst; } var sea = new GuildStickersUpdateEventArgs(this.ServiceProvider) { Guild = guild, StickersBefore = oldStickers, StickersAfter = guild.Stickers }; await this._guildStickersUpdated.InvokeAsync(this, sea).ConfigureAwait(false); } /// /// Handles the guild integrations update event. /// /// The guild. internal async Task OnGuildIntegrationsUpdateEventAsync(DiscordGuild guild) { var ea = new GuildIntegrationsUpdateEventArgs(this.ServiceProvider) { Guild = guild }; await this._guildIntegrationsUpdated.InvokeAsync(this, ea).ConfigureAwait(false); } #endregion #region Guild Ban /// /// Handles the guild ban add event. /// /// The user. /// The guild. internal async Task OnGuildBanAddEventAsync(TransportUser user, DiscordGuild guild) { var usr = new DiscordUser(user) { Discord = this }; usr = this.UserCache.AddOrUpdate(user.Id, usr, (id, old) => { old.Username = usr.Username; old.Discriminator = usr.Discriminator; old.AvatarHash = usr.AvatarHash; return old; }); if (!guild.Members.TryGetValue(user.Id, out var mbr)) mbr = new DiscordMember(usr) { Discord = this, _guild_id = guild.Id }; var ea = new GuildBanAddEventArgs(this.ServiceProvider) { Guild = guild, Member = mbr }; await this._guildBanAdded.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the guild ban remove event. /// /// The user. /// The guild. internal async Task OnGuildBanRemoveEventAsync(TransportUser user, DiscordGuild guild) { var usr = new DiscordUser(user) { Discord = this }; usr = this.UserCache.AddOrUpdate(user.Id, usr, (id, old) => { old.Username = usr.Username; old.Discriminator = usr.Discriminator; old.AvatarHash = usr.AvatarHash; return old; }); if (!guild.Members.TryGetValue(user.Id, out var mbr)) mbr = new DiscordMember(usr) { Discord = this, _guild_id = guild.Id }; var ea = new GuildBanRemoveEventArgs(this.ServiceProvider) { Guild = guild, Member = mbr }; await this._guildBanRemoved.InvokeAsync(this, ea).ConfigureAwait(false); } #endregion #region Guild Scheduled Event /// /// Dispatches the event. /// /// The created event. /// The target guild. internal async Task OnGuildScheduledEventCreateEventAsync(DiscordScheduledEvent scheduled_event, DiscordGuild guild) { scheduled_event.Discord = this; guild._scheduledEvents.AddOrUpdate(scheduled_event.Id, scheduled_event, (old, newScheduledEvent) => newScheduledEvent); if (scheduled_event.Creator != null) { scheduled_event.Creator.Discord = this; this.UserCache.AddOrUpdate(scheduled_event.Creator.Id, scheduled_event.Creator, (id, old) => { old.Username = scheduled_event.Creator.Username; old.Discriminator = scheduled_event.Creator.Discriminator; old.AvatarHash = scheduled_event.Creator.AvatarHash; old.Flags = scheduled_event.Creator.Flags; return old; }); } await this._guildScheduledEventCreated.InvokeAsync(this, new GuildScheduledEventCreateEventArgs(this.ServiceProvider) { ScheduledEvent = scheduled_event, Guild = scheduled_event.Guild }).ConfigureAwait(false); } /// /// Dispatches the event. /// /// The updated event. /// The target guild. internal async Task OnGuildScheduledEventUpdateEventAsync(DiscordScheduledEvent scheduled_event, DiscordGuild guild) { if (guild == null) return; DiscordScheduledEvent old_event; if (!guild._scheduledEvents.ContainsKey(scheduled_event.Id)) { old_event = null; } else { var ev = guild._scheduledEvents[scheduled_event.Id]; old_event = new DiscordScheduledEvent { Id = ev.Id, ChannelId = ev.ChannelId, EntityId = ev.EntityId, EntityMetadata = ev.EntityMetadata, CreatorId = ev.CreatorId, Creator = ev.Creator, Discord = this, Description = ev.Description, EntityType = ev.EntityType, ScheduledStartTimeRaw = ev.ScheduledStartTimeRaw, ScheduledEndTimeRaw = ev.ScheduledEndTimeRaw, GuildId = ev.GuildId, Status = ev.Status, Name = ev.Name, UserCount = ev.UserCount }; } if (scheduled_event.Creator != null) { scheduled_event.Creator.Discord = this; this.UserCache.AddOrUpdate(scheduled_event.Creator.Id, scheduled_event.Creator, (id, old) => { old.Username = scheduled_event.Creator.Username; old.Discriminator = scheduled_event.Creator.Discriminator; old.AvatarHash = scheduled_event.Creator.AvatarHash; old.Flags = scheduled_event.Creator.Flags; return old; }); } if (scheduled_event.Status == ScheduledEventStatus.Completed) { guild._scheduledEvents.TryRemove(scheduled_event.Id, out var deleted_event); await this._guildScheduledEventDeleted.InvokeAsync(this, new GuildScheduledEventDeleteEventArgs(this.ServiceProvider) { ScheduledEvent = scheduled_event, Guild = guild, Reason = ScheduledEventStatus.Completed }).ConfigureAwait(false); } else if (scheduled_event.Status == ScheduledEventStatus.Canceled) { guild._scheduledEvents.TryRemove(scheduled_event.Id, out var deleted_event); scheduled_event.Status = ScheduledEventStatus.Canceled; await this._guildScheduledEventDeleted.InvokeAsync(this, new GuildScheduledEventDeleteEventArgs(this.ServiceProvider) { ScheduledEvent = scheduled_event, Guild = guild, Reason = ScheduledEventStatus.Canceled }).ConfigureAwait(false); } else { this.UpdateScheduledEvent(scheduled_event, guild); await this._guildScheduledEventUpdated.InvokeAsync(this, new GuildScheduledEventUpdateEventArgs(this.ServiceProvider) { ScheduledEventBefore = old_event, ScheduledEventAfter = scheduled_event, Guild = guild }).ConfigureAwait(false); } } /// /// Dispatches the event. /// /// The deleted event. /// The target guild. internal async Task OnGuildScheduledEventDeleteEventAsync(DiscordScheduledEvent scheduled_event, DiscordGuild guild) { scheduled_event.Discord = this; if (scheduled_event.Status == ScheduledEventStatus.Scheduled) scheduled_event.Status = ScheduledEventStatus.Canceled; if (scheduled_event.Creator != null) { scheduled_event.Creator.Discord = this; this.UserCache.AddOrUpdate(scheduled_event.Creator.Id, scheduled_event.Creator, (id, old) => { old.Username = scheduled_event.Creator.Username; old.Discriminator = scheduled_event.Creator.Discriminator; old.AvatarHash = scheduled_event.Creator.AvatarHash; old.Flags = scheduled_event.Creator.Flags; return old; }); } await this._guildScheduledEventDeleted.InvokeAsync(this, new GuildScheduledEventDeleteEventArgs(this.ServiceProvider) { ScheduledEvent = scheduled_event, Guild = scheduled_event.Guild, Reason = scheduled_event.Status }).ConfigureAwait(false); guild._scheduledEvents.TryRemove(scheduled_event.Id, out var deleted_event); } /// /// Dispatches the event. /// The target event. /// The added user id. /// The target guild. /// internal async Task OnGuildScheduledEventUserAddedEventAsync(ulong guild_scheduled_event_id, ulong user_id, DiscordGuild guild) { var scheduled_event = this.InternalGetCachedScheduledEvent(guild_scheduled_event_id) ?? this.UpdateScheduledEvent(new DiscordScheduledEvent { Id = guild_scheduled_event_id, GuildId = guild.Id, Discord = this, UserCount = 0 }, guild); scheduled_event.UserCount++; scheduled_event.Discord = this; guild.Discord = this; var user = this.GetUserAsync(user_id, true).Result; user.Discord = this; var member = guild.Members.TryGetValue(user_id, out var mem) ? mem : guild.GetMemberAsync(user_id).Result; member.Discord = this; await this._guildScheduledEventUserAdded.InvokeAsync(this, new GuildScheduledEventUserAddEventArgs(this.ServiceProvider) { ScheduledEvent = scheduled_event, Guild = guild, User = user, Member = member }).ConfigureAwait(false); } /// /// Dispatches the event. /// The target event. /// The removed user id. /// The target guild. /// internal async Task OnGuildScheduledEventUserRemovedEventAsync(ulong guild_scheduled_event_id, ulong user_id, DiscordGuild guild) { var scheduled_event = this.InternalGetCachedScheduledEvent(guild_scheduled_event_id) ?? this.UpdateScheduledEvent(new DiscordScheduledEvent { Id = guild_scheduled_event_id, GuildId = guild.Id, Discord = this, UserCount = 0 }, guild); scheduled_event.UserCount = scheduled_event.UserCount == 0 ? 0 : scheduled_event.UserCount - 1; scheduled_event.Discord = this; guild.Discord = this; var user = this.GetUserAsync(user_id, true).Result; user.Discord = this; var member = guild.Members.TryGetValue(user_id, out var mem) ? mem : guild.GetMemberAsync(user_id).Result; member.Discord = this; await this._guildScheduledEventUserRemoved.InvokeAsync(this, new GuildScheduledEventUserRemoveEventArgs(this.ServiceProvider) { ScheduledEvent = scheduled_event, Guild = guild, User = user, Member = member }).ConfigureAwait(false); } #endregion #region Guild Integration /// /// Handles the guild integration create event. /// /// The guild. /// The integration. internal async Task OnGuildIntegrationCreateEventAsync(DiscordGuild guild, DiscordIntegration integration) { integration.Discord = this; await this._guildIntegrationCreated.InvokeAsync(this, new GuildIntegrationCreateEventArgs(this.ServiceProvider) { Integration = integration, Guild = guild }).ConfigureAwait(false); } /// /// Handles the guild integration update event. /// /// The guild. /// The integration. internal async Task OnGuildIntegrationUpdateEventAsync(DiscordGuild guild, DiscordIntegration integration) { integration.Discord = this; await this._guildIntegrationUpdated.InvokeAsync(this, new GuildIntegrationUpdateEventArgs(this.ServiceProvider) { Integration = integration, Guild = guild }).ConfigureAwait(false); } /// /// Handles the guild integration delete event. /// /// The guild. /// The integration_id. /// The application_id. internal async Task OnGuildIntegrationDeleteEventAsync(DiscordGuild guild, ulong integration_id, ulong? application_id) => await this._guildIntegrationDeleted.InvokeAsync(this, new GuildIntegrationDeleteEventArgs(this.ServiceProvider) { Guild = guild, IntegrationId = integration_id, ApplicationId = application_id }).ConfigureAwait(false); #endregion #region Guild Member /// /// Handles the guild member add event. /// /// The member. /// The guild. internal async Task OnGuildMemberAddEventAsync(TransportMember member, DiscordGuild guild) { var usr = new DiscordUser(member.User) { Discord = this }; usr = this.UserCache.AddOrUpdate(member.User.Id, usr, (id, old) => { old.Username = usr.Username; old.Discriminator = usr.Discriminator; old.AvatarHash = usr.AvatarHash; return old; }); var mbr = new DiscordMember(member) { Discord = this, _guild_id = guild.Id }; guild._members[mbr.Id] = mbr; guild.MemberCount++; var ea = new GuildMemberAddEventArgs(this.ServiceProvider) { Guild = guild, Member = mbr }; await this._guildMemberAdded.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the guild member remove event. /// /// The user. /// The guild. internal async Task OnGuildMemberRemoveEventAsync(TransportUser user, DiscordGuild guild) { var usr = new DiscordUser(user); if (!guild._members.TryRemove(user.Id, out var mbr)) mbr = new DiscordMember(usr) { Discord = this, _guild_id = guild.Id }; guild.MemberCount--; _ = this.UserCache.AddOrUpdate(user.Id, usr, (old, @new) => @new); var ea = new GuildMemberRemoveEventArgs(this.ServiceProvider) { Guild = guild, Member = mbr }; await this._guildMemberRemoved.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the guild member update event. /// /// The member. /// The guild. /// The roles. /// The nick. /// If true, pending. internal async Task OnGuildMemberUpdateEventAsync(TransportMember member, DiscordGuild guild, IEnumerable roles, string nick, bool? pending) { var usr = new DiscordUser(member.User) { Discord = this }; usr = this.UserCache.AddOrUpdate(usr.Id, usr, (id, old) => { old.Username = usr.Username; old.Discriminator = usr.Discriminator; old.AvatarHash = usr.AvatarHash; return old; }); if (!guild.Members.TryGetValue(member.User.Id, out var mbr)) mbr = new DiscordMember(usr) { Discord = this, _guild_id = guild.Id }; var nick_old = mbr.Nickname; var pending_old = mbr.IsPending; var roles_old = new ReadOnlyCollection(new List(mbr.Roles)); var cdu_old = mbr.CommunicationDisabledUntil; mbr._avatarHash = member.AvatarHash; mbr.GuildAvatarHash = member.GuildAvatarHash; mbr.Nickname = nick; mbr.IsPending = pending; mbr.CommunicationDisabledUntil = member.CommunicationDisabledUntil; mbr._role_ids.Clear(); mbr._role_ids.AddRange(roles); var ea = new GuildMemberUpdateEventArgs(this.ServiceProvider) { Guild = guild, Member = mbr, NicknameAfter = mbr.Nickname, RolesAfter = new ReadOnlyCollection(new List(mbr.Roles)), PendingAfter = mbr.IsPending, TimeoutAfter = mbr.CommunicationDisabledUntil, NicknameBefore = nick_old, RolesBefore = roles_old, PendingBefore = pending_old, TimeoutBefore = cdu_old }; await this._guildMemberUpdated.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the guild members chunk event. /// /// The dat. internal async Task OnGuildMembersChunkEventAsync(JObject dat) { var guild = this.Guilds[(ulong)dat["guild_id"]]; var chunkIndex = (int)dat["chunk_index"]; var chunkCount = (int)dat["chunk_count"]; var nonce = (string)dat["nonce"]; var mbrs = new HashSet(); var pres = new HashSet(); var members = dat["members"].ToObject(); var memCount = members.Count(); for (var i = 0; i < memCount; i++) { var mbr = new DiscordMember(members[i]) { Discord = this, _guild_id = guild.Id }; if (!this.UserCache.ContainsKey(mbr.Id)) this.UserCache[mbr.Id] = new DiscordUser(members[i].User) { Discord = this }; guild._members[mbr.Id] = mbr; mbrs.Add(mbr); } guild.MemberCount = guild._members.Count; var ea = new GuildMembersChunkEventArgs(this.ServiceProvider) { Guild = guild, Members = new ReadOnlySet(mbrs), ChunkIndex = chunkIndex, ChunkCount = chunkCount, Nonce = nonce, }; if (dat["presences"] != null) { var presences = dat["presences"].ToObject(); var presCount = presences.Count(); for (var i = 0; i < presCount; i++) { var xp = presences[i]; xp.Discord = this; xp.Activity = new DiscordActivity(xp.RawActivity); if (xp.RawActivities != null) { xp._internalActivities = new DiscordActivity[xp.RawActivities.Length]; for (var j = 0; j < xp.RawActivities.Length; j++) xp._internalActivities[j] = new DiscordActivity(xp.RawActivities[j]); } pres.Add(xp); } ea.Presences = new ReadOnlySet(pres); } if (dat["not_found"] != null) { var nf = dat["not_found"].ToObject>(); ea.NotFound = new ReadOnlySet(nf); } await this._guildMembersChunked.InvokeAsync(this, ea).ConfigureAwait(false); } #endregion #region Guild Role /// /// Handles the guild role create event. /// /// The role. /// The guild. internal async Task OnGuildRoleCreateEventAsync(DiscordRole role, DiscordGuild guild) { role.Discord = this; role._guild_id = guild.Id; guild._roles[role.Id] = role; var ea = new GuildRoleCreateEventArgs(this.ServiceProvider) { Guild = guild, Role = role }; await this._guildRoleCreated.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the guild role update event. /// /// The role. /// The guild. internal async Task OnGuildRoleUpdateEventAsync(DiscordRole role, DiscordGuild guild) { var newRole = guild.GetRole(role.Id); var oldRole = new DiscordRole { _guild_id = guild.Id, _color = newRole._color, Discord = this, IsHoisted = newRole.IsHoisted, Id = newRole.Id, IsManaged = newRole.IsManaged, IsMentionable = newRole.IsMentionable, Name = newRole.Name, Permissions = newRole.Permissions, Position = newRole.Position }; newRole._guild_id = guild.Id; newRole._color = role._color; newRole.IsHoisted = role.IsHoisted; newRole.IsManaged = role.IsManaged; newRole.IsMentionable = role.IsMentionable; newRole.Name = role.Name; newRole.Permissions = role.Permissions; newRole.Position = role.Position; var ea = new GuildRoleUpdateEventArgs(this.ServiceProvider) { Guild = guild, RoleAfter = newRole, RoleBefore = oldRole }; await this._guildRoleUpdated.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the guild role delete event. /// /// The role id. /// The guild. internal async Task OnGuildRoleDeleteEventAsync(ulong roleId, DiscordGuild guild) { if (!guild._roles.TryRemove(roleId, out var role)) this.Logger.LogWarning($"Attempted to delete a nonexistent role ({roleId}) from guild ({guild})."); var ea = new GuildRoleDeleteEventArgs(this.ServiceProvider) { Guild = guild, Role = role }; await this._guildRoleDeleted.InvokeAsync(this, ea).ConfigureAwait(false); } #endregion #region Invite /// /// Handles the invite create event. /// /// The channel id. /// The guild id. /// The invite. internal async Task OnInviteCreateEventAsync(ulong channelId, ulong guildId, DiscordInvite invite) { var guild = this.InternalGetCachedGuild(guildId); var channel = this.InternalGetCachedChannel(channelId); invite.Discord = this; if(invite.Inviter is not null) { invite.Inviter.Discord = this; this.UserCache.AddOrUpdate(invite.Inviter.Id, invite.Inviter, (old, @new) => @new); } guild._invites[invite.Code] = invite; var ea = new InviteCreateEventArgs(this.ServiceProvider) { Channel = channel, Guild = guild, Invite = invite }; await this._inviteCreated.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the invite delete event. /// /// The channel id. /// The guild id. /// The dat. internal async Task OnInviteDeleteEventAsync(ulong channelId, ulong guildId, JToken dat) { var guild = this.InternalGetCachedGuild(guildId); var channel = this.InternalGetCachedChannel(channelId); if (!guild._invites.TryRemove(dat["code"].ToString(), out var invite)) { invite = dat.ToObject(); invite.Discord = this; } invite.IsRevoked = true; var ea = new InviteDeleteEventArgs(this.ServiceProvider) { Channel = channel, Guild = guild, Invite = invite }; await this._inviteDeleted.InvokeAsync(this, ea).ConfigureAwait(false); } #endregion #region Message /// /// Handles the message ack event. /// /// The chn. /// The message id. internal async Task OnMessageAckEventAsync(DiscordChannel chn, ulong messageId) { if (this.MessageCache == null || !this.MessageCache.TryGet(xm => xm.Id == messageId && xm.ChannelId == chn.Id, out var msg)) { msg = new DiscordMessage { Id = messageId, ChannelId = chn.Id, Discord = this, }; } await this._messageAcknowledged.InvokeAsync(this, new MessageAcknowledgeEventArgs(this.ServiceProvider) { Message = msg }).ConfigureAwait(false); } /// /// Handles the message create event. /// /// The message. /// The author. /// The member. /// The reference author. /// The reference member. internal async Task OnMessageCreateEventAsync(DiscordMessage message, TransportUser author, TransportMember member, TransportUser referenceAuthor, TransportMember referenceMember) { message.Discord = this; this.PopulateMessageReactionsAndCache(message, author, member); message.PopulateMentions(); if (message.Channel == null && message.ChannelId == default) this.Logger.LogWarning(LoggerEvents.WebSocketReceive, "Channel which the last message belongs to is not in cache - cache state might be invalid!"); if (message.ReferencedMessage != null) { message.ReferencedMessage.Discord = this; this.PopulateMessageReactionsAndCache(message.ReferencedMessage, referenceAuthor, referenceMember); message.ReferencedMessage.PopulateMentions(); } foreach (var sticker in message.Stickers) sticker.Discord = this; var ea = new MessageCreateEventArgs(this.ServiceProvider) { Message = message, MentionedUsers = new ReadOnlyCollection(message._mentionedUsers), MentionedRoles = message._mentionedRoles != null ? new ReadOnlyCollection(message._mentionedRoles) : null, MentionedChannels = message._mentionedChannels != null ? new ReadOnlyCollection(message._mentionedChannels) : null }; await this._messageCreated.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the message update event. /// /// The message. /// The author. /// The member. /// The reference author. /// The reference member. internal async Task OnMessageUpdateEventAsync(DiscordMessage message, TransportUser author, TransportMember member, TransportUser referenceAuthor, TransportMember referenceMember) { DiscordGuild guild; message.Discord = this; var event_message = message; DiscordMessage oldmsg = null; if (this.Configuration.MessageCacheSize == 0 || this.MessageCache == null || !this.MessageCache.TryGet(xm => xm.Id == event_message.Id && xm.ChannelId == event_message.ChannelId, out message)) { message = event_message; this.PopulateMessageReactionsAndCache(message, author, member); guild = message.Channel?.Guild; if (message.ReferencedMessage != null) { message.ReferencedMessage.Discord = this; this.PopulateMessageReactionsAndCache(message.ReferencedMessage, referenceAuthor, referenceMember); message.ReferencedMessage.PopulateMentions(); } } else { oldmsg = new DiscordMessage(message); guild = message.Channel?.Guild; message.EditedTimestampRaw = event_message.EditedTimestampRaw; if (event_message.Content != null) message.Content = event_message.Content; message._embeds.Clear(); message._embeds.AddRange(event_message._embeds); message.Pinned = event_message.Pinned; message.IsTTS = event_message.IsTTS; } message.PopulateMentions(); var ea = new MessageUpdateEventArgs(this.ServiceProvider) { Message = message, MessageBefore = oldmsg, MentionedUsers = new ReadOnlyCollection(message._mentionedUsers), MentionedRoles = message._mentionedRoles != null ? new ReadOnlyCollection(message._mentionedRoles) : null, MentionedChannels = message._mentionedChannels != null ? new ReadOnlyCollection(message._mentionedChannels) : null }; await this._messageUpdated.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the message delete event. /// /// The message id. /// The channel id. /// The guild id. internal async Task OnMessageDeleteEventAsync(ulong messageId, ulong channelId, ulong? guildId) { var channel = this.InternalGetCachedChannel(channelId) ?? this.InternalGetCachedThread(channelId); var guild = this.InternalGetCachedGuild(guildId); if (channel == null || this.Configuration.MessageCacheSize == 0 || this.MessageCache == null || !this.MessageCache.TryGet(xm => xm.Id == messageId && xm.ChannelId == channelId, out var msg)) { msg = new DiscordMessage { Id = messageId, ChannelId = channelId, Discord = this, }; } if (this.Configuration.MessageCacheSize > 0) this.MessageCache?.Remove(xm => xm.Id == msg.Id && xm.ChannelId == channelId); var ea = new MessageDeleteEventArgs(this.ServiceProvider) { Channel = channel, Message = msg, Guild = guild }; await this._messageDeleted.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the message bulk delete event. /// /// The message ids. /// The channel id. /// The guild id. internal async Task OnMessageBulkDeleteEventAsync(ulong[] messageIds, ulong channelId, ulong? guildId) { var channel = this.InternalGetCachedChannel(channelId) ?? this.InternalGetCachedThread(channelId); var msgs = new List(messageIds.Length); foreach (var messageId in messageIds) { if (channel == null || this.Configuration.MessageCacheSize == 0 || this.MessageCache == null || !this.MessageCache.TryGet(xm => xm.Id == messageId && xm.ChannelId == channelId, out var msg)) { msg = new DiscordMessage { Id = messageId, ChannelId = channelId, Discord = this, }; } if (this.Configuration.MessageCacheSize > 0) this.MessageCache?.Remove(xm => xm.Id == msg.Id && xm.ChannelId == channelId); msgs.Add(msg); } var guild = this.InternalGetCachedGuild(guildId); var ea = new MessageBulkDeleteEventArgs(this.ServiceProvider) { Channel = channel, Messages = new ReadOnlyCollection(msgs), Guild = guild }; await this._messagesBulkDeleted.InvokeAsync(this, ea).ConfigureAwait(false); } #endregion #region Message Reaction /// /// Handles the message reaction add. /// /// The user id. /// The message id. /// The channel id. /// The guild id. /// The mbr. /// The emoji. internal async Task OnMessageReactionAddAsync(ulong userId, ulong messageId, ulong channelId, ulong? guildId, TransportMember mbr, DiscordEmoji emoji) { var channel = this.InternalGetCachedChannel(channelId) ?? this.InternalGetCachedThread(channelId); var guild = this.InternalGetCachedGuild(guildId); emoji.Discord = this; var usr = this.UpdateUser(new DiscordUser { Id = userId, Discord = this }, guildId, guild, mbr); if (channel == null || this.Configuration.MessageCacheSize == 0 || this.MessageCache == null || !this.MessageCache.TryGet(xm => xm.Id == messageId && xm.ChannelId == channelId, out var msg)) { msg = new DiscordMessage { Id = messageId, ChannelId = channelId, Discord = this, _reactions = new List() }; } var react = msg._reactions.FirstOrDefault(xr => xr.Emoji == emoji); if (react == null) { msg._reactions.Add(react = new DiscordReaction { Count = 1, Emoji = emoji, IsMe = this.CurrentUser.Id == userId }); } else { react.Count++; react.IsMe |= this.CurrentUser.Id == userId; } var ea = new MessageReactionAddEventArgs(this.ServiceProvider) { Message = msg, User = usr, Guild = guild, Emoji = emoji }; await this._messageReactionAdded.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the message reaction remove. /// /// The user id. /// The message id. /// The channel id. /// The guild id. /// The emoji. internal async Task OnMessageReactionRemoveAsync(ulong userId, ulong messageId, ulong channelId, ulong? guildId, DiscordEmoji emoji) { var channel = this.InternalGetCachedChannel(channelId) ?? this.InternalGetCachedThread(channelId); emoji.Discord = this; if (!this.UserCache.TryGetValue(userId, out var usr)) usr = new DiscordUser { Id = userId, Discord = this }; if (channel?.Guild != null) usr = channel.Guild.Members.TryGetValue(userId, out var member) ? member : new DiscordMember(usr) { Discord = this, _guild_id = channel.GuildId.Value }; if (channel == null || this.Configuration.MessageCacheSize == 0 || this.MessageCache == null || !this.MessageCache.TryGet(xm => xm.Id == messageId && xm.ChannelId == channelId, out var msg)) { msg = new DiscordMessage { Id = messageId, ChannelId = channelId, Discord = this }; } var react = msg._reactions?.FirstOrDefault(xr => xr.Emoji == emoji); if (react != null) { react.Count--; react.IsMe &= this.CurrentUser.Id != userId; if (msg._reactions != null && react.Count <= 0) // shit happens for (var i = 0; i < msg._reactions.Count; i++) if (msg._reactions[i].Emoji == emoji) { msg._reactions.RemoveAt(i); break; } } var guild = this.InternalGetCachedGuild(guildId); var ea = new MessageReactionRemoveEventArgs(this.ServiceProvider) { Message = msg, User = usr, Guild = guild, Emoji = emoji }; await this._messageReactionRemoved.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the message reaction remove all. /// /// The message id. /// The channel id. /// The guild id. internal async Task OnMessageReactionRemoveAllAsync(ulong messageId, ulong channelId, ulong? guildId) { var channel = this.InternalGetCachedChannel(channelId) ?? this.InternalGetCachedThread(channelId); if (channel == null || this.Configuration.MessageCacheSize == 0 || this.MessageCache == null || !this.MessageCache.TryGet(xm => xm.Id == messageId && xm.ChannelId == channelId, out var msg)) { msg = new DiscordMessage { Id = messageId, ChannelId = channelId, Discord = this }; } msg._reactions?.Clear(); var guild = this.InternalGetCachedGuild(guildId); var ea = new MessageReactionsClearEventArgs(this.ServiceProvider) { Message = msg }; await this._messageReactionsCleared.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the message reaction remove emoji. /// /// The message id. /// The channel id. /// The guild id. /// The dat. internal async Task OnMessageReactionRemoveEmojiAsync(ulong messageId, ulong channelId, ulong guildId, JToken dat) { var guild = this.InternalGetCachedGuild(guildId); var channel = this.InternalGetCachedChannel(channelId) ?? this.InternalGetCachedThread(channelId); if (channel == null || this.Configuration.MessageCacheSize == 0 || this.MessageCache == null || !this.MessageCache.TryGet(xm => xm.Id == messageId && xm.ChannelId == channelId, out var msg)) { msg = new DiscordMessage { Id = messageId, ChannelId = channelId, Discord = this }; } var partialEmoji = dat.ToObject(); if (!guild._emojis.TryGetValue(partialEmoji.Id, out var emoji)) { emoji = partialEmoji; emoji.Discord = this; } msg._reactions?.RemoveAll(r => r.Emoji.Equals(emoji)); var ea = new MessageReactionRemoveEmojiEventArgs(this.ServiceProvider) { Channel = channel, Guild = guild, Message = msg, Emoji = emoji }; await this._messageReactionRemovedEmoji.InvokeAsync(this, ea).ConfigureAwait(false); } #endregion #region Stage Instance /// /// Dispatches the event. /// /// The created stage instance. internal async Task OnStageInstanceCreateEventAsync(DiscordStageInstance stage) { stage.Discord = this; var guild = this.InternalGetCachedGuild(stage.GuildId); guild._stageInstances[stage.Id] = stage; await this._stageInstanceCreated.InvokeAsync(this, new StageInstanceCreateEventArgs(this.ServiceProvider) { StageInstance = stage, Guild = stage.Guild }).ConfigureAwait(false); } /// /// Dispatches the event. /// /// The updated stage instance. internal async Task OnStageInstanceUpdateEventAsync(DiscordStageInstance stage) { stage.Discord = this; var guild = this.InternalGetCachedGuild(stage.GuildId); guild._stageInstances[stage.Id] = stage; await this._stageInstanceUpdated.InvokeAsync(this, new StageInstanceUpdateEventArgs(this.ServiceProvider) { StageInstance = stage, Guild = stage.Guild }).ConfigureAwait(false); } /// /// Dispatches the event. /// /// The deleted stage instance. internal async Task OnStageInstanceDeleteEventAsync(DiscordStageInstance stage) { stage.Discord = this; var guild = this.InternalGetCachedGuild(stage.GuildId); guild._stageInstances[stage.Id] = stage; await this._stageInstanceDeleted.InvokeAsync(this, new StageInstanceDeleteEventArgs(this.ServiceProvider) { StageInstance = stage, Guild = stage.Guild }).ConfigureAwait(false); } #endregion #region Thread /// /// Dispatches the event. /// /// The created thread. internal async Task OnThreadCreateEventAsync(DiscordThreadChannel thread) { thread.Discord = this; this.InternalGetCachedGuild(thread.GuildId)._threads.AddOrUpdate(thread.Id, thread, (oldThread, newThread) => newThread); await this._threadCreated.InvokeAsync(this, new ThreadCreateEventArgs(this.ServiceProvider) { Thread = thread, Guild = thread.Guild, Parent = thread.Parent }).ConfigureAwait(false); } /// /// Dispatches the event. /// /// The updated thread. internal async Task OnThreadUpdateEventAsync(DiscordThreadChannel thread) { if (thread == null) return; thread.Discord = this; var guild = thread.Guild; var threadNew = this.InternalGetCachedThread(thread.Id); DiscordThreadChannel threadOld = null; ThreadUpdateEventArgs updateEvent; if (threadNew != null) { threadOld = new DiscordThreadChannel { Discord = this, Type = threadNew.Type, ThreadMetadata = thread.ThreadMetadata, _threadMembers = threadNew._threadMembers, ParentId = thread.ParentId, OwnerId = thread.OwnerId, Name = thread.Name, LastMessageId = threadNew.LastMessageId, MessageCount = thread.MessageCount, MemberCount = thread.MemberCount, GuildId = thread.GuildId, LastPinTimestampRaw = threadNew.LastPinTimestampRaw, PerUserRateLimit = threadNew.PerUserRateLimit, CurrentMember = threadNew.CurrentMember }; threadNew.ThreadMetadata = thread.ThreadMetadata; threadNew.ParentId = thread.ParentId; threadNew.OwnerId = thread.OwnerId; threadNew.Name = thread.Name; threadNew.LastMessageId = thread.LastMessageId.HasValue ? thread.LastMessageId : threadOld.LastMessageId; threadNew.MessageCount = thread.MessageCount; threadNew.MemberCount = thread.MemberCount; threadNew.GuildId = thread.GuildId; updateEvent = new ThreadUpdateEventArgs(this.ServiceProvider) { ThreadAfter = thread, ThreadBefore = threadOld, Guild = thread.Guild, Parent = thread.Parent }; } else { updateEvent = new ThreadUpdateEventArgs(this.ServiceProvider) { ThreadAfter = thread, Guild = thread.Guild, Parent = thread.Parent }; guild._threads[thread.Id] = thread; } await this._threadUpdated.InvokeAsync(this, updateEvent).ConfigureAwait(false); } /// /// Dispatches the event. /// /// The deleted thread. internal async Task OnThreadDeleteEventAsync(DiscordThreadChannel thread) { if (thread == null) return; thread.Discord = this; var gld = thread.Guild; if (gld._threads.TryRemove(thread.Id, out var cachedThread)) thread = cachedThread; await this._threadDeleted.InvokeAsync(this, new ThreadDeleteEventArgs(this.ServiceProvider) { Thread = thread, Guild = thread.Guild, Parent = thread.Parent, Type = thread.Type }).ConfigureAwait(false); } /// /// Dispatches the event. /// /// The synced guild. /// The synced channel ids. /// The synced threads. /// The synced members. internal async Task OnThreadListSyncEventAsync(DiscordGuild guild, IReadOnlyList channel_ids, IReadOnlyList threads, IReadOnlyList members) { guild.Discord = this; var channels = channel_ids.Select(x => guild.GetChannel(x.Value)); //getting channel objects foreach (var chan in channels) { chan.Discord = this; } threads.Select(x => x.Discord = this); await this._threadListSynced.InvokeAsync(this, new ThreadListSyncEventArgs(this.ServiceProvider) { Guild = guild, Channels = channels.ToList().AsReadOnly(), Threads = threads, Members = members.ToList().AsReadOnly() }).ConfigureAwait(false); } /// /// Dispatches the event. /// /// The updated member. internal async Task OnThreadMemberUpdateEventAsync(DiscordThreadChannelMember member) { member.Discord = this; var thread = this.InternalGetCachedThread(member.Id); if (thread == null) { var tempThread = await this.ApiClient.GetThreadAsync(member.Id); thread = this._guilds[member._guild_id]._threads.AddOrUpdate(member.Id, tempThread, (old, newThread) => newThread); } thread.CurrentMember = member; thread.Guild._threads.AddOrUpdate(member.Id, thread, (oldThread, newThread) => newThread); await this._threadMemberUpdated.InvokeAsync(this, new ThreadMemberUpdateEventArgs(this.ServiceProvider) { ThreadMember = member, Thread = thread }).ConfigureAwait(false); } /// /// Dispatches the event. /// /// The target guild. /// The thread id of the target thread this update belongs to. /// The added members. /// The ids of the removed members. /// The new member count. internal async Task OnThreadMembersUpdateEventAsync(DiscordGuild guild, ulong thread_id, JArray added_members, JArray removed_members, int member_count) { var thread = this.InternalGetCachedThread(thread_id); if (thread == null) { var tempThread = await this.ApiClient.GetThreadAsync(thread_id); thread = guild._threads.AddOrUpdate(thread_id, tempThread, (old, newThread) => newThread); } thread.Discord = this; guild.Discord = this; List addedMembers = new(); List removed_member_ids = new(); if (added_members != null) { foreach (var xj in added_members) { var xtm = xj.ToDiscordObject(); xtm.Discord = this; xtm._guild_id = guild.Id; if(xtm != null) addedMembers.Add(xtm); if (xtm.Id == this.CurrentUser.Id) thread.CurrentMember = xtm; } } var removedMembers = new List(); if (removed_members != null) { foreach (var removedId in removed_members) { removedMembers.Add(guild._members.TryGetValue((ulong)removedId, out var member) ? member : new DiscordMember { Id = (ulong)removedId, _guild_id = guild.Id, Discord = this }); } } if (removed_member_ids.Contains(this.CurrentUser.Id)) //indicates the bot was removed from the thread thread.CurrentMember = null; thread.MemberCount = member_count; var threadMembersUpdateArg = new ThreadMembersUpdateEventArgs(this.ServiceProvider) { Guild = guild, Thread = thread, AddedMembers = addedMembers, RemovedMembers = removedMembers, MemberCount = member_count }; await this._threadMembersUpdated.InvokeAsync(this, threadMembersUpdateArg).ConfigureAwait(false); } #endregion #region Activities /// /// Dispatches the event. /// /// The transport activity. /// The guild. /// The channel id. /// The users in the activity. /// The application id. /// A Task. internal async Task OnEmbeddedActivityUpdateAsync(JObject tr_activity, DiscordGuild guild, ulong channel_id, JArray j_users, ulong app_id) => await Task.Delay(20); /*{ try { var users = j_users?.ToObject>(); DiscordActivity old = null; var uid = $"{guild.Id}_{channel_id}_{app_id}"; if (this._embeddedActivities.TryGetValue(uid, out var activity)) { old = new DiscordActivity(activity); DiscordJson.PopulateObject(tr_activity, activity); } else { activity = tr_activity.ToObject(); this._embeddedActivities[uid] = activity; } var activity_users = new List(); var channel = this.InternalGetCachedChannel(channel_id) ?? await this.ApiClient.GetChannelAsync(channel_id); if (users != null) { foreach (var user in users) { var activity_user = guild._members.TryGetValue(user, out var member) ? member : new DiscordMember { Id = user, _guild_id = guild.Id, Discord = this }; activity_users.Add(activity_user); } } else activity_users = null; var ea = new EmbeddedActivityUpdateEventArgs(this.ServiceProvider) { Guild = guild, Users = activity_users, Channel = channel }; await this._embeddedActivityUpdated.InvokeAsync(this, ea).ConfigureAwait(false); } catch (Exception ex) { this.Logger.LogError(ex, ex.Message); } }*/ #endregion #region User/Presence Update /// /// Handles the presence update event. /// /// The raw presence. /// The raw user. internal async Task OnPresenceUpdateEventAsync(JObject rawPresence, JObject rawUser) { var uid = (ulong)rawUser["id"]; DiscordPresence old = null; if (this._presences.TryGetValue(uid, out var presence)) { old = new DiscordPresence(presence); DiscordJson.PopulateObject(rawPresence, presence); } else { presence = rawPresence.ToObject(); presence.Discord = this; presence.Activity = new DiscordActivity(presence.RawActivity); this._presences[presence.InternalUser.Id] = presence; } // reuse arrays / avoid linq (this is a hot zone) if (presence.Activities == null || rawPresence["activities"] == null) { presence._internalActivities = Array.Empty(); } else { if (presence._internalActivities.Length != presence.RawActivities.Length) presence._internalActivities = new DiscordActivity[presence.RawActivities.Length]; for (var i = 0; i < presence._internalActivities.Length; i++) presence._internalActivities[i] = new DiscordActivity(presence.RawActivities[i]); if (presence._internalActivities.Length > 0) { presence.RawActivity = presence.RawActivities[0]; if (presence.Activity != null) presence.Activity.UpdateWith(presence.RawActivity); else presence.Activity = new DiscordActivity(presence.RawActivity); } } if (this.UserCache.TryGetValue(uid, out var usr)) { if (old != null) { old.InternalUser.Username = usr.Username; old.InternalUser.Discriminator = usr.Discriminator; old.InternalUser.AvatarHash = usr.AvatarHash; } if (rawUser["username"] is object) usr.Username = (string)rawUser["username"]; if (rawUser["discriminator"] is object) usr.Discriminator = (string)rawUser["discriminator"]; if (rawUser["avatar"] is object) usr.AvatarHash = (string)rawUser["avatar"]; presence.InternalUser.Username = usr.Username; presence.InternalUser.Discriminator = usr.Discriminator; presence.InternalUser.AvatarHash = usr.AvatarHash; } var usrafter = usr ?? new DiscordUser(presence.InternalUser); var ea = new PresenceUpdateEventArgs(this.ServiceProvider) { Status = presence.Status, Activity = presence.Activity, User = usr, PresenceBefore = old, PresenceAfter = presence, UserBefore = old != null ? new DiscordUser(old.InternalUser) : usrafter, UserAfter = usrafter }; await this._presenceUpdated.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the user settings update event. /// /// The user. internal async Task OnUserSettingsUpdateEventAsync(TransportUser user) { var usr = new DiscordUser(user) { Discord = this }; var ea = new UserSettingsUpdateEventArgs(this.ServiceProvider) { User = usr }; await this._userSettingsUpdated.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the user update event. /// /// The user. internal async Task OnUserUpdateEventAsync(TransportUser user) { var usr_old = new DiscordUser { AvatarHash = this.CurrentUser.AvatarHash, Discord = this, Discriminator = this.CurrentUser.Discriminator, Email = this.CurrentUser.Email, Id = this.CurrentUser.Id, IsBot = this.CurrentUser.IsBot, MfaEnabled = this.CurrentUser.MfaEnabled, Username = this.CurrentUser.Username, Verified = this.CurrentUser.Verified }; this.CurrentUser.AvatarHash = user.AvatarHash; this.CurrentUser.Discriminator = user.Discriminator; this.CurrentUser.Email = user.Email; this.CurrentUser.Id = user.Id; this.CurrentUser.IsBot = user.IsBot; this.CurrentUser.MfaEnabled = user.MfaEnabled; this.CurrentUser.Username = user.Username; this.CurrentUser.Verified = user.Verified; var ea = new UserUpdateEventArgs(this.ServiceProvider) { UserAfter = this.CurrentUser, UserBefore = usr_old }; await this._userUpdated.InvokeAsync(this, ea).ConfigureAwait(false); } #endregion #region Voice /// /// Handles the voice state update event. /// /// The raw. internal async Task OnVoiceStateUpdateEventAsync(JObject raw) { var gid = (ulong)raw["guild_id"]; var uid = (ulong)raw["user_id"]; var gld = this._guilds[gid]; var vstateNew = raw.ToObject(); vstateNew.Discord = this; gld._voiceStates.TryRemove(uid, out var vstateOld); if (vstateNew.Channel != null) { gld._voiceStates[vstateNew.UserId] = vstateNew; } if (gld._members.TryGetValue(uid, out var mbr)) { mbr.IsMuted = vstateNew.IsServerMuted; mbr.IsDeafened = vstateNew.IsServerDeafened; } else { var transportMbr = vstateNew.TransportMember; this.UpdateUser(new DiscordUser(transportMbr.User) { Discord = this }, gid, gld, transportMbr); } var ea = new VoiceStateUpdateEventArgs(this.ServiceProvider) { Guild = vstateNew.Guild, Channel = vstateNew.Channel, User = vstateNew.User, SessionId = vstateNew.SessionId, Before = vstateOld, After = vstateNew }; await this._voiceStateUpdated.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the voice server update event. /// /// The endpoint. /// The token. /// The guild. internal async Task OnVoiceServerUpdateEventAsync(string endpoint, string token, DiscordGuild guild) { var ea = new VoiceServerUpdateEventArgs(this.ServiceProvider) { Endpoint = endpoint, VoiceToken = token, Guild = guild }; await this._voiceServerUpdated.InvokeAsync(this, ea).ConfigureAwait(false); } #endregion #region Commands /// /// Handles the application command create. /// /// The cmd. /// The guild_id. internal async Task OnApplicationCommandCreateAsync(DiscordApplicationCommand cmd, ulong? guild_id) { cmd.Discord = this; var guild = this.InternalGetCachedGuild(guild_id); if (guild == null && guild_id.HasValue) { guild = new DiscordGuild { Id = guild_id.Value, Discord = this }; } var ea = new ApplicationCommandEventArgs(this.ServiceProvider) { Guild = guild, Command = cmd }; await this._applicationCommandCreated.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the application command update. /// /// The cmd. /// The guild_id. internal async Task OnApplicationCommandUpdateAsync(DiscordApplicationCommand cmd, ulong? guild_id) { cmd.Discord = this; var guild = this.InternalGetCachedGuild(guild_id); if (guild == null && guild_id.HasValue) { guild = new DiscordGuild { Id = guild_id.Value, Discord = this }; } var ea = new ApplicationCommandEventArgs(this.ServiceProvider) { Guild = guild, Command = cmd }; await this._applicationCommandUpdated.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the application command delete. /// /// The cmd. /// The guild_id. internal async Task OnApplicationCommandDeleteAsync(DiscordApplicationCommand cmd, ulong? guild_id) { cmd.Discord = this; var guild = this.InternalGetCachedGuild(guild_id); if (guild == null && guild_id.HasValue) { guild = new DiscordGuild { Id = guild_id.Value, Discord = this }; } var ea = new ApplicationCommandEventArgs(this.ServiceProvider) { Guild = guild, Command = cmd }; await this._applicationCommandDeleted.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the guild application command counts update. /// /// The count. /// The count. /// The count. /// The guild_id. /// Count of application commands. internal async Task OnGuildApplicationCommandCountsUpdateAsync(int sc, int ucmc, int mcmc, ulong guild_id) { var guild = this.InternalGetCachedGuild(guild_id); if (guild == null) { guild = new DiscordGuild { Id = guild_id, Discord = this }; } var ea = new GuildApplicationCommandCountEventArgs(this.ServiceProvider) { SlashCommands = sc, UserContextMenuCommands = ucmc, MessageContextMenuCommands = mcmc, Guild = guild }; await this._guildApplicationCommandCountUpdated.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the application command permissions update. /// /// The new permissions. /// The command id. /// The guild id. /// The application id. internal async Task OnApplicationCommandPermissionsUpdateAsync(IEnumerable perms, ulong c_id, ulong guild_id, ulong a_id) { if (a_id != this.CurrentApplication.Id) return; var guild = this.InternalGetCachedGuild(guild_id); DiscordApplicationCommand cmd; try { cmd = await this.GetGuildApplicationCommandAsync(guild_id, c_id); } catch(NotFoundException) { cmd = await this.GetGlobalApplicationCommandAsync(c_id); } if (guild == null) { guild = new DiscordGuild { Id = guild_id, Discord = this }; } var ea = new ApplicationCommandPermissionsUpdateEventArgs(this.ServiceProvider) { Permissions = perms.ToList(), Command = cmd, ApplicationId = a_id, Guild = guild }; await this._applicationCommandPermissionsUpdated.InvokeAsync(this, ea).ConfigureAwait(false); } #endregion #region Interaction /// /// Handles the interaction create. /// /// The guild id. /// The channel id. /// The user. /// The member. /// The interaction. internal async Task OnInteractionCreateAsync(ulong? guildId, ulong channelId, TransportUser user, TransportMember member, DiscordInteraction interaction) { var usr = new DiscordUser(user) { Discord = this }; interaction.ChannelId = channelId; interaction.GuildId = guildId; interaction.Discord = this; interaction.Data.Discord = this; if (member != null) { usr = new DiscordMember(member) { _guild_id = guildId.Value, Discord = this }; this.UpdateUser(usr, guildId, interaction.Guild, member); } else { this.UserCache.AddOrUpdate(usr.Id, usr, (old, @new) => @new); } interaction.User = usr; var resolved = interaction.Data.Resolved; if (resolved != null) { if (resolved.Users != null) { foreach (var c in resolved.Users) { c.Value.Discord = this; this.UserCache.AddOrUpdate(c.Value.Id, c.Value, (old, @new) => @new); } } if (resolved.Members != null) { foreach (var c in resolved.Members) { c.Value.Discord = this; c.Value.Id = c.Key; c.Value._guild_id = guildId.Value; c.Value.User.Discord = this; this.UserCache.AddOrUpdate(c.Value.User.Id, c.Value.User, (old, @new) => @new); } } if (resolved.Channels != null) { foreach (var c in resolved.Channels) { c.Value.Discord = this; if (guildId.HasValue) c.Value.GuildId = guildId.Value; } } if (resolved.Roles != null) { foreach (var c in resolved.Roles) { c.Value.Discord = this; if (guildId.HasValue) c.Value._guild_id = guildId.Value; } } if (resolved.Messages != null) { foreach (var m in resolved.Messages) { m.Value.Discord = this; if (guildId.HasValue) m.Value.GuildId = guildId.Value; } } } if (interaction.Type is InteractionType.Component || interaction.Type is InteractionType.ModalSubmit) { - - interaction.Message.Discord = this; - interaction.Message.ChannelId = interaction.ChannelId; + if (interaction.Message != null) + { + interaction.Message.Discord = this; + interaction.Message.ChannelId = interaction.ChannelId; + } var cea = new ComponentInteractionCreateEventArgs(this.ServiceProvider) { Message = interaction.Message, Interaction = interaction }; await this._componentInteractionCreated.InvokeAsync(this, cea).ConfigureAwait(false); } else { if (interaction.Data.Target.HasValue) // Context-Menu. // { var targetId = interaction.Data.Target.Value; DiscordUser targetUser = null; DiscordMember targetMember = null; DiscordMessage targetMessage = null; interaction.Data.Resolved.Messages?.TryGetValue(targetId, out targetMessage); interaction.Data.Resolved.Members?.TryGetValue(targetId, out targetMember); interaction.Data.Resolved.Users?.TryGetValue(targetId, out targetUser); var ctea = new ContextMenuInteractionCreateEventArgs(this.ServiceProvider) { Interaction = interaction, TargetUser = targetMember ?? targetUser, TargetMessage = targetMessage, Type = interaction.Data.Type, }; await this._contextMenuInteractionCreated.InvokeAsync(this, ctea).ConfigureAwait(false); } else { var ea = new InteractionCreateEventArgs(this.ServiceProvider) { Interaction = interaction }; await this._interactionCreated.InvokeAsync(this, ea).ConfigureAwait(false); } } } #endregion #region Misc /// /// Handles the typing start event. /// /// The user id. /// The channel id. /// The channel. /// The guild id. /// The started. /// The mbr. internal async Task OnTypingStartEventAsync(ulong userId, ulong channelId, DiscordChannel channel, ulong? guildId, DateTimeOffset started, TransportMember mbr) { if (channel == null) { channel = new DiscordChannel { Discord = this, Id = channelId, GuildId = guildId ?? default, }; } var guild = this.InternalGetCachedGuild(guildId); var usr = this.UpdateUser(new DiscordUser { Id = userId, Discord = this }, guildId, guild, mbr); var ea = new TypingStartEventArgs(this.ServiceProvider) { Channel = channel, User = usr, Guild = guild, StartedAt = started }; await this._typingStarted.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the webhooks update. /// /// The channel. /// The guild. internal async Task OnWebhooksUpdateAsync(DiscordChannel channel, DiscordGuild guild) { var ea = new WebhooksUpdateEventArgs(this.ServiceProvider) { Channel = channel, Guild = guild }; await this._webhooksUpdated.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the unknown event. /// /// The payload. internal async Task OnUnknownEventAsync(GatewayPayload payload) { var ea = new UnknownEventArgs(this.ServiceProvider) { EventName = payload.EventName, Json = (payload.Data as JObject)?.ToString() }; await this._unknownEvent.InvokeAsync(this, ea).ConfigureAwait(false); } #endregion #endregion } } diff --git a/DisCatSharp/DisCatSharp.csproj b/DisCatSharp/DisCatSharp.csproj index 63c826ce2..ab435d598 100644 --- a/DisCatSharp/DisCatSharp.csproj +++ b/DisCatSharp/DisCatSharp.csproj @@ -1,66 +1,72 @@ DisCatSharp DisCatSharp Library netstandard2.0 DisCatSharp Another C# API/Framework for Discord Bots. discord, discord-api, bots, discord-bots, chat, dcs, discatsharp, csharp, dotnet, vb-net, fsharp, webhooks LICENSE.md True True True Resources.resx ResXFileCodeGenerator Resources.Designer.cs + + + + + + diff --git a/DisCatSharp/Entities/Application/DiscordApplicationCommand.cs b/DisCatSharp/Entities/Application/DiscordApplicationCommand.cs index acdbf321b..d5b9bdeb4 100644 --- a/DisCatSharp/Entities/Application/DiscordApplicationCommand.cs +++ b/DisCatSharp/Entities/Application/DiscordApplicationCommand.cs @@ -1,190 +1,202 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using DisCatSharp.Enums; using Newtonsoft.Json; namespace DisCatSharp.Entities { /// /// Represents a command that is registered to an application. /// public sealed class DiscordApplicationCommand : SnowflakeObject, IEquatable { /// /// Gets the type of this application command. /// [JsonProperty("type")] public ApplicationCommandType Type { get; internal set; } /// /// Gets the unique ID of this command's application. /// [JsonProperty("application_id")] public ulong ApplicationId { get; internal set; } /// /// Gets the name of this command. /// [JsonProperty("name")] public string Name { get; internal set; } /// /// Sets the name localizations. /// [JsonProperty("name_localizations", NullValueHandling = NullValueHandling.Ignore)] internal Dictionary RawNameLocalizations { get; set; } /// /// Gets the name localizations. /// [JsonIgnore] public DiscordApplicationCommandLocalization NameLocalizations => new(this.RawNameLocalizations); /// /// Gets the description of this command. /// [JsonProperty("description")] public string Description { get; internal set; } /// /// Sets the description localizations. /// [JsonProperty("description_localizations", NullValueHandling = NullValueHandling.Ignore)] internal Dictionary RawDescriptionLocalizations { get; set; } /// /// Gets the description localizations. /// [JsonIgnore] public DiscordApplicationCommandLocalization DescriptionLocalizations => new(this.RawDescriptionLocalizations); /// /// Gets the potential parameters for this command. /// [JsonProperty("options", NullValueHandling = NullValueHandling.Ignore)] public IReadOnlyCollection Options { get; internal set; } /// /// Gets whether the command is enabled by default when the app is added to a guild. /// [JsonProperty("default_permission", NullValueHandling = NullValueHandling.Ignore)] public bool DefaultPermission { get; internal set; } + /// + /// Gets the commands needed permissions. + /// + [JsonProperty("default_member_permissions", NullValueHandling = NullValueHandling.Ignore)] + public Permissions? Permission { get; internal set; } + + /// + /// Gets whether the command can be used in direct messages. + /// + [JsonProperty("dm_permission", NullValueHandling = NullValueHandling.Ignore)] + public bool? DmPermission { get; internal set; } + /// /// Gets the version number for this command. /// [JsonProperty("version")] public ulong Version { get; internal set; } /// /// Creates a new instance of a . /// /// The name of the command. /// The description of the command. /// Optional parameters for this command. /// Optional default permission for this command. /// The type of the command. Defaults to ChatInput. /// The localizations of the command name. /// The localizations of the command description. public DiscordApplicationCommand(string name, string description, IEnumerable options = null, bool default_permission = true, ApplicationCommandType type = ApplicationCommandType.ChatInput, DiscordApplicationCommandLocalization nameLocalizations = null, DiscordApplicationCommandLocalization descriptionLocalizations = null) { if (type is ApplicationCommandType.ChatInput) { if (!Utilities.IsValidSlashCommandName(name)) throw new ArgumentException("Invalid slash command name specified. It must be below 32 characters and not contain any whitespace.", nameof(name)); if (name.Any(ch => char.IsUpper(ch))) throw new ArgumentException("Slash command name cannot have any upper case characters.", nameof(name)); if (description.Length > 100) throw new ArgumentException("Slash command description cannot exceed 100 characters.", nameof(description)); this.RawNameLocalizations = nameLocalizations?.GetKeyValuePairs(); this.RawDescriptionLocalizations = descriptionLocalizations?.GetKeyValuePairs(); } else { if (!string.IsNullOrWhiteSpace(description)) throw new ArgumentException("Context menus do not support descriptions."); if (options?.Any() ?? false) throw new ArgumentException("Context menus do not support options."); description = string.Empty; this.RawNameLocalizations = nameLocalizations?.GetKeyValuePairs(); } var optionsList = options != null ? new ReadOnlyCollection(options.ToList()) : null; this.Type = type; this.Name = name; this.Description = description; this.Options = optionsList; this.DefaultPermission = default_permission; } /// /// Checks whether this object is equal to another object. /// /// The command to compare to. /// Whether the command is equal to this . public bool Equals(DiscordApplicationCommand other) => this.Id == other.Id; /// /// Determines if two objects are equal. /// /// The first command object. /// The second command object. /// Whether the two objects are equal. public static bool operator ==(DiscordApplicationCommand e1, DiscordApplicationCommand e2) => e1.Equals(e2); /// /// Determines if two objects are not equal. /// /// The first command object. /// The second command object. /// Whether the two objects are not equal. public static bool operator !=(DiscordApplicationCommand e1, DiscordApplicationCommand e2) => !(e1 == e2); /// /// Determines if a is equal to the current . /// /// The object to compare to. /// Whether the two objects are not equal. public override bool Equals(object other) => other is DiscordApplicationCommand dac && this.Equals(dac); /// /// Gets the hash code for this . /// /// The hash code for this . public override int GetHashCode() => this.Id.GetHashCode(); } } diff --git a/DisCatSharp/Entities/Interaction/Components/Text/DiscordTextComponent.cs b/DisCatSharp/Entities/Interaction/Components/Text/DiscordTextComponent.cs index 4380762eb..bbaaf5114 100644 --- a/DisCatSharp/Entities/Interaction/Components/Text/DiscordTextComponent.cs +++ b/DisCatSharp/Entities/Interaction/Components/Text/DiscordTextComponent.cs @@ -1,152 +1,153 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using DisCatSharp.Enums; using Newtonsoft.Json; namespace DisCatSharp.Entities { /// /// Represents a text component that can be submitted. Fires event when submitted. /// public sealed class DiscordTextComponent : DiscordComponent { /// /// The style of the text component. /// [JsonProperty("style", NullValueHandling = NullValueHandling.Ignore)] public TextComponentStyle Style { get; internal set; } /// /// The text description to apply to the text component. /// [JsonProperty("label", NullValueHandling = NullValueHandling.Ignore)] public string Label { get; internal set; } /// /// The placeholder for the text component. /// [JsonProperty("placeholder", NullValueHandling = NullValueHandling.Ignore)] public string Placeholder { get; internal set; } /// /// The pre-filled value for the text component. /// [JsonProperty("value", NullValueHandling = NullValueHandling.Ignore)] public string Value { get; internal set; } /// /// The minimal length of text input. /// [JsonProperty("min_length", NullValueHandling = NullValueHandling.Ignore)] public int? MinLength { get; internal set; } /// /// The maximal length of text input. /// [JsonProperty("max_length", NullValueHandling = NullValueHandling.Ignore)] public int? MaxLength { get; internal set; } + // NOTE: Probably will be introdruced in future /*/// /// Whether this text component can be used. /// [JsonProperty("disabled", NullValueHandling = NullValueHandling.Ignore)] public bool Disabled { get; internal set; }*/ /// /// Whether this text component is required. /// [JsonProperty("required", NullValueHandling = NullValueHandling.Ignore)] public bool Required { get; internal set; } /*/// /// Enables this component if it was disabled before. /// /// The current component. public DiscordTextComponent Enable() { this.Disabled = false; return this; } /// /// Disables this component. /// /// The current component. public DiscordTextComponent Disable() { this.Disabled = true; return this; }*/ /// /// Constructs a new . /// internal DiscordTextComponent() { this.Type = ComponentType.InputText; } /// /// Constucts a new text component based on another text component. /// /// The button to copy. public DiscordTextComponent(DiscordTextComponent other) : this() { this.CustomId = other.CustomId; this.Style = other.Style; this.Label = other.Label; //this.Disabled = other.Disabled; this.MinLength = other.MinLength; this.MaxLength = other.MaxLength; this.Placeholder = other.Placeholder; this.Required = other.Required; this.Value = other.Value; } /// /// Constructs a new text component field with the specified options. /// /// The style of the text component. /// The Id to assign to the text component. This is sent back when a user presses it. /// The text to display before the text component, up to 80 characters. /// The placeholder for the text input. /// The minimal length of text input. /// The maximal length of text input. /// Whether this text component should be required. /// Pre-filled value for text field. public DiscordTextComponent(TextComponentStyle style, string customId, string label, string placeholder = null, int? minLength = null, int? maxLength = null, bool required = true, string default_value = null) { this.Style = style; this.Label = label; this.CustomId = customId; this.MinLength = minLength; this.MaxLength = maxLength; this.Placeholder = placeholder; //this.Disabled = disabled; this.Required = required; this.Value = default_value; this.Type = ComponentType.InputText; } } } diff --git a/DisCatSharp/Entities/Interaction/DiscordInteractionModalBuilder.cs b/DisCatSharp/Entities/Interaction/DiscordInteractionModalBuilder.cs index 2c2b0d9da..2c96f7fdd 100644 --- a/DisCatSharp/Entities/Interaction/DiscordInteractionModalBuilder.cs +++ b/DisCatSharp/Entities/Interaction/DiscordInteractionModalBuilder.cs @@ -1,156 +1,156 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Generic; using System.Linq; namespace DisCatSharp.Entities { /// /// Constructs an interaction modal response. /// public sealed class DiscordInteractionModalBuilder { /// /// Title of modal. /// public string Title { get => this._title; set { if (value != null && value.Length > 128) throw new ArgumentException("Title length cannot exceed 128 characters.", nameof(value)); this._title = value; } } private string _title; /// /// Custom id of modal. /// public string CustomId { get; set; } /// /// Components to send on this interaction response. /// public IReadOnlyList ModalComponents => this._components; private readonly List _components = new(); /// /// Constructs a new empty interaction modal builder. /// public DiscordInteractionModalBuilder() { } public DiscordInteractionModalBuilder WithTitle(string title) { this.Title = title; return this; } public DiscordInteractionModalBuilder WithCustomId(string customId) { this.CustomId = customId; return this; } /// /// Appends a collection of components to the builder. Each call will append to a new row. /// /// The components to append. Up to five. /// The current builder to chain calls with. /// Thrown when passing more than 5 components. - public DiscordInteractionModalBuilder AddModalComponents(params DiscordComponent[] components) - => this.AddModalComponents((IEnumerable)components); + public DiscordInteractionModalBuilder AddModalComponents(params DiscordTextComponent[] components) + => this.AddModalComponents((IEnumerable)components); /// /// Appends a text component to the builder. /// /// The component to append. /// The current builder to chain calls with. public DiscordInteractionModalBuilder AddTextComponent(DiscordTextComponent component) { - List comp = new(1); + List comp = new(1); comp.Add(component); return this.AddModalComponents(comp); } /// /// Appends several rows of components to the message /// /// The rows of components to add, holding up to five each. /// public DiscordInteractionModalBuilder AddModalComponents(IEnumerable components) { var ara = components.ToArray(); if (ara.Length + this._components.Count > 5) throw new ArgumentException("ActionRow count exceeds maximum of five."); foreach (var ar in ara) this._components.Add(ar); return this; } /// /// Appends a collection of components to the builder. Each call will append to a new row. /// If you add a you can only add one. /// /// The components to append. Up to five. /// The current builder to chain calls with. /// Thrown when passing more than 5 components. - public DiscordInteractionModalBuilder AddModalComponents(IEnumerable components) + public DiscordInteractionModalBuilder AddModalComponents(IEnumerable components) { var compArr = components.ToArray(); var count = compArr.Length; if (count > 5) throw new ArgumentException("Cannot add more than 5 components per action row!"); if (components.Where(c => c.Type == Enums.ComponentType.InputText).Any() && count < 1) throw new ArgumentException("Cannot add more than 1 text components per action row!"); var arc = new DiscordActionRowComponent(compArr); this._components.Add(arc); return this; } /// /// Clears all message components on this builder. /// public void ClearComponents() => this._components.Clear(); /// /// Allows for clearing the Interaction Response Builder so that it can be used again to send a new response. /// public void Clear() { this._components.Clear(); this.Title = null; this.CustomId = null; } } } diff --git a/DisCatSharp/Net/Abstractions/Rest/RestApplicationCommandPayloads.cs b/DisCatSharp/Net/Abstractions/Rest/RestApplicationCommandPayloads.cs index 48ec13363..ba8a74cf4 100644 --- a/DisCatSharp/Net/Abstractions/Rest/RestApplicationCommandPayloads.cs +++ b/DisCatSharp/Net/Abstractions/Rest/RestApplicationCommandPayloads.cs @@ -1,227 +1,251 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System.Collections.Generic; using DisCatSharp.Entities; using DisCatSharp.Enums; using Newtonsoft.Json; namespace DisCatSharp.Net.Abstractions { /// /// Represents a application command create payload. /// internal class RestApplicationCommandCreatePayload { /// /// Gets the type. /// [JsonProperty("type")] public ApplicationCommandType Type { get; set; } /// /// Gets the name. /// [JsonProperty("name")] public string Name { get; set; } [JsonProperty("name_localizations", NullValueHandling = NullValueHandling.Ignore)] public Optional> NameLocalizations { get; set; } /// /// Gets the description. /// [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] public string Description { get; set; } [JsonProperty("description_localizations", NullValueHandling = NullValueHandling.Ignore)] public Optional> DescriptionLocalizations { get; set; } /// /// Gets the options. /// [JsonProperty("options", NullValueHandling = NullValueHandling.Ignore)] public IEnumerable Options { get; set; } /// /// Whether the command is allowed for everyone. /// [JsonProperty("default_permission")] public bool DefaultPermission { get; set; } + + /// + /// The command needed permissions. + /// + [JsonProperty("default_member_permissions", NullValueHandling = NullValueHandling.Ignore)] + public Permissions? DefaultMemberPermission { get; set; } + + /// + /// Whether the command is allowed for dms. + /// + [JsonProperty("dm_permission", NullValueHandling = NullValueHandling.Ignore)] + public bool? DmPermission { get; set; } } /// /// Represents a application command edit payload. /// internal class RestApplicationCommandEditPayload { /// /// Gets the name. /// [JsonProperty("name")] public Optional Name { get; set; } [JsonProperty("name_localizations")] public Optional> NameLocalizations { get; set; } /// /// Gets the description. /// [JsonProperty("description")] public Optional Description { get; set; } [JsonProperty("description_localizations")] public Optional> DescriptionLocalizations { get; set; } /// /// Gets the options. /// [JsonProperty("options")] public Optional> Options { get; set; } /// /// Gets the default permission. /// [JsonProperty("default_permission")] public Optional DefaultPermission { get; set; } + + /// + /// The command needed permissions. + /// + [JsonProperty("default_member_permissions", NullValueHandling = NullValueHandling.Ignore)] + public Optional DefaultMemberPermission { get; set; } + + /// + /// Whether the command is allowed for dms. + /// + [JsonProperty("dm_permission", NullValueHandling = NullValueHandling.Ignore)] + public Optional DmPermission { get; set; } } /// /// Represents a interaction response payload. /// internal class RestInteractionResponsePayload { /// /// Gets the type. /// [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] public InteractionResponseType Type { get; set; } /// /// Gets the data. /// [JsonProperty("data", NullValueHandling = NullValueHandling.Ignore)] public DiscordInteractionApplicationCommandCallbackData Data { get; set; } /// /// Gets the attachments. /// [JsonProperty("attachments", NullValueHandling = NullValueHandling.Ignore)] public List Attachments { get; set; } } /// /// Represents a interaction response payload. /// internal class RestInteractionModalResponsePayload { /// /// Gets the type. /// [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] public InteractionResponseType Type { get; set; } /// /// Gets the data. /// [JsonProperty("data", NullValueHandling = NullValueHandling.Ignore)] public DiscordInteractionApplicationCommandModalCallbackData Data { get; set; } } /// /// Represents a followup message create payload. /// internal class RestFollowupMessageCreatePayload { /// /// Gets the content. /// [JsonProperty("content", NullValueHandling = NullValueHandling.Ignore)] public string Content { get; set; } /// /// Get whether the message is tts. /// [JsonProperty("tts", NullValueHandling = NullValueHandling.Ignore)] public bool? IsTTS { get; set; } /// /// Gets the embeds. /// [JsonProperty("embeds", NullValueHandling = NullValueHandling.Ignore)] public IEnumerable Embeds { get; set; } /// /// Gets the mentions. /// [JsonProperty("allowed_mentions", NullValueHandling = NullValueHandling.Ignore)] public DiscordMentions Mentions { get; set; } /// /// Gets the flags. /// [JsonProperty("flags", NullValueHandling = NullValueHandling.Ignore)] public int? Flags { get; set; } /// /// Gets the components. /// [JsonProperty("components", NullValueHandling = NullValueHandling.Ignore)] public IReadOnlyCollection Components { get; set; } /// /// Gets attachments. /// [JsonProperty("attachments", NullValueHandling = NullValueHandling.Ignore)] public List Attachments { get; set; } } /// /// Represents a application command permission edit payload. /// internal class RestApplicationCommandPermissionEditPayload { /// /// Gets the permissions. /// [JsonProperty("permissions")] public IEnumerable Permissions { get; set; } } /// /// Represents a guild application command permission edit payload. /// internal class RestGuildApplicationCommandPermissionEditPayload { /// /// Gets the command id. /// [JsonProperty("id")] public ulong CommandId { get; set; } /// /// Gets the permissions. /// [JsonProperty("permissions")] public IEnumerable Permissions { get; set; } } } diff --git a/DisCatSharp/Net/Models/ApplicationCommandEditModel.cs b/DisCatSharp/Net/Models/ApplicationCommandEditModel.cs index f1a8da911..85bbbc88f 100644 --- a/DisCatSharp/Net/Models/ApplicationCommandEditModel.cs +++ b/DisCatSharp/Net/Models/ApplicationCommandEditModel.cs @@ -1,78 +1,89 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Generic; using DisCatSharp.Entities; +using Newtonsoft.Json; namespace DisCatSharp.Net.Models { /// /// Represents a application command edit model. /// public class ApplicationCommandEditModel { /// /// Sets the command's new name. /// public Optional Name { internal get => this._name; set { if (value.Value.Length > 32) throw new ArgumentException("Application command name cannot exceed 32 characters.", nameof(value)); this._name = value; } } private Optional _name; /// /// Sets the command's new description /// public Optional Description { internal get => this._description; set { if (value.Value.Length > 100) throw new ArgumentException("Application command description cannot exceed 100 characters.", nameof(value)); this._description = value; } } private Optional _description; public Optional NameLocalizations { internal get; set; } public Optional DescriptionLocalizations { internal get; set; } /// /// Sets the command's new options. /// public Optional> Options { internal get; set; } /// /// Sets the command's default permission. /// public Optional DefaultPermission { internal get; set; } + + /// + /// Sets the command's needed permissions. + /// + public Optional Permission { internal get; set; } + + /// + /// Sets whether the command can be used in direct messages. + /// + public Optional DmPermission { internal get; set; } } } diff --git a/DisCatSharp/Net/Rest/DiscordApiClient.cs b/DisCatSharp/Net/Rest/DiscordApiClient.cs index 28c76926a..177a3070f 100644 --- a/DisCatSharp/Net/Rest/DiscordApiClient.cs +++ b/DisCatSharp/Net/Rest/DiscordApiClient.cs @@ -1,5406 +1,5409 @@ // This file is part of the DisCatSharp project. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Globalization; using System.Linq; using System.Net; using System.Text; using System.Threading.Tasks; using DisCatSharp.Entities; using DisCatSharp.Net.Abstractions; using DisCatSharp.Net.Serialization; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace DisCatSharp.Net { /// /// Represents a discord api client. /// public sealed class DiscordApiClient { /// /// The audit log reason header name. /// private const string REASON_HEADER_NAME = "X-Audit-Log-Reason"; /// /// Gets the discord client. /// internal BaseDiscordClient Discord { get; } /// /// Gets the rest client. /// internal RestClient Rest { get; } /// /// Initializes a new instance of the class. /// /// The client. internal DiscordApiClient(BaseDiscordClient client) { this.Discord = client; this.Rest = new RestClient(client); } /// /// Initializes a new instance of the class. /// /// The proxy. /// The timeout. /// If true, use relative rate limit. /// The logger. internal DiscordApiClient(IWebProxy proxy, TimeSpan timeout, bool useRelativeRateLimit, ILogger logger) // This is for meta-clients, such as the webhook client { this.Rest = new RestClient(proxy, timeout, useRelativeRateLimit, logger); } /// /// Builds the query string. /// /// The values. /// If true, post. /// A string. private static string BuildQueryString(IDictionary values, bool post = false) { if (values == null || values.Count == 0) return string.Empty; var vals_collection = values.Select(xkvp => $"{WebUtility.UrlEncode(xkvp.Key)}={WebUtility.UrlEncode(xkvp.Value)}"); var vals = string.Join("&", vals_collection); return !post ? $"?{vals}" : vals; } /// /// Prepares the message. /// /// The msg_raw. /// A DiscordMessage. private DiscordMessage PrepareMessage(JToken msg_raw) { var author = msg_raw["author"].ToObject(); var ret = msg_raw.ToDiscordObject(); ret.Discord = this.Discord; this.PopulateMessage(author, ret); var referencedMsg = msg_raw["referenced_message"]; if (ret.MessageType == MessageType.Reply && !string.IsNullOrWhiteSpace(referencedMsg?.ToString())) { author = referencedMsg["author"].ToObject(); ret.ReferencedMessage.Discord = this.Discord; this.PopulateMessage(author, ret.ReferencedMessage); } if (ret.Channel != null) return ret; var channel = !ret.GuildId.HasValue ? new DiscordDmChannel { Id = ret.ChannelId, Discord = this.Discord, Type = ChannelType.Private } : new DiscordChannel { Id = ret.ChannelId, GuildId = ret.GuildId, Discord = this.Discord }; ret.Channel = channel; return ret; } /// /// Populates the message. /// /// The author. /// The ret. private void PopulateMessage(TransportUser author, DiscordMessage ret) { var guild = ret.Channel?.Guild; //If this is a webhook, it shouldn't be in the user cache. if (author.IsBot && int.Parse(author.Discriminator) == 0) { ret.Author = new DiscordUser(author) { Discord = this.Discord }; } else { if (!this.Discord.UserCache.TryGetValue(author.Id, out var usr)) { this.Discord.UserCache[author.Id] = usr = new DiscordUser(author) { Discord = this.Discord }; } if (guild != null) { if (!guild.Members.TryGetValue(author.Id, out var mbr)) mbr = new DiscordMember(usr) { Discord = this.Discord, _guild_id = guild.Id }; ret.Author = mbr; } else { ret.Author = usr; } } ret.PopulateMentions(); if (ret._reactions == null) ret._reactions = new List(); foreach (var xr in ret._reactions) xr.Emoji.Discord = this.Discord; } /// /// Executes a rest request. /// /// The client. /// The bucket. /// The url. /// The method. /// The route. /// The headers. /// The payload. /// The ratelimit wait override. internal Task DoRequestAsync(BaseDiscordClient client, RateLimitBucket bucket, Uri url, RestRequestMethod method, string route, IReadOnlyDictionary headers = null, string payload = null, double? ratelimitWaitOverride = null) { var req = new RestRequest(client, bucket, url, method, route, headers, payload, ratelimitWaitOverride); if (this.Discord != null) this.Rest.ExecuteRequestAsync(req).LogTaskFault(this.Discord.Logger, LogLevel.Error, LoggerEvents.RestError, "Error while executing request"); else _ = this.Rest.ExecuteRequestAsync(req); return req.WaitForCompletionAsync(); } /// /// Executes a multipart rest request for stickers. /// /// The client. /// The bucket. /// The url. /// The method. /// The route. /// The headers. /// The file. /// The sticker name. /// The sticker tag. /// The sticker description. /// The ratelimit wait override. private Task DoStickerMultipartAsync(BaseDiscordClient client, RateLimitBucket bucket, Uri url, RestRequestMethod method, string route, IReadOnlyDictionary headers = null, DiscordMessageFile file = null, string name = "", string tags = "", string description = "", double? ratelimitWaitOverride = null) { var req = new MultipartStickerWebRequest(client, bucket, url, method, route, headers, file, name, tags, description, ratelimitWaitOverride); if (this.Discord != null) this.Rest.ExecuteRequestAsync(req).LogTaskFault(this.Discord.Logger, LogLevel.Error, LoggerEvents.RestError, "Error while executing request"); else _ = this.Rest.ExecuteRequestAsync(req); return req.WaitForCompletionAsync(); } /// /// Executes a multipart request. /// /// The client. /// The bucket. /// The url. /// The method. /// The route. /// The headers. /// The values. /// The files. /// The ratelimit wait override. private Task DoMultipartAsync(BaseDiscordClient client, RateLimitBucket bucket, Uri url, RestRequestMethod method, string route, IReadOnlyDictionary headers = null, IReadOnlyDictionary values = null, IReadOnlyCollection files = null, double? ratelimitWaitOverride = null) { var req = new MultipartWebRequest(client, bucket, url, method, route, headers, values, files, ratelimitWaitOverride); if (this.Discord != null) this.Rest.ExecuteRequestAsync(req).LogTaskFault(this.Discord.Logger, LogLevel.Error, LoggerEvents.RestError, "Error while executing request"); else _ = this.Rest.ExecuteRequestAsync(req); return req.WaitForCompletionAsync(); } #region Guild /// /// Searches the members async. /// /// The guild_id. /// The name. /// The limit. internal async Task> SearchMembersAsync(ulong guild_id, string name, int? limit) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBERS}{Endpoints.SEARCH}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); var querydict = new Dictionary { ["query"] = name, ["limit"] = limit.ToString() }; var url = Utilities.GetApiUriFor(path, BuildQueryString(querydict), this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var json = JArray.Parse(res.Response); var tms = json.ToObject>(); var mbrs = new List(); foreach (var xtm in tms) { var usr = new DiscordUser(xtm.User) { Discord = this.Discord }; this.Discord.UserCache.AddOrUpdate(xtm.User.Id, usr, (id, old) => { old.Username = usr.Username; old.Discord = usr.Discord; old.AvatarHash = usr.AvatarHash; return old; }); mbrs.Add(new DiscordMember(xtm) { Discord = this.Discord, _guild_id = guild_id }); } return mbrs; } /// /// Gets the guild ban async. /// /// The guild_id. /// The user_id. internal async Task GetGuildBanAsync(ulong guild_id, ulong user_id) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.BANS}/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {guild_id, user_id}, out var path); var uri = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, uri, RestRequestMethod.GET, route).ConfigureAwait(false); var json = JObject.Parse(res.Response); var ban = json.ToObject(); return ban; } /// /// Creates the guild async. /// /// The name. /// The region_id. /// The iconb64. /// The verification_level. /// The default_message_notifications. /// The system_channel_flags. internal async Task CreateGuildAsync(string name, string region_id, Optional iconb64, VerificationLevel? verification_level, DefaultMessageNotifications? default_message_notifications, SystemChannelFlags? system_channel_flags) { var pld = new RestGuildCreatePayload { Name = name, RegionId = region_id, DefaultMessageNotifications = default_message_notifications, VerificationLevel = verification_level, IconBase64 = iconb64, SystemChannelFlags = system_channel_flags }; var route = $"{Endpoints.GUILDS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var json = JObject.Parse(res.Response); var raw_members = (JArray)json["members"]; var guild = json.ToDiscordObject(); if (this.Discord is DiscordClient dc) await dc.OnGuildCreateEventAsync(guild, raw_members, null).ConfigureAwait(false); return guild; } /// /// Creates the guild from template async. /// /// The template_code. /// The name. /// The iconb64. internal async Task CreateGuildFromTemplateAsync(string template_code, string name, Optional iconb64) { var pld = new RestGuildCreateFromTemplatePayload { Name = name, IconBase64 = iconb64 }; var route = $"{Endpoints.GUILDS}{Endpoints.TEMPLATES}/:template_code"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { template_code }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var json = JObject.Parse(res.Response); var raw_members = (JArray)json["members"]; var guild = json.ToDiscordObject(); if (this.Discord is DiscordClient dc) await dc.OnGuildCreateEventAsync(guild, raw_members, null).ConfigureAwait(false); return guild; } /// /// Deletes the guild async. /// /// The guild_id. internal async Task DeleteGuildAsync(ulong guild_id) { var route = $"{Endpoints.GUILDS}/:guild_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { guild_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route).ConfigureAwait(false); if (this.Discord is DiscordClient dc) { var gld = dc._guilds[guild_id]; await dc.OnGuildDeleteEventAsync(gld).ConfigureAwait(false); } } /// /// Modifies the guild. /// /// The guild id. /// The name. /// The verification level. /// The default message notifications. /// The mfa level. /// The explicit content filter. /// The afk channel id. /// The afk timeout. /// The iconb64. /// The owner id. /// The splashb64. /// The system channel id. /// The system channel flags. /// The public updates channel id. /// The rules channel id. /// The description. /// The banner base64. /// The discovery base64. /// The preferred locale. /// Whether the premium progress bar should be enabled. /// The reason. internal async Task ModifyGuildAsync(ulong guildId, Optional name, Optional verificationLevel, Optional defaultMessageNotifications, Optional mfaLevel, Optional explicitContentFilter, Optional afkChannelId, Optional afkTimeout, Optional iconb64, Optional ownerId, Optional splashb64, Optional systemChannelId, Optional systemChannelFlags, Optional publicUpdatesChannelId, Optional rulesChannelId, Optional description, Optional bannerb64, Optional discorverySplashb64, Optional preferredLocale, Optional premiumProgressBarEnabled, string reason) { var pld = new RestGuildModifyPayload { Name = name, VerificationLevel = verificationLevel, DefaultMessageNotifications = defaultMessageNotifications, MfaLevel = mfaLevel, ExplicitContentFilter = explicitContentFilter, AfkChannelId = afkChannelId, AfkTimeout = afkTimeout, IconBase64 = iconb64, SplashBase64 = splashb64, BannerBase64 = bannerb64, DiscoverySplashBase64 = discorverySplashb64, OwnerId = ownerId, SystemChannelId = systemChannelId, SystemChannelFlags = systemChannelFlags, RulesChannelId = rulesChannelId, PublicUpdatesChannelId = publicUpdatesChannelId, PreferredLocale = preferredLocale, Description = description, PremiumProgressBarEnabled = premiumProgressBarEnabled }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var json = JObject.Parse(res.Response); var rawMembers = (JArray)json["members"]; var guild = json.ToDiscordObject(); foreach (var r in guild._roles.Values) r._guild_id = guild.Id; if (this.Discord is DiscordClient dc) await dc.OnGuildUpdateEventAsync(guild, rawMembers).ConfigureAwait(false); return guild; } /// /// Modifies the guild community settings. /// /// The guild id. /// The guild features. /// The rules channel id. /// The public updates channel id. /// The preferred locale. /// The description. /// The default message notifications. /// The explicit content filter. /// The verification level. /// The reason. internal async Task ModifyGuildCommunitySettingsAsync(ulong guildId, List features, Optional rulesChannelId, Optional publicUpdatesChannelId, string preferredLocale, string description, DefaultMessageNotifications defaultMessageNotifications, ExplicitContentFilter explicitContentFilter, VerificationLevel verificationLevel, string reason) { var pld = new RestGuildCommunityModifyPayload { VerificationLevel = verificationLevel, DefaultMessageNotifications = defaultMessageNotifications, ExplicitContentFilter = explicitContentFilter, RulesChannelId = rulesChannelId, PublicUpdatesChannelId = publicUpdatesChannelId, PreferredLocale = preferredLocale, Description = description ?? Optional.FromNoValue(), Features = features }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var json = JObject.Parse(res.Response); var rawMembers = (JArray)json["members"]; var guild = json.ToDiscordObject(); foreach (var r in guild._roles.Values) r._guild_id = guild.Id; if (this.Discord is DiscordClient dc) await dc.OnGuildUpdateEventAsync(guild, rawMembers).ConfigureAwait(false); return guild; } /// /// Gets the guild bans async. /// /// The guild_id. /// A Task. internal async Task> GetGuildBansAsync(ulong guild_id) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.BANS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var bans_raw = JsonConvert.DeserializeObject>(res.Response).Select(xb => { if (!this.Discord.TryGetCachedUserInternal(xb.RawUser.Id, out var usr)) { usr = new DiscordUser(xb.RawUser) { Discord = this.Discord }; usr = this.Discord.UserCache.AddOrUpdate(usr.Id, usr, (id, old) => { old.Username = usr.Username; old.Discriminator = usr.Discriminator; old.AvatarHash = usr.AvatarHash; return old; }); } xb.User = usr; return xb; }); var bans = new ReadOnlyCollection(new List(bans_raw)); return bans; } /// /// Creates the guild ban async. /// /// The guild_id. /// The user_id. /// The delete_message_days. /// The reason. internal Task CreateGuildBanAsync(ulong guild_id, ulong user_id, int delete_message_days, string reason) { if (delete_message_days < 0 || delete_message_days > 7) throw new ArgumentException("Delete message days must be a number between 0 and 7.", nameof(delete_message_days)); var urlparams = new Dictionary { ["delete_message_days"] = delete_message_days.ToString(CultureInfo.InvariantCulture) }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.BANS}/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new { guild_id, user_id }, out var path); var url = Utilities.GetApiUriFor(path, BuildQueryString(urlparams), this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route, headers); } /// /// Removes the guild ban async. /// /// The guild_id. /// The user_id. /// The reason. /// A Task. internal Task RemoveGuildBanAsync(ulong guild_id, ulong user_id, string reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.BANS}/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { guild_id, user_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers); } /// /// Leaves the guild async. /// /// The guild_id. /// A Task. internal Task LeaveGuildAsync(ulong guild_id) { var route = $"{Endpoints.USERS}{Endpoints.ME}{Endpoints.GUILDS}/:guild_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { guild_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route); } /// /// Adds the guild member async. /// /// The guild_id. /// The user_id. /// The access_token. /// The nick. /// The roles. /// If true, muted. /// If true, deafened. /// A Task. internal async Task AddGuildMemberAsync(ulong guild_id, ulong user_id, string access_token, string nick, IEnumerable roles, bool muted, bool deafened) { var pld = new RestGuildMemberAddPayload { AccessToken = access_token, Nickname = nick ?? "", Roles = roles ?? new List(), Deaf = deafened, Mute = muted }; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBERS}/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new { guild_id, user_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var tm = JsonConvert.DeserializeObject(res.Response); return new DiscordMember(tm) { Discord = this.Discord, _guild_id = guild_id }; } /// /// Lists the guild members async. /// /// The guild_id. /// The limit. /// The after. /// A Task. internal async Task> ListGuildMembersAsync(ulong guild_id, int? limit, ulong? after) { var urlparams = new Dictionary(); if (limit != null && limit > 0) urlparams["limit"] = limit.Value.ToString(CultureInfo.InvariantCulture); if (after != null) urlparams["after"] = after.Value.ToString(CultureInfo.InvariantCulture); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBERS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); var url = Utilities.GetApiUriFor(path, urlparams.Any() ? BuildQueryString(urlparams) : "", this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var members_raw = JsonConvert.DeserializeObject>(res.Response); return new ReadOnlyCollection(members_raw); } /// /// Adds the guild member role async. /// /// The guild_id. /// The user_id. /// The role_id. /// The reason. /// A Task. internal Task AddGuildMemberRoleAsync(ulong guild_id, ulong user_id, ulong role_id, string reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBERS}/:user_id{Endpoints.ROLES}/:role_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new { guild_id, user_id, role_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route, headers); } /// /// Removes the guild member role async. /// /// The guild_id. /// The user_id. /// The role_id. /// The reason. /// A Task. internal Task RemoveGuildMemberRoleAsync(ulong guild_id, ulong user_id, ulong role_id, string reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBERS}/:user_id{Endpoints.ROLES}/:role_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { guild_id, user_id, role_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers); } /// /// Modifies the guild channel position async. /// /// The guild_id. /// The pld. /// The reason. /// A Task. internal Task ModifyGuildChannelPositionAsync(ulong guild_id, IEnumerable pld, string reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.CHANNELS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)); } /// /// Modifies the guild channel parent async. /// /// The guild_id. /// The pld. /// The reason. /// A Task. internal Task ModifyGuildChannelParentAsync(ulong guild_id, IEnumerable pld, string reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.CHANNELS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)); } /// /// Detaches the guild channel parent async. /// /// The guild_id. /// The pld. /// The reason. /// A Task. internal Task DetachGuildChannelParentAsync(ulong guild_id, IEnumerable pld, string reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.CHANNELS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)); } /// /// Modifies the guild role position async. /// /// The guild_id. /// The pld. /// The reason. /// A Task. internal Task ModifyGuildRolePositionAsync(ulong guild_id, IEnumerable pld, string reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.ROLES}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)); } /// /// Gets the audit logs async. /// /// The guild_id. /// The limit. /// The after. /// The before. /// The responsible. /// The action_type. /// A Task. internal async Task GetAuditLogsAsync(ulong guild_id, int limit, ulong? after, ulong? before, ulong? responsible, int? action_type) { var urlparams = new Dictionary { ["limit"] = limit.ToString(CultureInfo.InvariantCulture) }; if (after != null) urlparams["after"] = after?.ToString(CultureInfo.InvariantCulture); if (before != null) urlparams["before"] = before?.ToString(CultureInfo.InvariantCulture); if (responsible != null) urlparams["user_id"] = responsible?.ToString(CultureInfo.InvariantCulture); if (action_type != null) urlparams["action_type"] = action_type?.ToString(CultureInfo.InvariantCulture); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.AUDIT_LOGS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); var url = Utilities.GetApiUriFor(path, urlparams.Any() ? BuildQueryString(urlparams) : "", this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var audit_log_data_raw = JsonConvert.DeserializeObject(res.Response); return audit_log_data_raw; } /// /// Gets the guild vanity url async. /// /// The guild_id. /// A Task. internal async Task GetGuildVanityUrlAsync(ulong guild_id) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.VANITY_URL}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var invite = JsonConvert.DeserializeObject(res.Response); return invite; } /// /// Gets the guild widget async. /// /// The guild_id. /// A Task. internal async Task GetGuildWidgetAsync(ulong guild_id) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.WIDGET_JSON}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var json = JObject.Parse(res.Response); var rawChannels = (JArray)json["channels"]; var ret = json.ToDiscordObject(); ret.Discord = this.Discord; ret.Guild = this.Discord.Guilds[guild_id]; ret.Channels = ret.Guild == null ? rawChannels.Select(r => new DiscordChannel { Id = (ulong)r["id"], Name = r["name"].ToString(), Position = (int)r["position"] }).ToList() : rawChannels.Select(r => { var c = ret.Guild.GetChannel((ulong)r["id"]); c.Position = (int)r["position"]; return c; }).ToList(); return ret; } /// /// Gets the guild widget settings async. /// /// The guild_id. /// A Task. internal async Task GetGuildWidgetSettingsAsync(ulong guild_id) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.WIDGET}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Guild = this.Discord.Guilds[guild_id]; return ret; } /// /// Modifies the guild widget settings async. /// /// The guild_id. /// If true, is enabled. /// The channel id. /// The reason. /// A Task. internal async Task ModifyGuildWidgetSettingsAsync(ulong guild_id, bool? isEnabled, ulong? channelId, string reason) { var pld = new RestGuildWidgetSettingsPayload { Enabled = isEnabled, ChannelId = channelId }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.WIDGET}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Guild = this.Discord.Guilds[guild_id]; return ret; } /// /// Gets the guild templates async. /// /// The guild_id. /// A Task. internal async Task> GetGuildTemplatesAsync(ulong guild_id) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.TEMPLATES}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var templates_raw = JsonConvert.DeserializeObject>(res.Response); return new ReadOnlyCollection(new List(templates_raw)); } /// /// Creates the guild template async. /// /// The guild_id. /// The name. /// The description. /// A Task. internal async Task CreateGuildTemplateAsync(ulong guild_id, string name, string description) { var pld = new RestGuildTemplateCreateOrModifyPayload { Name = name, Description = description }; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.TEMPLATES}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { guild_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); return ret; } /// /// Syncs the guild template async. /// /// The guild_id. /// The template_code. /// A Task. internal async Task SyncGuildTemplateAsync(ulong guild_id, string template_code) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.TEMPLATES}/:template_code"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new { guild_id, template_code }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route).ConfigureAwait(false); var template_raw = JsonConvert.DeserializeObject(res.Response); return template_raw; } /// /// Modifies the guild template async. /// /// The guild_id. /// The template_code. /// The name. /// The description. /// A Task. internal async Task ModifyGuildTemplateAsync(ulong guild_id, string template_code, string name, string description) { var pld = new RestGuildTemplateCreateOrModifyPayload { Name = name, Description = description }; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.TEMPLATES}/:template_code"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id, template_code }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var template_raw = JsonConvert.DeserializeObject(res.Response); return template_raw; } /// /// Deletes the guild template async. /// /// The guild_id. /// The template_code. /// A Task. internal async Task DeleteGuildTemplateAsync(ulong guild_id, string template_code) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.TEMPLATES}/:template_code"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { guild_id, template_code }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route).ConfigureAwait(false); var template_raw = JsonConvert.DeserializeObject(res.Response); return template_raw; } /// /// Gets the guild membership screening form async. /// /// The guild_id. /// A Task. internal async Task GetGuildMembershipScreeningFormAsync(ulong guild_id) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBER_VERIFICATION}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var screening_raw = JsonConvert.DeserializeObject(res.Response); return screening_raw; } /// /// Modifies the guild membership screening form async. /// /// The guild_id. /// The enabled. /// The fields. /// The description. /// A Task. internal async Task ModifyGuildMembershipScreeningFormAsync(ulong guild_id, Optional enabled, Optional fields, Optional description) { var pld = new RestGuildMembershipScreeningFormModifyPayload { Enabled = enabled, Description = description, Fields = fields }; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBER_VERIFICATION}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var screening_raw = JsonConvert.DeserializeObject(res.Response); return screening_raw; } /// /// Gets the guild welcome screen async. /// /// The guild_id. /// A Task. internal async Task GetGuildWelcomeScreenAsync(ulong guild_id) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.WELCOME_SCREEN}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route); var ret = JsonConvert.DeserializeObject(res.Response); return ret; } /// /// Modifies the guild welcome screen async. /// /// The guild_id. /// The enabled. /// The welcome channels. /// The description. /// A Task. internal async Task ModifyGuildWelcomeScreenAsync(ulong guild_id, Optional enabled, Optional> welcomeChannels, Optional description) { var pld = new RestGuildWelcomeScreenModifyPayload { Enabled = enabled, WelcomeChannels = welcomeChannels, Description = description }; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.WELCOME_SCREEN}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, payload: DiscordJson.SerializeObject(pld)); var ret = JsonConvert.DeserializeObject(res.Response); return ret; } /// /// Updates the current user voice state async. /// /// The guild_id. /// The channel id. /// If true, suppress. /// The request to speak timestamp. /// A Task. internal async Task UpdateCurrentUserVoiceStateAsync(ulong guild_id, ulong channelId, bool? suppress, DateTimeOffset? requestToSpeakTimestamp) { var pld = new RestGuildUpdateCurrentUserVoiceStatePayload { ChannelId = channelId, Suppress = suppress, RequestToSpeakTimestamp = requestToSpeakTimestamp }; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.VOICE_STATES}/@me"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, payload: DiscordJson.SerializeObject(pld)); } /// /// Updates the user voice state async. /// /// The guild_id. /// The user_id. /// The channel id. /// If true, suppress. /// A Task. internal async Task UpdateUserVoiceStateAsync(ulong guild_id, ulong user_id, ulong channelId, bool? suppress) { var pld = new RestGuildUpdateUserVoiceStatePayload { ChannelId = channelId, Suppress = suppress }; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.VOICE_STATES}/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id, user_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, payload: DiscordJson.SerializeObject(pld)); } #endregion #region Guild Scheduled Events /// /// Creates a scheduled event. /// internal async Task CreateGuildScheduledEventAsync(ulong guild_id, ulong? channel_id, DiscordScheduledEventEntityMetadata metadata, string name, DateTimeOffset scheduled_start_time, DateTimeOffset? scheduled_end_time, string description, ScheduledEventEntityType type, string reason = null) { var pld = new RestGuildScheduledEventCreatePayload { ChannelId = channel_id, EntityMetadata = metadata, Name = name, ScheduledStartTime = scheduled_start_time, ScheduledEndTime = scheduled_end_time, Description = description, EntityType = type }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.SCHEDULED_EVENTS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { guild_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, headers, DiscordJson.SerializeObject(pld)); var scheduled_event = JsonConvert.DeserializeObject(res.Response); var guild = this.Discord.Guilds[guild_id]; scheduled_event.Discord = this.Discord; if (scheduled_event.Creator != null) scheduled_event.Creator.Discord = this.Discord; if (this.Discord is DiscordClient dc) await dc.OnGuildScheduledEventCreateEventAsync(scheduled_event, guild); return scheduled_event; } /// /// Modifies a scheduled event. /// internal async Task ModifyGuildScheduledEventAsync(ulong guild_id, ulong scheduled_event_id, Optional channel_id, Optional metadata, Optional name, Optional scheduled_start_time, Optional scheduled_end_time, Optional description, Optional type, Optional status, string reason = null) { var pld = new RestGuildSheduledEventModifyPayload { ChannelId = channel_id, EntityMetadata = metadata, Name = name, ScheduledStartTime = scheduled_start_time, ScheduledEndTime = scheduled_end_time, Description = description, EntityType = type, Status = status }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.SCHEDULED_EVENTS}/:scheduled_event_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id, scheduled_event_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)); var scheduled_event = JsonConvert.DeserializeObject(res.Response); var guild = this.Discord.Guilds[guild_id]; scheduled_event.Discord = this.Discord; if (scheduled_event.Creator != null) { scheduled_event.Creator.Discord = this.Discord; this.Discord.UserCache.AddOrUpdate(scheduled_event.Creator.Id, scheduled_event.Creator, (id, old) => { old.Username = scheduled_event.Creator.Username; old.Discriminator = scheduled_event.Creator.Discriminator; old.AvatarHash = scheduled_event.Creator.AvatarHash; old.Flags = scheduled_event.Creator.Flags; return old; }); } if (this.Discord is DiscordClient dc) await dc.OnGuildScheduledEventUpdateEventAsync(scheduled_event, guild); return scheduled_event; } /// /// Modifies a scheduled event. /// internal async Task ModifyGuildScheduledEventStatusAsync(ulong guild_id, ulong scheduled_event_id, ScheduledEventStatus status, string reason = null) { var pld = new RestGuildSheduledEventModifyPayload { Status = status }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.SCHEDULED_EVENTS}/:scheduled_event_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id, scheduled_event_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)); var scheduled_event = JsonConvert.DeserializeObject(res.Response); var guild = this.Discord.Guilds[guild_id]; scheduled_event.Discord = this.Discord; if (scheduled_event.Creator != null) { scheduled_event.Creator.Discord = this.Discord; this.Discord.UserCache.AddOrUpdate(scheduled_event.Creator.Id, scheduled_event.Creator, (id, old) => { old.Username = scheduled_event.Creator.Username; old.Discriminator = scheduled_event.Creator.Discriminator; old.AvatarHash = scheduled_event.Creator.AvatarHash; old.Flags = scheduled_event.Creator.Flags; return old; }); } if (this.Discord is DiscordClient dc) await dc.OnGuildScheduledEventUpdateEventAsync(scheduled_event, guild); return scheduled_event; } /// /// Gets a scheduled event. /// /// The guild_id. /// The event id. /// Whether to include user count. internal async Task GetGuildScheduledEventAsync(ulong guild_id, ulong scheduled_event_id, bool? with_user_count) { var urlparams = new Dictionary(); if (with_user_count.HasValue) urlparams["with_user_count"] = with_user_count?.ToString(); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.SCHEDULED_EVENTS}/:scheduled_event_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id, scheduled_event_id }, out var path); var url = Utilities.GetApiUriFor(path, urlparams.Any() ? BuildQueryString(urlparams) : "", this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route); var scheduled_event = JsonConvert.DeserializeObject(res.Response); var guild = this.Discord.Guilds[guild_id]; scheduled_event.Discord = this.Discord; if (scheduled_event.Creator != null) { scheduled_event.Creator.Discord = this.Discord; this.Discord.UserCache.AddOrUpdate(scheduled_event.Creator.Id, scheduled_event.Creator, (id, old) => { old.Username = scheduled_event.Creator.Username; old.Discriminator = scheduled_event.Creator.Discriminator; old.AvatarHash = scheduled_event.Creator.AvatarHash; old.Flags = scheduled_event.Creator.Flags; return old; }); } return scheduled_event; } /// /// Gets the guilds scheduled events. /// /// The guild_id. /// Whether to include the count of users subscribed to the scheduled event. internal async Task> ListGuildScheduledEventsAsync(ulong guild_id, bool? with_user_count) { var urlparams = new Dictionary(); if (with_user_count.HasValue) urlparams["with_user_count"] = with_user_count?.ToString(); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.SCHEDULED_EVENTS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); var url = Utilities.GetApiUriFor(path, urlparams.Any() ? BuildQueryString(urlparams) : "", this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route); var events = new Dictionary(); var events_raw = JsonConvert.DeserializeObject>(res.Response); var guild = this.Discord.Guilds[guild_id]; foreach (var ev in events_raw) { ev.Discord = this.Discord; if(ev.Creator != null) { ev.Creator.Discord = this.Discord; this.Discord.UserCache.AddOrUpdate(ev.Creator.Id, ev.Creator, (id, old) => { old.Username = ev.Creator.Username; old.Discriminator = ev.Creator.Discriminator; old.AvatarHash = ev.Creator.AvatarHash; old.Flags = ev.Creator.Flags; return old; }); } events.Add(ev.Id, ev); } return new ReadOnlyDictionary(new Dictionary(events)); } /// /// Deletes a guild sheduled event. /// /// The guild_id. /// The sheduled event id. /// The reason. internal Task DeleteGuildScheduledEventAsync(ulong guild_id, ulong scheduled_event_id, string reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.SCHEDULED_EVENTS}/:scheduled_event_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { guild_id, scheduled_event_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers); } /// /// Gets the users who RSVP'd to a sheduled event. /// Optional with member objects. /// This endpoint is paginated. /// /// The guild_id. /// The sheduled event id. /// The limit how many users to receive from the event. /// Get results before the given id. /// Get results after the given id. /// Wether to include guild member data. attaches guild_member property to the user object. internal async Task> GetGuildScheduledEventRSPVUsersAsync(ulong guild_id, ulong scheduled_event_id, int? limit, ulong? before, ulong? after, bool? with_member) { var urlparams = new Dictionary(); if (limit != null && limit > 0) urlparams["limit"] = limit.Value.ToString(CultureInfo.InvariantCulture); if (before != null) urlparams["before"] = before.Value.ToString(CultureInfo.InvariantCulture); if (after != null) urlparams["after"] = after.Value.ToString(CultureInfo.InvariantCulture); if (with_member != null) urlparams["with_member"] = with_member.Value.ToString(CultureInfo.InvariantCulture); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.SCHEDULED_EVENTS}/:scheduled_event_id{Endpoints.USERS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id, scheduled_event_id }, out var path); var url = Utilities.GetApiUriFor(path, urlparams.Any() ? BuildQueryString(urlparams) : "", this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var rspv_users = JsonConvert.DeserializeObject>(res.Response); Dictionary rspv = new(); foreach (var rspv_user in rspv_users) { rspv_user.Discord = this.Discord; rspv_user.GuildId = guild_id; rspv_user.User.Discord = this.Discord; rspv_user.User = this.Discord.UserCache.AddOrUpdate(rspv_user.User.Id, rspv_user.User, (id, old) => { old.Username = rspv_user.User.Username; old.Discriminator = rspv_user.User.Discriminator; old.AvatarHash = rspv_user.User.AvatarHash; old.BannerHash = rspv_user.User.BannerHash; old._bannerColor = rspv_user.User._bannerColor; return old; }); /*if (with_member.HasValue && with_member.Value && rspv_user.Member != null) { rspv_user.Member.Discord = this.Discord; }*/ rspv.Add(rspv_user.User.Id, rspv_user); } return new ReadOnlyDictionary(new Dictionary(rspv)); } #endregion #region Channel /// /// Creates the guild channel async. /// /// The guild_id. /// The name. /// The type. /// The parent. /// The topic. /// The bitrate. /// The user_limit. /// The overwrites. /// If true, nsfw. /// The per user rate limit. /// The quality mode. /// The reason. /// A Task. internal async Task CreateGuildChannelAsync(ulong guild_id, string name, ChannelType type, ulong? parent, Optional topic, int? bitrate, int? user_limit, IEnumerable overwrites, bool? nsfw, Optional perUserRateLimit, VideoQualityMode? qualityMode, string reason) { var restoverwrites = new List(); if (overwrites != null) foreach (var ow in overwrites) restoverwrites.Add(ow.Build()); var pld = new RestChannelCreatePayload { Name = name, Type = type, Parent = parent, Topic = topic, Bitrate = bitrate, UserLimit = user_limit, PermissionOverwrites = restoverwrites, Nsfw = nsfw, PerUserRateLimit = perUserRateLimit, QualityMode = qualityMode }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.CHANNELS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { guild_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; foreach (var xo in ret._permissionOverwrites) { xo.Discord = this.Discord; xo._channel_id = ret.Id; } return ret; } /// /// Modifies the channel async. /// /// The channel_id. /// The name. /// The position. /// The topic. /// If true, nsfw. /// The parent. /// The bitrate. /// The user_limit. /// The per user rate limit. /// The rtc region. /// The quality mode. /// The default auto archive duration. /// The type. /// The permission overwrites. /// The banner. /// The reason. internal Task ModifyChannelAsync(ulong channel_id, string name, int? position, Optional topic, bool? nsfw, Optional parent, int? bitrate, int? user_limit, Optional perUserRateLimit, Optional rtcRegion, VideoQualityMode? qualityMode, ThreadAutoArchiveDuration? autoArchiveDuration, Optional type, IEnumerable permissionOverwrites, Optional bannerb64, string reason) { List restoverwrites = null; if (permissionOverwrites != null) { restoverwrites = new List(); foreach (var ow in permissionOverwrites) restoverwrites.Add(ow.Build()); } var pld = new RestChannelModifyPayload { Name = name, Position = position, Topic = topic, Nsfw = nsfw, Parent = parent, Bitrate = bitrate, UserLimit = user_limit, PerUserRateLimit = perUserRateLimit, RtcRegion = rtcRegion, QualityMode = qualityMode, DefaultAutoArchiveDuration = autoArchiveDuration, Type = type, PermissionOverwrites = restoverwrites, BannerBase64 = bannerb64 }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.CHANNELS}/:channel_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { channel_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)); } /// /// Gets the channel async. /// /// The channel_id. /// A Task. internal async Task GetChannelAsync(ulong channel_id) { var route = $"{Endpoints.CHANNELS}/:channel_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { channel_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; foreach (var xo in ret._permissionOverwrites) { xo.Discord = this.Discord; xo._channel_id = ret.Id; } return ret; } /// /// Deletes the channel async. /// /// The channel_id. /// The reason. /// A Task. internal Task DeleteChannelAsync(ulong channel_id, string reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.CHANNELS}/:channel_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { channel_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers); } /// /// Gets the message async. /// /// The channel_id. /// The message_id. /// A Task. internal async Task GetMessageAsync(ulong channel_id, ulong message_id) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}/:message_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { channel_id, message_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var ret = this.PrepareMessage(JObject.Parse(res.Response)); return ret; } /// /// Creates the message async. /// /// The channel_id. /// The content. /// The embeds. /// The sticker. /// The reply message id. /// If true, mention reply. /// If true, fail on invalid reply. /// A Task. internal async Task CreateMessageAsync(ulong channel_id, string content, IEnumerable embeds, DiscordSticker sticker, ulong? replyMessageId, bool mentionReply, bool failOnInvalidReply) { if (content != null && content.Length > 2000) throw new ArgumentException("Message content length cannot exceed 2000 characters."); if (!embeds?.Any() ?? true) { if (content == null && sticker == null) throw new ArgumentException("You must specify message content, a sticker or an embed."); if (content.Length == 0) throw new ArgumentException("Message content must not be empty."); } if (embeds != null) foreach (var embed in embeds) if (embed.Timestamp != null) embed.Timestamp = embed.Timestamp.Value.ToUniversalTime(); var pld = new RestChannelMessageCreatePayload { HasContent = content != null, Content = content, StickersIds = sticker is null ? Array.Empty() : new[] {sticker.Id}, IsTTS = false, HasEmbed = embeds?.Any() ?? false, Embeds = embeds }; if (replyMessageId != null) pld.MessageReference = new InternalDiscordMessageReference { MessageId = replyMessageId, FailIfNotExists = failOnInvalidReply }; if (replyMessageId != null) pld.Mentions = new DiscordMentions(Mentions.All, true, mentionReply); var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { channel_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = this.PrepareMessage(JObject.Parse(res.Response)); return ret; } /// /// Creates the message async. /// /// The channel_id. /// The builder. /// A Task. internal async Task CreateMessageAsync(ulong channel_id, DiscordMessageBuilder builder) { builder.Validate(); if (builder.Embeds != null) foreach (var embed in builder.Embeds) if (embed?.Timestamp != null) embed.Timestamp = embed.Timestamp.Value.ToUniversalTime(); var pld = new RestChannelMessageCreatePayload { HasContent = builder.Content != null, Content = builder.Content, StickersIds = builder.Sticker is null ? Array.Empty() : new[] {builder.Sticker.Id}, IsTTS = builder.IsTTS, HasEmbed = builder.Embeds != null, Embeds = builder.Embeds, Components = builder.Components }; if (builder.ReplyId != null) pld.MessageReference = new InternalDiscordMessageReference { MessageId = builder.ReplyId, FailIfNotExists = builder.FailOnInvalidReply }; pld.Mentions = new DiscordMentions(builder.Mentions ?? Mentions.All, builder.Mentions?.Any() ?? false, builder.MentionOnReply); if (builder.Files.Count == 0) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { channel_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = this.PrepareMessage(JObject.Parse(res.Response)); return ret; } else { ulong file_id = 0; List attachments = new(builder.Files.Count); foreach(var file in builder.Files) { DiscordAttachment att = new() { Id = file_id, Discord = this.Discord, Description = file.Description, FileName = file.FileName }; attachments.Add(att); file_id++; } pld.Attachments = attachments; var values = new Dictionary { ["payload_json"] = DiscordJson.SerializeObject(pld) }; var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { channel_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoMultipartAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, values: values, files: builder.Files).ConfigureAwait(false); var ret = this.PrepareMessage(JObject.Parse(res.Response)); foreach (var file in builder._files.Where(x => x.ResetPositionTo.HasValue)) { file.Stream.Position = file.ResetPositionTo.Value; } return ret; } } /// /// Gets the guild channels async. /// /// The guild_id. /// A Task. internal async Task> GetGuildChannelsAsync(ulong guild_id) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.CHANNELS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var channels_raw = JsonConvert.DeserializeObject>(res.Response).Select(xc => { xc.Discord = this.Discord; return xc; }); foreach (var ret in channels_raw) foreach (var xo in ret._permissionOverwrites) { xo.Discord = this.Discord; xo._channel_id = ret.Id; } return new ReadOnlyCollection(new List(channels_raw)); } /// /// Creates the stage instance async. /// /// The channel_id. /// The topic. /// Whether everyone should be notified about the stage. /// The privacy_level. /// The reason. internal async Task CreateStageInstanceAsync(ulong channel_id, string topic, bool send_start_notification, StagePrivacyLevel privacy_level, string reason) { var pld = new RestStageInstanceCreatePayload { ChannelId = channel_id, Topic = topic, PrivacyLevel = privacy_level, SendStartNotification = send_start_notification }; var route = $"{Endpoints.STAGE_INSTANCES}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { }, out var path); var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var stageInstance = JsonConvert.DeserializeObject(res.Response); return stageInstance; } /// /// Gets the stage instance async. /// /// The channel_id. internal async Task GetStageInstanceAsync(ulong channel_id) { var route = $"{Endpoints.STAGE_INSTANCES}/:channel_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { channel_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var stageInstance = JsonConvert.DeserializeObject(res.Response); return stageInstance; } /// /// Modifies the stage instance async. /// /// The channel_id. /// The topic. /// The privacy_level. /// The reason. internal Task ModifyStageInstanceAsync(ulong channel_id, Optional topic, Optional privacy_level, string reason) { var pld = new RestStageInstanceModifyPayload { Topic = topic, PrivacyLevel = privacy_level }; var route = $"{Endpoints.STAGE_INSTANCES}/:channel_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { channel_id }, out var path); var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)); } /// /// Deletes the stage instance async. /// /// The channel_id. /// The reason. internal Task DeleteStageInstanceAsync(ulong channel_id, string reason) { var route = $"{Endpoints.STAGE_INSTANCES}/:channel_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { channel_id }, out var path); var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers); } /// /// Gets the channel messages async. /// /// The channel id. /// The limit. /// The before. /// The after. /// The around. /// A Task. internal async Task> GetChannelMessagesAsync(ulong channel_id, int limit, ulong? before, ulong? after, ulong? around) { var urlparams = new Dictionary(); if (around != null) urlparams["around"] = around?.ToString(CultureInfo.InvariantCulture); if (before != null) urlparams["before"] = before?.ToString(CultureInfo.InvariantCulture); if (after != null) urlparams["after"] = after?.ToString(CultureInfo.InvariantCulture); if (limit > 0) urlparams["limit"] = limit.ToString(CultureInfo.InvariantCulture); var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { channel_id }, out var path); var url = Utilities.GetApiUriFor(path, urlparams.Any() ? BuildQueryString(urlparams) : "", this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var msgs_raw = JArray.Parse(res.Response); var msgs = new List(); foreach (var xj in msgs_raw) msgs.Add(this.PrepareMessage(xj)); return new ReadOnlyCollection(new List(msgs)); } /// /// Gets the channel message async. /// /// The channel_id. /// The message_id. /// A Task. internal async Task GetChannelMessageAsync(ulong channel_id, ulong message_id) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}/:message_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { channel_id, message_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var ret = this.PrepareMessage(JObject.Parse(res.Response)); return ret; } /// /// Edits the message async. /// /// The channel_id. /// The message_id. /// The content. /// The embeds. /// The mentions. /// The components. /// The suppress_embed. /// The files. /// The attachments to keep. /// A Task. internal async Task EditMessageAsync(ulong channel_id, ulong message_id, Optional content, Optional> embeds, IEnumerable mentions, IReadOnlyList components, Optional suppress_embed, IReadOnlyCollection files, Optional> attachments) { if (embeds.HasValue && embeds.Value != null) foreach (var embed in embeds.Value) if (embed.Timestamp != null) embed.Timestamp = embed.Timestamp.Value.ToUniversalTime(); var pld = new RestChannelMessageEditPayload { HasContent = content.HasValue, Content = content.HasValue ? (string)content : null, HasEmbed = embeds.HasValue && (embeds.Value?.Any() ?? false), Embeds = embeds.HasValue && (embeds.Value?.Any() ?? false) ? embeds.Value : null, Components = components ?? null, Flags = suppress_embed.HasValue ? (bool)suppress_embed ? MessageFlags.SuppressedEmbeds : null : null }; pld.Mentions = new DiscordMentions(mentions ?? Mentions.None, false, mentions?.OfType().Any() ?? false); if (files?.Count > 0) { ulong file_id = 0; List attachments_new = new(); foreach (var file in files) { DiscordAttachment att = new() { Id = file_id, Discord = this.Discord, Description = file.Description, FileName = file.FileName }; attachments_new.Add(att); file_id++; } if (attachments.HasValue && attachments.Value.Any()) attachments_new.AddRange(attachments.Value); pld.Attachments = attachments_new; var values = new Dictionary { ["payload_json"] = DiscordJson.SerializeObject(pld) }; var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}/:message_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { channel_id, message_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoMultipartAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, values: values, files: files).ConfigureAwait(false); var ret = this.PrepareMessage(JObject.Parse(res.Response)); foreach (var file in files.Where(x => x.ResetPositionTo.HasValue)) { file.Stream.Position = file.ResetPositionTo.Value; } return ret; } else { pld.Attachments = attachments.HasValue ? attachments.Value : null; var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}/:message_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { channel_id, message_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = this.PrepareMessage(JObject.Parse(res.Response)); return ret; } } /// /// Deletes the message async. /// /// The channel_id. /// The message_id. /// The reason. /// A Task. internal Task DeleteMessageAsync(ulong channel_id, ulong message_id, string reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}/:message_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { channel_id, message_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers); } /// /// Deletes the messages async. /// /// The channel_id. /// The message_ids. /// The reason. /// A Task. internal Task DeleteMessagesAsync(ulong channel_id, IEnumerable message_ids, string reason) { var pld = new RestChannelMessageBulkDeletePayload { Messages = message_ids }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}{Endpoints.BULK_DELETE}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { channel_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, headers, DiscordJson.SerializeObject(pld)); } /// /// Gets the channel invites async. /// /// The channel_id. /// A Task. internal async Task> GetChannelInvitesAsync(ulong channel_id) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.INVITES}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { channel_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var invites_raw = JsonConvert.DeserializeObject>(res.Response).Select(xi => { xi.Discord = this.Discord; return xi; }); return new ReadOnlyCollection(new List(invites_raw)); } /// /// Creates the channel invite async. /// /// The channel_id. /// The max_age. /// The max_uses. /// The target_type. /// The target_application. /// The target_user. /// If true, temporary. /// If true, unique. /// The reason. /// A Task. internal async Task CreateChannelInviteAsync(ulong channel_id, int max_age, int max_uses, TargetType? target_type, TargetActivity? target_application, ulong? target_user, bool temporary, bool unique, string reason) { var pld = new RestChannelInviteCreatePayload { MaxAge = max_age, MaxUses = max_uses, TargetType = target_type, TargetApplication = target_application, TargetUserId = target_user, Temporary = temporary, Unique = unique }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.INVITES}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { channel_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; return ret; } /// /// Deletes the channel permission async. /// /// The channel_id. /// The overwrite_id. /// The reason. /// A Task. internal Task DeleteChannelPermissionAsync(ulong channel_id, ulong overwrite_id, string reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.PERMISSIONS}/:overwrite_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { channel_id, overwrite_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers); } /// /// Edits the channel permissions async. /// /// The channel_id. /// The overwrite_id. /// The allow. /// The deny. /// The type. /// The reason. /// A Task. internal Task EditChannelPermissionsAsync(ulong channel_id, ulong overwrite_id, Permissions allow, Permissions deny, string type, string reason) { var pld = new RestChannelPermissionEditPayload { Type = type, Allow = allow & PermissionMethods.FULL_PERMS, Deny = deny & PermissionMethods.FULL_PERMS }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.PERMISSIONS}/:overwrite_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new { channel_id, overwrite_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route, headers, DiscordJson.SerializeObject(pld)); } /// /// Triggers the typing async. /// /// The channel_id. /// A Task. internal Task TriggerTypingAsync(ulong channel_id) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.TYPING}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { channel_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route); } /// /// Gets the pinned messages async. /// /// The channel_id. /// A Task. internal async Task> GetPinnedMessagesAsync(ulong channel_id) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.PINS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { channel_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var msgs_raw = JArray.Parse(res.Response); var msgs = new List(); foreach (var xj in msgs_raw) msgs.Add(this.PrepareMessage(xj)); return new ReadOnlyCollection(new List(msgs)); } /// /// Pins the message async. /// /// The channel_id. /// The message_id. /// A Task. internal Task PinMessageAsync(ulong channel_id, ulong message_id) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.PINS}/:message_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new { channel_id, message_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route); } /// /// Unpins the message async. /// /// The channel_id. /// The message_id. /// A Task. internal Task UnpinMessageAsync(ulong channel_id, ulong message_id) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.PINS}/:message_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { channel_id, message_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route); } /// /// Adds the group dm recipient async. /// /// The channel_id. /// The user_id. /// The access_token. /// The nickname. /// A Task. internal Task AddGroupDmRecipientAsync(ulong channel_id, ulong user_id, string access_token, string nickname) { var pld = new RestChannelGroupDmRecipientAddPayload { AccessToken = access_token, Nickname = nickname }; var route = $"{Endpoints.USERS}{Endpoints.ME}{Endpoints.CHANNELS}/:channel_id{Endpoints.RECIPIENTS}/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new { channel_id, user_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route, payload: DiscordJson.SerializeObject(pld)); } /// /// Removes the group dm recipient async. /// /// The channel_id. /// The user_id. /// A Task. internal Task RemoveGroupDmRecipientAsync(ulong channel_id, ulong user_id) { var route = $"{Endpoints.USERS}{Endpoints.ME}{Endpoints.CHANNELS}/:channel_id{Endpoints.RECIPIENTS}/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { channel_id, user_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route); } /// /// Creates the group dm async. /// /// The access_tokens. /// The nicks. /// A Task. internal async Task CreateGroupDmAsync(IEnumerable access_tokens, IDictionary nicks) { var pld = new RestUserGroupDmCreatePayload { AccessTokens = access_tokens, Nicknames = nicks }; var route = $"{Endpoints.USERS}{Endpoints.ME}{Endpoints.CHANNELS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; return ret; } /// /// Creates the dm async. /// /// The recipient_id. /// A Task. internal async Task CreateDmAsync(ulong recipient_id) { var pld = new RestUserDmCreatePayload { Recipient = recipient_id }; var route = $"{Endpoints.USERS}{Endpoints.ME}{Endpoints.CHANNELS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; return ret; } /// /// Follows the channel async. /// /// The channel_id. /// The webhook_channel_id. /// A Task. internal async Task FollowChannelAsync(ulong channel_id, ulong webhook_channel_id) { var pld = new FollowedChannelAddPayload { WebhookChannelId = webhook_channel_id }; var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.FOLLOWERS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { channel_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var response = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); return JsonConvert.DeserializeObject(response.Response); } /// /// Crossposts the message async. /// /// The channel_id. /// The message_id. /// A Task. internal async Task CrosspostMessageAsync(ulong channel_id, ulong message_id) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}/:message_id{Endpoints.CROSSPOST}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { channel_id, message_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var response = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route).ConfigureAwait(false); return JsonConvert.DeserializeObject(response.Response); } #endregion #region Member /// /// Gets the current user async. /// /// A Task. internal Task GetCurrentUserAsync() => this.GetUserAsync("@me"); /// /// Gets the user async. /// /// The user_id. /// A Task. internal Task GetUserAsync(ulong user_id) => this.GetUserAsync(user_id.ToString(CultureInfo.InvariantCulture)); /// /// Gets the user async. /// /// The user_id. /// A Task. internal async Task GetUserAsync(string user_id) { var route = $"{Endpoints.USERS}/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { user_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var user_raw = JsonConvert.DeserializeObject(res.Response); var duser = new DiscordUser(user_raw) { Discord = this.Discord }; return duser; } /// /// Gets the guild member async. /// /// The guild_id. /// The user_id. /// A Task. internal async Task GetGuildMemberAsync(ulong guild_id, ulong user_id) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBERS}/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id, user_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var tm = JsonConvert.DeserializeObject(res.Response); var usr = new DiscordUser(tm.User) { Discord = this.Discord }; usr = this.Discord.UserCache.AddOrUpdate(tm.User.Id, usr, (id, old) => { old.Username = usr.Username; old.Discriminator = usr.Discriminator; old.AvatarHash = usr.AvatarHash; return old; }); return new DiscordMember(tm) { Discord = this.Discord, _guild_id = guild_id }; } /// /// Removes the guild member async. /// /// The guild_id. /// The user_id. /// The reason. /// A Task. internal Task RemoveGuildMemberAsync(ulong guild_id, ulong user_id, string reason) { var urlparams = new Dictionary(); if (reason != null) urlparams["reason"] = reason; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBERS}/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { guild_id, user_id }, out var path); var url = Utilities.GetApiUriFor(path, BuildQueryString(urlparams), this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route); } /// /// Modifies the current user async. /// /// The username. /// The base64_avatar. /// A Task. internal async Task ModifyCurrentUserAsync(string username, Optional base64_avatar) { var pld = new RestUserUpdateCurrentPayload { Username = username, AvatarBase64 = base64_avatar.HasValue ? base64_avatar.Value : null, AvatarSet = base64_avatar.HasValue }; var route = $"{Endpoints.USERS}{Endpoints.ME}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var user_raw = JsonConvert.DeserializeObject(res.Response); return user_raw; } /// /// Gets the current user guilds async. /// /// The limit. /// The before. /// The after. /// A Task. internal async Task> GetCurrentUserGuildsAsync(int limit = 100, ulong? before = null, ulong? after = null) { var route = $"{Endpoints.USERS}{Endpoints.ME}{Endpoints.GUILDS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { }, out var path); var url = Utilities.GetApiUriBuilderFor(path, this.Discord.Configuration) .AddParameter($"limit", limit.ToString(CultureInfo.InvariantCulture)); if (before != null) url.AddParameter("before", before.Value.ToString(CultureInfo.InvariantCulture)); if (after != null) url.AddParameter("after", after.Value.ToString(CultureInfo.InvariantCulture)); var res = await this.DoRequestAsync(this.Discord, bucket, url.Build(), RestRequestMethod.GET, route).ConfigureAwait(false); if (this.Discord is DiscordClient) { var guilds_raw = JsonConvert.DeserializeObject>(res.Response); var glds = guilds_raw.Select(xug => (this.Discord as DiscordClient)?._guilds[xug.Id]); return new ReadOnlyCollection(new List(glds)); } else { return new ReadOnlyCollection(JsonConvert.DeserializeObject>(res.Response)); } } /// /// Modifies the guild member async. /// /// The guild_id. /// The user_id. /// The nick. /// The role_ids. /// The mute. /// The deaf. /// The voice_channel_id. /// The reason. /// A Task. internal Task ModifyGuildMemberAsync(ulong guild_id, ulong user_id, Optional nick, Optional> role_ids, Optional mute, Optional deaf, Optional voice_channel_id, string reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var pld = new RestGuildMemberModifyPayload { Nickname = nick, RoleIds = role_ids, Deafen = deaf, Mute = mute, VoiceChannelId = voice_channel_id }; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBERS}/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id, user_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, payload: DiscordJson.SerializeObject(pld)); } /// /// Modifies the time out of a guild member. /// /// The guild_id. /// The user_id. /// Datetime offset. /// The reason. /// A Task. internal Task ModifyTimeoutAsync(ulong guild_id, ulong user_id, DateTimeOffset? until, string reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var pld = new RestGuildMemberTimeoutModifyPayload { CommunicationDisabledUntil = until }; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBERS}/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id, user_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, payload: DiscordJson.SerializeObject(pld)); } /// /// Modifies the current member nickname async. /// /// The guild_id. /// The nick. /// The reason. /// A Task. internal Task ModifyCurrentMemberNicknameAsync(ulong guild_id, string nick, string reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var pld = new RestGuildMemberModifyPayload { Nickname = nick }; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBERS}{Endpoints.ME}{Endpoints.NICK}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, payload: DiscordJson.SerializeObject(pld)); } #endregion #region Roles /// /// Gets the guild roles async. /// /// The guild_id. /// A Task. internal async Task> GetGuildRolesAsync(ulong guild_id) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.ROLES}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var roles_raw = JsonConvert.DeserializeObject>(res.Response).Select(xr => { xr.Discord = this.Discord; xr._guild_id = guild_id; return xr; }); return new ReadOnlyCollection(new List(roles_raw)); } /// /// Gets the guild async. /// /// The guild id. /// If true, with_counts. /// A Task. internal async Task GetGuildAsync(ulong guildId, bool? with_counts) { var urlparams = new Dictionary(); if (with_counts.HasValue) urlparams["with_counts"] = with_counts?.ToString(); var route = $"{Endpoints.GUILDS}/:guild_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, urlparams.Any() ? BuildQueryString(urlparams) : "", this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route, urlparams).ConfigureAwait(false); var json = JObject.Parse(res.Response); var rawMembers = (JArray)json["members"]; var guildRest = json.ToDiscordObject(); foreach (var r in guildRest._roles.Values) r._guild_id = guildRest.Id; if (this.Discord is DiscordClient dc) { await dc.OnGuildUpdateEventAsync(guildRest, rawMembers).ConfigureAwait(false); return dc._guilds[guildRest.Id]; } else { guildRest.Discord = this.Discord; return guildRest; } } /// /// Modifies the guild role async. /// /// The guild_id. /// The role_id. /// The name. /// The permissions. /// The color. /// If true, hoist. /// If true, mentionable. /// The icon. /// The unicode emoji icon. /// The reason. internal async Task ModifyGuildRoleAsync(ulong guild_id, ulong role_id, string name, Permissions? permissions, int? color, bool? hoist, bool? mentionable, Optional iconb64, Optional emoji, string reason) { var pld = new RestGuildRolePayload { Name = name, Permissions = permissions & PermissionMethods.FULL_PERMS, Color = color, Hoist = hoist, Mentionable = mentionable, }; if (emoji.HasValue && !iconb64.HasValue) pld.UnicodeEmoji = emoji; if (emoji.HasValue && iconb64.HasValue) { pld.IconBase64 = null; pld.UnicodeEmoji = emoji; } if (iconb64.HasValue) pld.IconBase64 = iconb64; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.ROLES}/:role_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id, role_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; ret._guild_id = guild_id; return ret; } /// /// Deletes the role async. /// /// The guild_id. /// The role_id. /// The reason. /// A Task. internal Task DeleteRoleAsync(ulong guild_id, ulong role_id, string reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.ROLES}/:role_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { guild_id, role_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers); } /// /// Creates the guild role async. /// /// The guild_id. /// The name. /// The permissions. /// The color. /// If true, hoist. /// If true, mentionable. /// The reason. /// A Task. internal async Task CreateGuildRoleAsync(ulong guild_id, string name, Permissions? permissions, int? color, bool? hoist, bool? mentionable, string reason) { var pld = new RestGuildRolePayload { Name = name, Permissions = permissions & PermissionMethods.FULL_PERMS, Color = color, Hoist = hoist, Mentionable = mentionable }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.ROLES}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { guild_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; ret._guild_id = guild_id; return ret; } #endregion #region Prune /// /// Gets the guild prune count async. /// /// The guild_id. /// The days. /// The include_roles. /// A Task. internal async Task GetGuildPruneCountAsync(ulong guild_id, int days, IEnumerable include_roles) { if (days < 0 || days > 30) throw new ArgumentException("Prune inactivity days must be a number between 0 and 30.", nameof(days)); var urlparams = new Dictionary { ["days"] = days.ToString(CultureInfo.InvariantCulture) }; var sb = new StringBuilder(); if (include_roles != null) { var roleArray = include_roles.ToArray(); var roleArrayCount = roleArray.Count(); for (var i = 0; i < roleArrayCount; i++) sb.Append($"&include_roles={roleArray[i]}"); } var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.PRUNE}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); var url = Utilities.GetApiUriFor(path, $"{BuildQueryString(urlparams)}{sb}", this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var pruned = JsonConvert.DeserializeObject(res.Response); return pruned.Pruned.Value; } /// /// Begins the guild prune async. /// /// The guild_id. /// The days. /// If true, compute_prune_count. /// The include_roles. /// The reason. /// A Task. internal async Task BeginGuildPruneAsync(ulong guild_id, int days, bool compute_prune_count, IEnumerable include_roles, string reason) { if (days < 0 || days > 30) throw new ArgumentException("Prune inactivity days must be a number between 0 and 30.", nameof(days)); var urlparams = new Dictionary { ["days"] = days.ToString(CultureInfo.InvariantCulture), ["compute_prune_count"] = compute_prune_count.ToString() }; var sb = new StringBuilder(); if (include_roles != null) { var roleArray = include_roles.ToArray(); var roleArrayCount = roleArray.Count(); for (var i = 0; i < roleArrayCount; i++) sb.Append($"&include_roles={roleArray[i]}"); } var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.PRUNE}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { guild_id }, out var path); var url = Utilities.GetApiUriFor(path, $"{BuildQueryString(urlparams)}{sb}", this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, headers).ConfigureAwait(false); var pruned = JsonConvert.DeserializeObject(res.Response); return pruned.Pruned; } #endregion #region GuildVarious /// /// Gets the template async. /// /// The code. /// A Task. internal async Task GetTemplateAsync(string code) { var route = $"{Endpoints.GUILDS}{Endpoints.TEMPLATES}/:code"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { code }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var templates_raw = JsonConvert.DeserializeObject(res.Response); return templates_raw; } /// /// Gets the guild integrations async. /// /// The guild_id. /// A Task. internal async Task> GetGuildIntegrationsAsync(ulong guild_id) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.INTEGRATIONS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var integrations_raw = JsonConvert.DeserializeObject>(res.Response).Select(xi => { xi.Discord = this.Discord; return xi; }); return new ReadOnlyCollection(new List(integrations_raw)); } /// /// Gets the guild preview async. /// /// The guild_id. /// A Task. internal async Task GetGuildPreviewAsync(ulong guild_id) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.PREVIEW}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; return ret; } /// /// Creates the guild integration async. /// /// The guild_id. /// The type. /// The id. /// A Task. internal async Task CreateGuildIntegrationAsync(ulong guild_id, string type, ulong id) { var pld = new RestGuildIntegrationAttachPayload { Type = type, Id = id }; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.INTEGRATIONS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { guild_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; return ret; } /// /// Modifies the guild integration async. /// /// The guild_id. /// The integration_id. /// The expire_behaviour. /// The expire_grace_period. /// If true, enable_emoticons. /// A Task. internal async Task ModifyGuildIntegrationAsync(ulong guild_id, ulong integration_id, int expire_behaviour, int expire_grace_period, bool enable_emoticons) { var pld = new RestGuildIntegrationModifyPayload { ExpireBehavior = expire_behaviour, ExpireGracePeriod = expire_grace_period, EnableEmoticons = enable_emoticons }; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.INTEGRATIONS}/:integration_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id, integration_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; return ret; } /// /// Deletes the guild integration async. /// /// The guild_id. /// The integration. /// A Task. internal Task DeleteGuildIntegrationAsync(ulong guild_id, DiscordIntegration integration) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.INTEGRATIONS}/:integration_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { guild_id, integration_id = integration.Id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, payload: DiscordJson.SerializeObject(integration)); } /// /// Syncs the guild integration async. /// /// The guild_id. /// The integration_id. /// A Task. internal Task SyncGuildIntegrationAsync(ulong guild_id, ulong integration_id) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.INTEGRATIONS}/:integration_id{Endpoints.SYNC}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { guild_id, integration_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route); } /// /// Gets the guild voice regions async. /// /// The guild_id. /// A Task. internal async Task> GetGuildVoiceRegionsAsync(ulong guild_id) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.REGIONS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var regions_raw = JsonConvert.DeserializeObject>(res.Response); return new ReadOnlyCollection(new List(regions_raw)); } /// /// Gets the guild invites async. /// /// The guild_id. /// A Task. internal async Task> GetGuildInvitesAsync(ulong guild_id) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.INVITES}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var invites_raw = JsonConvert.DeserializeObject>(res.Response).Select(xi => { xi.Discord = this.Discord; return xi; }); return new ReadOnlyCollection(new List(invites_raw)); } #endregion #region Invite /// /// Gets the invite async. /// /// The invite_code. /// If true, with_counts. /// If true, with_expiration. /// The scheduled event id to get. /// A Task. internal async Task GetInviteAsync(string invite_code, bool? with_counts, bool? with_expiration, ulong? guild_scheduled_event_id) { var urlparams = new Dictionary(); if (with_counts.HasValue) urlparams["with_counts"] = with_counts?.ToString(); if (with_expiration.HasValue) urlparams["with_expiration"] = with_expiration?.ToString(); if (guild_scheduled_event_id.HasValue) urlparams["guild_scheduled_event_id"] = guild_scheduled_event_id?.ToString(); var route = $"{Endpoints.INVITES}/:invite_code"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { invite_code }, out var path); var url = Utilities.GetApiUriFor(path, urlparams.Any() ? BuildQueryString(urlparams) : "", this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; return ret; } /// /// Deletes the invite async. /// /// The invite_code. /// The reason. /// A Task. internal async Task DeleteInviteAsync(string invite_code, string reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.INVITES}/:invite_code"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { invite_code }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; return ret; } /* * Disabled due to API restrictions * * internal async Task InternalAcceptInvite(string invite_code) * { * this.Discord.DebugLogger.LogMessage(LogLevel.Warning, "REST API", "Invite accept endpoint was used; this account is now likely unverified", DateTime.Now); * * var url = new Uri($"{Utils.GetApiBaseUri(this.Configuration), Endpoints.INVITES}/{invite_code)); * var bucket = this.Rest.GetBucket(0, MajorParameterType.Unbucketed, url, HttpRequestMethod.POST); * var res = await this.DoRequestAsync(this.Discord, bucket, url, HttpRequestMethod.POST).ConfigureAwait(false); * * var ret = JsonConvert.DeserializeObject(res.Response); * ret.Discord = this.Discord; * * return ret; * } */ #endregion #region Connections /// /// Gets the users connections async. /// /// A Task. internal async Task> GetUsersConnectionsAsync() { var route = $"{Endpoints.USERS}{Endpoints.ME}{Endpoints.CONNECTIONS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var connections_raw = JsonConvert.DeserializeObject>(res.Response).Select(xc => { xc.Discord = this.Discord; return xc; }); return new ReadOnlyCollection(new List(connections_raw)); } #endregion #region Voice /// /// Lists the voice regions async. /// /// A Task. internal async Task> ListVoiceRegionsAsync() { var route = $"{Endpoints.VOICE}{Endpoints.REGIONS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var regions = JsonConvert.DeserializeObject>(res.Response); return new ReadOnlyCollection(new List(regions)); } #endregion #region Webhooks /// /// Creates the webhook async. /// /// The channel_id. /// The name. /// The base64_avatar. /// The reason. /// A Task. internal async Task CreateWebhookAsync(ulong channel_id, string name, Optional base64_avatar, string reason) { var pld = new RestWebhookPayload { Name = name, AvatarBase64 = base64_avatar.HasValue ? base64_avatar.Value : null, AvatarSet = base64_avatar.HasValue }; var headers = new Dictionary(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.WEBHOOKS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { channel_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; ret.ApiClient = this; return ret; } /// /// Gets the channel webhooks async. /// /// The channel_id. /// A Task. internal async Task> GetChannelWebhooksAsync(ulong channel_id) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.WEBHOOKS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { channel_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var webhooks_raw = JsonConvert.DeserializeObject>(res.Response).Select(xw => { xw.Discord = this.Discord; xw.ApiClient = this; return xw; }); return new ReadOnlyCollection(new List(webhooks_raw)); } /// /// Gets the guild webhooks async. /// /// The guild_id. /// A Task. internal async Task> GetGuildWebhooksAsync(ulong guild_id) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.WEBHOOKS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var webhooks_raw = JsonConvert.DeserializeObject>(res.Response).Select(xw => { xw.Discord = this.Discord; xw.ApiClient = this; return xw; }); return new ReadOnlyCollection(new List(webhooks_raw)); } /// /// Gets the webhook async. /// /// The webhook_id. /// A Task. internal async Task GetWebhookAsync(ulong webhook_id) { var route = $"{Endpoints.WEBHOOKS}/:webhook_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { webhook_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; ret.ApiClient = this; return ret; } /// /// Gets the webhook with token async. /// /// The webhook_id. /// The webhook_token. /// A Task. internal async Task GetWebhookWithTokenAsync(ulong webhook_id, string webhook_token) { var route = $"{Endpoints.WEBHOOKS}/:webhook_id/:webhook_token"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { webhook_id, webhook_token }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Token = webhook_token; ret.Id = webhook_id; ret.Discord = this.Discord; ret.ApiClient = this; return ret; } /// /// Modifies the webhook async. /// /// The webhook_id. /// The channel id. /// The name. /// The base64_avatar. /// The reason. /// A Task. internal async Task ModifyWebhookAsync(ulong webhook_id, ulong channelId, string name, Optional base64_avatar, string reason) { var pld = new RestWebhookPayload { Name = name, AvatarBase64 = base64_avatar.HasValue ? base64_avatar.Value : null, AvatarSet = base64_avatar.HasValue, ChannelId = channelId }; var headers = new Dictionary(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.WEBHOOKS}/:webhook_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { webhook_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; ret.ApiClient = this; return ret; } /// /// Modifies the webhook async. /// /// The webhook_id. /// The name. /// The base64_avatar. /// The webhook_token. /// The reason. /// A Task. internal async Task ModifyWebhookAsync(ulong webhook_id, string name, string base64_avatar, string webhook_token, string reason) { var pld = new RestWebhookPayload { Name = name, AvatarBase64 = base64_avatar }; var headers = new Dictionary(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.WEBHOOKS}/:webhook_id/:webhook_token"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { webhook_id, webhook_token }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; ret.ApiClient = this; return ret; } /// /// Deletes the webhook async. /// /// The webhook_id. /// The reason. /// A Task. internal Task DeleteWebhookAsync(ulong webhook_id, string reason) { var headers = new Dictionary(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.WEBHOOKS}/:webhook_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { webhook_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers); } /// /// Deletes the webhook async. /// /// The webhook_id. /// The webhook_token. /// The reason. /// A Task. internal Task DeleteWebhookAsync(ulong webhook_id, string webhook_token, string reason) { var headers = new Dictionary(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.WEBHOOKS}/:webhook_id/:webhook_token"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { webhook_id, webhook_token }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers); } /// /// Executes the webhook async. /// /// The webhook_id. /// The webhook_token. /// The builder. /// The thread_id. /// A Task. internal async Task ExecuteWebhookAsync(ulong webhook_id, string webhook_token, DiscordWebhookBuilder builder, string thread_id) { builder.Validate(); if (builder.Embeds != null) foreach (var embed in builder.Embeds) if (embed.Timestamp != null) embed.Timestamp = embed.Timestamp.Value.ToUniversalTime(); var values = new Dictionary(); var pld = new RestWebhookExecutePayload { Content = builder.Content, Username = builder.Username.HasValue ? builder.Username.Value : null, AvatarUrl = builder.AvatarUrl.HasValue ? builder.AvatarUrl.Value : null, IsTTS = builder.IsTTS, Embeds = builder.Embeds, Components = builder.Components }; if (builder.Mentions != null) pld.Mentions = new DiscordMentions(builder.Mentions, builder.Mentions.Any()); if (builder.Files?.Count > 0) { ulong file_id = 0; List attachments = new(); foreach (var file in builder.Files) { DiscordAttachment att = new() { Id = file_id, Discord = this.Discord, Description = file.Description, FileName = file.FileName, FileSize = null }; attachments.Add(att); file_id++; } pld.Attachments = attachments; } if (!string.IsNullOrEmpty(builder.Content) || builder.Embeds?.Count() > 0 || builder.Files?.Count > 0 || builder.IsTTS == true || builder.Mentions != null) values["payload_json"] = DiscordJson.SerializeObject(pld); var route = $"{Endpoints.WEBHOOKS}/:webhook_id/:webhook_token"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { webhook_id, webhook_token }, out var path); var qub = Utilities.GetApiUriBuilderFor(path, this.Discord.Configuration).AddParameter("wait", "true"); if (thread_id != null) qub.AddParameter("thread_id", thread_id); var url = qub.Build(); var res = await this.DoMultipartAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, values: values, files: builder.Files).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); foreach (var att in ret.Attachments) att.Discord = this.Discord; foreach (var file in builder.Files.Where(x => x.ResetPositionTo.HasValue)) { file.Stream.Position = file.ResetPositionTo.Value; } ret.Discord = this.Discord; return ret; } /// /// Executes the webhook slack async. /// /// The webhook_id. /// The webhook_token. /// The json_payload. /// The thread_id. /// A Task. internal async Task ExecuteWebhookSlackAsync(ulong webhook_id, string webhook_token, string json_payload, string thread_id) { var route = $"{Endpoints.WEBHOOKS}/:webhook_id/:webhook_token{Endpoints.SLACK}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { webhook_id, webhook_token }, out var path); var qub = Utilities.GetApiUriBuilderFor(path, this.Discord.Configuration).AddParameter("wait", "true"); if (thread_id != null) qub.AddParameter("thread_id", thread_id); var url = qub.Build(); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: json_payload).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; return ret; } /// /// Executes the webhook github async. /// /// The webhook_id. /// The webhook_token. /// The json_payload. /// The thread_id. /// A Task. internal async Task ExecuteWebhookGithubAsync(ulong webhook_id, string webhook_token, string json_payload, string thread_id) { var route = $"{Endpoints.WEBHOOKS}/:webhook_id/:webhook_token{Endpoints.GITHUB}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { webhook_id, webhook_token }, out var path); var qub = Utilities.GetApiUriBuilderFor(path, this.Discord.Configuration).AddParameter("wait", "true"); if (thread_id != null) qub.AddParameter("thread_id", thread_id); var url = qub.Build(); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: json_payload).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; return ret; } /// /// Edits the webhook message async. /// /// The webhook_id. /// The webhook_token. /// The message_id. /// The builder. /// The thread_id. /// A Task. internal async Task EditWebhookMessageAsync(ulong webhook_id, string webhook_token, string message_id, DiscordWebhookBuilder builder, string thread_id) { builder.Validate(true); var pld = new RestWebhookMessageEditPayload { Content = builder.Content, Embeds = builder.Embeds, Mentions = builder.Mentions, Components = builder.Components, }; if (builder.Files?.Count > 0) { ulong file_id = 0; List attachments = new(); foreach (var file in builder.Files) { DiscordAttachment att = new() { Id = file_id, Discord = this.Discord, Description = file.Description, FileName = file.FileName, FileSize = null }; attachments.Add(att); file_id++; } if (builder.Attachments != null && builder.Attachments?.Count() > 0) attachments.AddRange(builder.Attachments); pld.Attachments = attachments; var values = new Dictionary { ["payload_json"] = DiscordJson.SerializeObject(pld) }; var route = $"{Endpoints.WEBHOOKS}/:webhook_id/:webhook_token{Endpoints.MESSAGES}/:message_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { webhook_id, webhook_token, message_id }, out var path); var qub = Utilities.GetApiUriBuilderFor(path, this.Discord.Configuration); if (thread_id != null) qub.AddParameter("thread_id", thread_id); var url = qub.Build(); var res = await this.DoMultipartAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, values: values, files: builder.Files); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; foreach (var att in ret._attachments) att.Discord = this.Discord; foreach (var file in builder.Files.Where(x => x.ResetPositionTo.HasValue)) { file.Stream.Position = file.ResetPositionTo.Value; } return ret; } else { pld.Attachments = builder.Attachments; var route = $"{Endpoints.WEBHOOKS}/:webhook_id/:webhook_token{Endpoints.MESSAGES}/:message_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { webhook_id, webhook_token, message_id }, out var path); var qub = Utilities.GetApiUriBuilderFor(path, this.Discord.Configuration); if (thread_id != null) qub.AddParameter("thread_id", thread_id); var url = qub.Build(); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, payload: DiscordJson.SerializeObject(pld)); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; foreach (var att in ret._attachments) att.Discord = this.Discord; return ret; } } /// /// Edits the webhook message async. /// /// The webhook_id. /// The webhook_token. /// The message_id. /// The builder. /// The thread_id. /// A Task. internal Task EditWebhookMessageAsync(ulong webhook_id, string webhook_token, ulong message_id, DiscordWebhookBuilder builder, ulong thread_id) => this.EditWebhookMessageAsync(webhook_id, webhook_token, message_id.ToString(), builder, thread_id.ToString()); /// /// Gets the webhook message async. /// /// The webhook_id. /// The webhook_token. /// The message_id. /// The thread_id. /// A Task. internal async Task GetWebhookMessageAsync(ulong webhook_id, string webhook_token, string message_id, string thread_id) { var route = $"{Endpoints.WEBHOOKS}/:webhook_id/:webhook_token{Endpoints.MESSAGES}/:message_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { webhook_id, webhook_token, message_id }, out var path); var qub = Utilities.GetApiUriBuilderFor(path, this.Discord.Configuration); if (thread_id != null) qub.AddParameter("thread_id", thread_id); var url = qub.Build(); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; return ret; } /// /// Gets the webhook message async. /// /// The webhook_id. /// The webhook_token. /// The message_id. /// A Task. internal Task GetWebhookMessageAsync(ulong webhook_id, string webhook_token, ulong message_id) => this.GetWebhookMessageAsync(webhook_id, webhook_token, message_id.ToString(), null); /// /// Gets the webhook message async. /// /// The webhook_id. /// The webhook_token. /// The message_id. /// The thread_id. /// A Task. internal Task GetWebhookMessageAsync(ulong webhook_id, string webhook_token, ulong message_id, ulong thread_id) => this.GetWebhookMessageAsync(webhook_id, webhook_token, message_id.ToString(), thread_id.ToString()); /// /// Deletes the webhook message async. /// /// The webhook_id. /// The webhook_token. /// The message_id. /// The thread_id. /// A Task. internal async Task DeleteWebhookMessageAsync(ulong webhook_id, string webhook_token, string message_id, string thread_id) { var route = $"{Endpoints.WEBHOOKS}/:webhook_id/:webhook_token{Endpoints.MESSAGES}/:message_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { webhook_id, webhook_token, message_id }, out var path); var qub = Utilities.GetApiUriBuilderFor(path, this.Discord.Configuration); if (thread_id != null) qub.AddParameter("thread_id", thread_id); var url = qub.Build(); await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route); } /// /// Deletes the webhook message async. /// /// The webhook_id. /// The webhook_token. /// The message_id. /// A Task. internal Task DeleteWebhookMessageAsync(ulong webhook_id, string webhook_token, ulong message_id) => this.DeleteWebhookMessageAsync(webhook_id, webhook_token, message_id.ToString(), null); /// /// Deletes the webhook message async. /// /// The webhook_id. /// The webhook_token. /// The message_id. /// The thread_id. /// A Task. internal Task DeleteWebhookMessageAsync(ulong webhook_id, string webhook_token, ulong message_id, ulong thread_id) => this.DeleteWebhookMessageAsync(webhook_id, webhook_token, message_id.ToString(), thread_id.ToString()); #endregion #region Reactions /// /// Creates the reaction async. /// /// The channel_id. /// The message_id. /// The emoji. /// A Task. internal Task CreateReactionAsync(ulong channel_id, ulong message_id, string emoji) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}/:message_id{Endpoints.REACTIONS}/:emoji{Endpoints.ME}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new { channel_id, message_id, emoji }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route, ratelimitWaitOverride: this.Discord.Configuration.UseRelativeRatelimit ? null : (double?)0.26); } /// /// Deletes the own reaction async. /// /// The channel_id. /// The message_id. /// The emoji. /// A Task. internal Task DeleteOwnReactionAsync(ulong channel_id, ulong message_id, string emoji) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}/:message_id{Endpoints.REACTIONS}/:emoji{Endpoints.ME}"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { channel_id, message_id, emoji }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, ratelimitWaitOverride: this.Discord.Configuration.UseRelativeRatelimit ? null : (double?)0.26); } /// /// Deletes the user reaction async. /// /// The channel_id. /// The message_id. /// The user_id. /// The emoji. /// The reason. /// A Task. internal Task DeleteUserReactionAsync(ulong channel_id, ulong message_id, ulong user_id, string emoji, string reason) { var headers = new Dictionary(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}/:message_id{Endpoints.REACTIONS}/:emoji/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { channel_id, message_id, emoji, user_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers, ratelimitWaitOverride: this.Discord.Configuration.UseRelativeRatelimit ? null : (double?)0.26); } /// /// Gets the reactions async. /// /// The channel_id. /// The message_id. /// The emoji. /// The after_id. /// The limit. /// A Task. internal async Task> GetReactionsAsync(ulong channel_id, ulong message_id, string emoji, ulong? after_id = null, int limit = 25) { var urlparams = new Dictionary(); if (after_id.HasValue) urlparams["after"] = after_id.Value.ToString(CultureInfo.InvariantCulture); urlparams["limit"] = limit.ToString(CultureInfo.InvariantCulture); var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}/:message_id{Endpoints.REACTIONS}/:emoji"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { channel_id, message_id, emoji }, out var path); var url = Utilities.GetApiUriFor(path, BuildQueryString(urlparams), this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var reacters_raw = JsonConvert.DeserializeObject>(res.Response); var reacters = new List(); foreach (var xr in reacters_raw) { var usr = new DiscordUser(xr) { Discord = this.Discord }; usr = this.Discord.UserCache.AddOrUpdate(xr.Id, usr, (id, old) => { old.Username = usr.Username; old.Discriminator = usr.Discriminator; old.AvatarHash = usr.AvatarHash; return old; }); reacters.Add(usr); } return new ReadOnlyCollection(new List(reacters)); } /// /// Deletes the all reactions async. /// /// The channel_id. /// The message_id. /// The reason. /// A Task. internal Task DeleteAllReactionsAsync(ulong channel_id, ulong message_id, string reason) { var headers = new Dictionary(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}/:message_id{Endpoints.REACTIONS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { channel_id, message_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers, ratelimitWaitOverride: this.Discord.Configuration.UseRelativeRatelimit ? null : (double?)0.26); } /// /// Deletes the reactions emoji async. /// /// The channel_id. /// The message_id. /// The emoji. /// A Task. internal Task DeleteReactionsEmojiAsync(ulong channel_id, ulong message_id, string emoji) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}/:message_id{Endpoints.REACTIONS}/:emoji"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { channel_id, message_id, emoji }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, ratelimitWaitOverride: this.Discord.Configuration.UseRelativeRatelimit ? null : (double?)0.26); } #endregion #region Threads /// /// Creates the thread with message. /// /// The channel id to create the thread in. /// The message id to create the thread from. /// The name of the thread. /// The auto_archive_duration for the thread. /// The rate limit per user. /// The reason. internal async Task CreateThreadWithMessageAsync(ulong channel_id, ulong message_id, string name, ThreadAutoArchiveDuration auto_archive_duration, int? rate_limit_per_user, string reason = null) { var pld = new RestThreadChannelCreatePayload { Name = name, AutoArchiveDuration = auto_archive_duration, PerUserRateLimit = rate_limit_per_user }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}/:message_id{Endpoints.THREADS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { channel_id, message_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, headers, DiscordJson.SerializeObject(pld)); var thread_channel = JsonConvert.DeserializeObject(res.Response); return thread_channel; } /// /// Creates the thread without a message. /// /// The channel id to create the thread in. /// The name of the thread. /// The auto_archive_duration for the thread. /// Can be either or . /// The rate limit per user. /// The reason. internal async Task CreateThreadWithoutMessageAsync(ulong channel_id, string name, ThreadAutoArchiveDuration auto_archive_duration, ChannelType type = ChannelType.PublicThread, int? rate_limit_per_user = null, string reason = null) { var pld = new RestThreadChannelCreatePayload { Name = name, AutoArchiveDuration = auto_archive_duration, PerUserRateLimit = rate_limit_per_user, Type = type }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.THREADS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { channel_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, headers, DiscordJson.SerializeObject(pld)); var thread_channel = JsonConvert.DeserializeObject(res.Response); return thread_channel; } /// /// Gets the thread. /// /// The thread id. internal async Task GetThreadAsync(ulong thread_id) { var route = $"{Endpoints.CHANNELS}/:channel_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { thread_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; return ret; } /// /// Joins the thread. /// /// The channel id. internal async Task JoinThreadAsync(ulong channel_id) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.THREAD_MEMBERS}{Endpoints.ME}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new { channel_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route); } /// /// Leaves the thread. /// /// The channel id. internal async Task LeaveThreadAsync(ulong channel_id) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.THREAD_MEMBERS}{Endpoints.ME}"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { channel_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route); } /// /// Adds a thread member. /// /// The channel id to add the member to. /// The user id to add. internal async Task AddThreadMemberAsync(ulong channel_id, ulong user_id) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.THREAD_MEMBERS}/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new { channel_id, user_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route); } /// /// Gets a thread member. /// /// The channel id to get the member from. /// The user id to get. internal async Task GetThreadMemberAsync(ulong channel_id, ulong user_id) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.THREAD_MEMBERS}/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { channel_id, user_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route); var thread_member = JsonConvert.DeserializeObject(res.Response); return thread_member; } /// /// Removes a thread member. /// /// The channel id to remove the member from. /// The user id to remove. internal async Task RemoveThreadMemberAsync(ulong channel_id, ulong user_id) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.THREAD_MEMBERS}/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { channel_id, user_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route); } /// /// Gets the thread members. /// /// The thread id. internal async Task> GetThreadMembersAsync(ulong thread_id) { var route = $"{Endpoints.CHANNELS}/:thread_id{Endpoints.THREAD_MEMBERS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { thread_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route); var thread_members_raw = JsonConvert.DeserializeObject>(res.Response); return new ReadOnlyCollection(thread_members_raw); } /// /// Gets the active threads in a guild. /// /// The guild id. internal async Task GetActiveThreadsAsync(ulong guild_id) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.THREADS}{Endpoints.THREAD_ACTIVE}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route); var thread_return = JsonConvert.DeserializeObject(res.Response); return thread_return; } /// /// Gets the joined private archived threads in a channel. /// /// The channel id. /// Get threads before snowflake. /// Limit the results. internal async Task GetJoinedPrivateArchivedThreadsAsync(ulong channel_id, ulong? before, int? limit) { var urlparams = new Dictionary(); if (before != null) urlparams["before"] = before.Value.ToString(CultureInfo.InvariantCulture); if (limit != null && limit > 0) urlparams["limit"] = limit.Value.ToString(CultureInfo.InvariantCulture); var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.USERS}{Endpoints.ME}{Endpoints.THREADS}{Endpoints.THREAD_ARCHIVED}{Endpoints.THREAD_PRIVATE}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { channel_id }, out var path); var url = Utilities.GetApiUriFor(path, urlparams.Any() ? BuildQueryString(urlparams) : "", this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route); var thread_return = JsonConvert.DeserializeObject(res.Response); return thread_return; } /// /// Gets the public archived threads in a channel. /// /// The channel id. /// Get threads before snowflake. /// Limit the results. internal async Task GetPublicArchivedThreadsAsync(ulong channel_id, ulong? before, int? limit) { var urlparams = new Dictionary(); if (before != null) urlparams["before"] = before.Value.ToString(CultureInfo.InvariantCulture); if (limit != null && limit > 0) urlparams["limit"] = limit.Value.ToString(CultureInfo.InvariantCulture); var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.THREADS}{Endpoints.THREAD_ARCHIVED}{Endpoints.THREAD_PUBLIC}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { channel_id }, out var path); var url = Utilities.GetApiUriFor(path, urlparams.Any() ? BuildQueryString(urlparams) : "", this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route); var thread_return = JsonConvert.DeserializeObject(res.Response); return thread_return; } /// /// Gets the private archived threads in a channel. /// /// The channel id. /// Get threads before snowflake. /// Limit the results. internal async Task GetPrivateArchivedThreadsAsync(ulong channel_id, ulong? before, int? limit) { var urlparams = new Dictionary(); if (before != null) urlparams["before"] = before.Value.ToString(CultureInfo.InvariantCulture); if (limit != null && limit > 0) urlparams["limit"] = limit.Value.ToString(CultureInfo.InvariantCulture); var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.THREADS}{Endpoints.THREAD_ARCHIVED}{Endpoints.THREAD_PRIVATE}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { channel_id }, out var path); var url = Utilities.GetApiUriFor(path, urlparams.Any() ? BuildQueryString(urlparams) : "", this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route); var thread_return = JsonConvert.DeserializeObject(res.Response); return thread_return; } /// /// Modifies a thread. /// /// The thread to modify. /// The new name. /// The new locked state. /// The new archived state. /// The new auto archive duration. /// The new per user rate limit. /// The new user invitable state. /// The reason for the modification. internal Task ModifyThreadAsync(ulong thread_id, string name, Optional locked, Optional archived, Optional autoArchiveDuration, Optional perUserRateLimit, Optional invitable, string reason) { var pld = new RestThreadChannelModifyPayload { Name = name, Archived = archived, AutoArchiveDuration = autoArchiveDuration, Locked = locked, PerUserRateLimit = perUserRateLimit, Invitable = invitable }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.CHANNELS}/:thread_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { thread_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)); } /// /// Deletes a thread. /// /// The thread to delete. /// The reason for deletion. internal Task DeleteThreadAsync(ulong thread_id, string reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.CHANNELS}/:thread_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { thread_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers); } #endregion #region Emoji /// /// Gets the guild emojis async. /// /// The guild_id. /// A Task. internal async Task> GetGuildEmojisAsync(ulong guild_id) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.EMOJIS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var emojisRaw = JsonConvert.DeserializeObject>(res.Response); this.Discord.Guilds.TryGetValue(guild_id, out var gld); var users = new Dictionary(); var emojis = new List(); foreach (var rawEmoji in emojisRaw) { var xge = rawEmoji.ToObject(); xge.Guild = gld; var xtu = rawEmoji["user"]?.ToObject(); if (xtu != null) { if (!users.ContainsKey(xtu.Id)) { var user = gld != null && gld.Members.TryGetValue(xtu.Id, out var member) ? member : new DiscordUser(xtu); users[user.Id] = user; } xge.User = users[xtu.Id]; } emojis.Add(xge); } return new ReadOnlyCollection(emojis); } /// /// Gets the guild emoji async. /// /// The guild_id. /// The emoji_id. /// A Task. internal async Task GetGuildEmojiAsync(ulong guild_id, ulong emoji_id) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.EMOJIS}/:emoji_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { guild_id, emoji_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); this.Discord.Guilds.TryGetValue(guild_id, out var gld); var emoji_raw = JObject.Parse(res.Response); var emoji = emoji_raw.ToObject(); emoji.Guild = gld; var xtu = emoji_raw["user"]?.ToObject(); if (xtu != null) emoji.User = gld != null && gld.Members.TryGetValue(xtu.Id, out var member) ? member : new DiscordUser(xtu); return emoji; } /// /// Creates the guild emoji async. /// /// The guild_id. /// The name. /// The imageb64. /// The roles. /// The reason. /// A Task. internal async Task CreateGuildEmojiAsync(ulong guild_id, string name, string imageb64, IEnumerable roles, string reason) { var pld = new RestGuildEmojiCreatePayload { Name = name, ImageB64 = imageb64, Roles = roles?.ToArray() }; var headers = new Dictionary(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.EMOJIS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { guild_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); this.Discord.Guilds.TryGetValue(guild_id, out var gld); var emoji_raw = JObject.Parse(res.Response); var emoji = emoji_raw.ToObject(); emoji.Guild = gld; var xtu = emoji_raw["user"]?.ToObject(); emoji.User = xtu != null ? gld != null && gld.Members.TryGetValue(xtu.Id, out var member) ? member : new DiscordUser(xtu) : this.Discord.CurrentUser; return emoji; } /// /// Modifies the guild emoji async. /// /// The guild_id. /// The emoji_id. /// The name. /// The roles. /// The reason. /// A Task. internal async Task ModifyGuildEmojiAsync(ulong guild_id, ulong emoji_id, string name, IEnumerable roles, string reason) { var pld = new RestGuildEmojiModifyPayload { Name = name, Roles = roles?.ToArray() }; var headers = new Dictionary(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.EMOJIS}/:emoji_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id, emoji_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); this.Discord.Guilds.TryGetValue(guild_id, out var gld); var emoji_raw = JObject.Parse(res.Response); var emoji = emoji_raw.ToObject(); emoji.Guild = gld; var xtu = emoji_raw["user"]?.ToObject(); if (xtu != null) emoji.User = gld != null && gld.Members.TryGetValue(xtu.Id, out var member) ? member : new DiscordUser(xtu); return emoji; } /// /// Deletes the guild emoji async. /// /// The guild_id. /// The emoji_id. /// The reason. /// A Task. internal Task DeleteGuildEmojiAsync(ulong guild_id, ulong emoji_id, string reason) { var headers = new Dictionary(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.EMOJIS}/:emoji_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { guild_id, emoji_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); return this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers); } #endregion #region Stickers /// /// Gets a sticker. /// /// The sticker id. internal async Task GetStickerAsync(ulong sticker_id) { var route = $"{Endpoints.STICKERS}/:sticker_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {sticker_id}, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var ret = JObject.Parse(res.Response).ToDiscordObject(); ret.Discord = this.Discord; return ret; } /// /// Gets the sticker packs. /// internal async Task> GetStickerPacksAsync() { var route = $"{Endpoints.STICKERPACKS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var json = JObject.Parse(res.Response)["sticker_packs"] as JArray; var ret = json.ToDiscordObject(); return ret.ToList(); } /// /// Gets the guild stickers. /// /// The guild id. internal async Task> GetGuildStickersAsync(ulong guild_id) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.STICKERS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {guild_id}, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var json = JArray.Parse(res.Response); var ret = json.ToDiscordObject(); for (var i = 0; i < ret.Length; i++) { var stkr = ret[i]; stkr.Discord = this.Discord; if (json[i]["user"] is JObject obj) // Null = Missing stickers perm // { var tsr = obj.ToDiscordObject(); var usr = new DiscordUser(tsr) {Discord = this.Discord}; usr = this.Discord.UserCache.AddOrUpdate(tsr.Id, usr, (id, old) => { old.Username = usr.Username; old.Discriminator = usr.Discriminator; old.AvatarHash = usr.AvatarHash; return old; }); stkr.User = usr; } } return ret.ToList(); } /// /// Gets a guild sticker. /// /// The guild id. /// The sticker id. internal async Task GetGuildStickerAsync(ulong guild_id, ulong sticker_id) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.STICKERS}/:sticker_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {guild_id, sticker_id}, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var json = JObject.Parse(res.Response); var ret = json.ToDiscordObject(); if (json["user"] is not null) // Null = Missing stickers perm // { var tsr = json["user"].ToDiscordObject(); var usr = new DiscordUser(tsr) {Discord = this.Discord}; usr = this.Discord.UserCache.AddOrUpdate(tsr.Id, usr, (id, old) => { old.Username = usr.Username; old.Discriminator = usr.Discriminator; old.AvatarHash = usr.AvatarHash; return old; }); ret.User = usr; } ret.Discord = this.Discord; return ret; } /// /// Creates the guild sticker. /// /// The guild id. /// The name. /// The description. /// The tags. /// The file. /// The reason. internal async Task CreateGuildStickerAsync(ulong guild_id, string name, string description, string tags, DiscordMessageFile file, string reason) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.STICKERS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new {guild_id}, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var res = await this.DoStickerMultipartAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, headers, file, name, tags, description); var ret = JObject.Parse(res.Response).ToDiscordObject(); ret.Discord = this.Discord; return ret; } /// /// Modifies the guild sticker. /// /// The guild id. /// The sticker id. /// The name. /// The description. /// The tags. /// The reason. internal async Task ModifyGuildStickerAsync(ulong guild_id, ulong sticker_id, Optional name, Optional description, Optional tags, string reason) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.STICKERS}/:sticker_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new {guild_id, sticker_id}, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var pld = new RestStickerModifyPayload() { Name = name, Description = description, Tags = tags }; var values = new Dictionary { ["payload_json"] = DiscordJson.SerializeObject(pld) }; var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route); var ret = JObject.Parse(res.Response).ToDiscordObject(); ret.Discord = this.Discord; return null; } /// /// Deletes the guild sticker async. /// /// The guild id. /// The sticker id. /// The reason. internal async Task DeleteGuildStickerAsync(ulong guild_id, ulong sticker_id, string reason) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.STICKERS}/:sticker_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { guild_id, sticker_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route, headers); } #endregion #region Application Commands /// /// Gets the global application commands async. /// /// The application_id. /// A Task. internal async Task> GetGlobalApplicationCommandsAsync(ulong application_id) { var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.COMMANDS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { application_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route); var ret = JsonConvert.DeserializeObject>(res.Response); foreach (var app in ret) app.Discord = this.Discord; return ret.ToList(); } /// /// Bulks the overwrite global application commands async. /// /// The application_id. /// The commands. /// A Task. internal async Task> BulkOverwriteGlobalApplicationCommandsAsync(ulong application_id, IEnumerable commands) { var pld = new List(); foreach (var command in commands) { pld.Add(new RestApplicationCommandCreatePayload { Type = command.Type, Name = command.Name, Description = command.Description, Options = command.Options, DefaultPermission = command.DefaultPermission, NameLocalizations = command.NameLocalizations?.GetKeyValuePairs(), DescriptionLocalizations = command.DescriptionLocalizations?.GetKeyValuePairs() }); } this.Discord.Logger.LogDebug(DiscordJson.SerializeObject(pld)); var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.COMMANDS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new { application_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route, payload: DiscordJson.SerializeObject(pld)); var ret = JsonConvert.DeserializeObject>(res.Response); foreach (var app in ret) app.Discord = this.Discord; return ret.ToList(); } /// /// Creates the global application command async. /// /// The application_id. /// The command. /// A Task. internal async Task CreateGlobalApplicationCommandAsync(ulong application_id, DiscordApplicationCommand command) { var pld = new RestApplicationCommandCreatePayload { Type = command.Type, Name = command.Name, Description = command.Description, Options = command.Options, DefaultPermission = command.DefaultPermission, NameLocalizations = command.NameLocalizations.GetKeyValuePairs(), DescriptionLocalizations = command.DescriptionLocalizations.GetKeyValuePairs() }; var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.COMMANDS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { application_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; return ret; } /// /// Gets the global application command async. /// /// The application_id. /// The command_id. /// A Task. internal async Task GetGlobalApplicationCommandAsync(ulong application_id, ulong command_id) { var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.COMMANDS}/:command_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { application_id, command_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; return ret; } /// /// Edits the global application command async. /// /// The application_id. /// The command_id. /// The name. /// The description. /// The options. /// The default_permission. /// The localizations of the name. /// The localizations of the description. /// A Task. internal async Task EditGlobalApplicationCommandAsync(ulong application_id, ulong command_id, Optional name, Optional description, Optional> options, Optional default_permission, Optional name_localization, Optional description_localization) { var pld = new RestApplicationCommandEditPayload { Name = name, Description = description, Options = options, DefaultPermission = default_permission, NameLocalizations = name_localization.HasValue ? name_localization.Value.GetKeyValuePairs() : null, DescriptionLocalizations = description_localization.HasValue ? description_localization.Value.GetKeyValuePairs() : null }; var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.COMMANDS}/:command_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { application_id, command_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, payload: DiscordJson.SerializeObject(pld)); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; return ret; } /// /// Deletes the global application command async. /// /// The application_id. /// The command_id. /// A Task. internal async Task DeleteGlobalApplicationCommandAsync(ulong application_id, ulong command_id) { var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.COMMANDS}/:command_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { application_id, command_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route); } /// /// Gets the guild application commands async. /// /// The application_id. /// The guild_id. /// A Task. internal async Task> GetGuildApplicationCommandsAsync(ulong application_id, ulong guild_id) { var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.GUILDS}/:guild_id{Endpoints.COMMANDS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { application_id, guild_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route); var ret = JsonConvert.DeserializeObject>(res.Response); foreach (var app in ret) app.Discord = this.Discord; return ret.ToList(); } /// /// Bulks the overwrite guild application commands async. /// /// The application_id. /// The guild_id. /// The commands. /// A Task. internal async Task> BulkOverwriteGuildApplicationCommandsAsync(ulong application_id, ulong guild_id, IEnumerable commands) { var pld = new List(); foreach (var command in commands) { pld.Add(new RestApplicationCommandCreatePayload { Type = command.Type, Name = command.Name, Description = command.Description, Options = command.Options, DefaultPermission = command.DefaultPermission, NameLocalizations = command.NameLocalizations?.GetKeyValuePairs(), DescriptionLocalizations = command.DescriptionLocalizations?.GetKeyValuePairs() }); } this.Discord.Logger.LogDebug(DiscordJson.SerializeObject(pld)); var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.GUILDS}/:guild_id{Endpoints.COMMANDS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new { application_id, guild_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route, payload: DiscordJson.SerializeObject(pld)); var ret = JsonConvert.DeserializeObject>(res.Response); foreach (var app in ret) app.Discord = this.Discord; return ret.ToList(); } /// /// Creates the guild application command async. /// /// The application_id. /// The guild_id. /// The command. /// A Task. internal async Task CreateGuildApplicationCommandAsync(ulong application_id, ulong guild_id, DiscordApplicationCommand command) { var pld = new RestApplicationCommandCreatePayload { Type = command.Type, Name = command.Name, Description = command.Description, Options = command.Options, DefaultPermission = command.DefaultPermission, NameLocalizations = command.NameLocalizations.GetKeyValuePairs(), DescriptionLocalizations = command.DescriptionLocalizations.GetKeyValuePairs() }; var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.GUILDS}/:guild_id{Endpoints.COMMANDS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { application_id, guild_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; return ret; } /// /// Gets the guild application command async. /// /// The application_id. /// The guild_id. /// The command_id. internal async Task GetGuildApplicationCommandAsync(ulong application_id, ulong guild_id, ulong command_id) { var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.GUILDS}/:guild_id{Endpoints.COMMANDS}/:command_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { application_id, guild_id, command_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; return ret; } /// /// Edits the guild application command async. /// /// The application_id. /// The guild_id. /// The command_id. /// The name. /// The description. /// The options. /// The default_permission. /// The localizations of the name. /// The localizations of the description. internal async Task EditGuildApplicationCommandAsync(ulong application_id, ulong guild_id, ulong command_id, Optional name, Optional description, Optional> options, Optional default_permission, Optional name_localization, Optional description_localization) { var pld = new RestApplicationCommandEditPayload { Name = name, Description = description, Options = options, DefaultPermission = default_permission, NameLocalizations = name_localization.HasValue ? name_localization.Value.GetKeyValuePairs() : null, DescriptionLocalizations = description_localization.HasValue ? description_localization.Value.GetKeyValuePairs() : null }; var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.GUILDS}/:guild_id{Endpoints.COMMANDS}/:command_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { application_id, guild_id, command_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, payload: DiscordJson.SerializeObject(pld)); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; return ret; } /// /// Deletes the guild application command async. /// /// The application_id. /// The guild_id. /// The command_id. internal async Task DeleteGuildApplicationCommandAsync(ulong application_id, ulong guild_id, ulong command_id) { var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.GUILDS}/:guild_id{Endpoints.COMMANDS}/:command_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new { application_id, guild_id, command_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route); } /// /// Gets the guild application command permissions. /// /// The target application id. /// The target guild id. internal async Task> GetGuildApplicationCommandPermissionsAsync(ulong application_id, ulong guild_id) { var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.GUILDS}/:guild_id{Endpoints.COMMANDS}{Endpoints.PERMISSIONS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { application_id, guild_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route); var ret = JsonConvert.DeserializeObject>(res.Response); foreach (var app in ret) app.Discord = this.Discord; return ret.ToList(); } /// /// Gets the application command permission. /// /// The target application id. /// The target guild id. /// The target command id. internal async Task GetApplicationCommandPermissionAsync(ulong application_id, ulong guild_id, ulong command_id) { var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.GUILDS}/:guild_id{Endpoints.COMMANDS}/:command_id{Endpoints.PERMISSIONS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { application_id, guild_id, command_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; return ret; } /// /// Overwrites the guild application command permissions. /// /// The target application id. /// The target guild id. /// The target command id. /// Array of permissions. internal async Task OverwriteGuildApplicationCommandPermissionsAsync(ulong application_id, ulong guild_id, ulong command_id, IEnumerable permissions) { var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.GUILDS}/:guild_id{Endpoints.COMMANDS}/:command_id{Endpoints.PERMISSIONS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new { application_id, guild_id, command_id }, out var path); if (permissions.ToArray().Length > 10) throw new NotSupportedException("You can add only up to 10 permission overwrites per command."); var pld = new RestApplicationCommandPermissionEditPayload { Permissions = permissions }; var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route, payload: DiscordJson.SerializeObject(pld)); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; return ret; } /// /// Bulks overwrite the application command permissions. /// /// The target application id. /// The target guild id. /// internal async Task> BulkOverwriteApplicationCommandPermissionsAsync(ulong application_id, ulong guild_id, IEnumerable permission_overwrites) { var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.GUILDS}/:guild_id{Endpoints.COMMANDS}{Endpoints.PERMISSIONS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new { application_id, guild_id }, out var path); var pld = new List(); foreach (var overwrite in permission_overwrites) { if (overwrite.Permissions.Count > 10) throw new NotSupportedException("You can add only up to 10 permission overwrites per command."); pld.Add(new RestGuildApplicationCommandPermissionEditPayload { CommandId = overwrite.Id, Permissions = overwrite.Permissions }); } var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route, payload: DiscordJson.SerializeObject(pld.ToArray())); var ret = JsonConvert.DeserializeObject>(res.Response); foreach (var app in ret) app.Discord = this.Discord; return ret.ToList(); } /// /// Creates the interaction response async. /// /// The interaction_id. /// The interaction_token. /// The type. /// The builder. /// A Task. internal async Task CreateInteractionResponseAsync(ulong interaction_id, string interaction_token, InteractionResponseType type, DiscordInteractionResponseBuilder builder) { if (builder?.Embeds != null) foreach (var embed in builder.Embeds) if (embed.Timestamp != null) embed.Timestamp = embed.Timestamp.Value.ToUniversalTime(); RestInteractionResponsePayload pld; if (type != InteractionResponseType.AutoCompleteResult) { var data = builder != null ? new DiscordInteractionApplicationCommandCallbackData { Content = builder.Content ?? null, Embeds = builder.Embeds ?? null, IsTTS = builder.IsTTS, Mentions = builder.Mentions ?? null, Flags = builder.IsEphemeral ? MessageFlags.Ephemeral : null, Components = builder.Components ?? null, Choices = null } : null; pld = new RestInteractionResponsePayload { Type = type, Data = data }; if (builder != null && builder.Files != null && builder.Files.Count > 0) { ulong file_id = 0; List attachments = new(); foreach (var file in builder.Files) { DiscordAttachment att = new() { Id = file_id, Discord = this.Discord, Description = file.Description, FileName = file.FileName, FileSize = null }; attachments.Add(att); file_id++; } pld.Attachments = attachments; pld.Data.Attachments = attachments; } } else { pld = new RestInteractionResponsePayload { Type = type, Data = new DiscordInteractionApplicationCommandCallbackData { Content = null, Embeds = null, IsTTS = null, Mentions = null, Flags = null, Components = null, Choices = builder.Choices, Attachments = null }, Attachments = null }; } var values = new Dictionary(); if (builder != null) if (!string.IsNullOrEmpty(builder.Content) || builder.Embeds?.Count() > 0 || builder.IsTTS == true || builder.Mentions != null || builder.Files?.Count > 0) values["payload_json"] = DiscordJson.SerializeObject(pld); var route = $"{Endpoints.INTERACTIONS}/:interaction_id/:interaction_token{Endpoints.CALLBACK}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { interaction_id, interaction_token }, out var path); var url = Utilities.GetApiUriBuilderFor(path, this.Discord.Configuration).AddParameter("wait", "false").Build(); if (builder != null) { await this.DoMultipartAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, values: values, files: builder.Files); foreach (var file in builder.Files.Where(x => x.ResetPositionTo.HasValue)) { file.Stream.Position = file.ResetPositionTo.Value; } } else { await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)); } } /// /// Creates the interaction response async. /// /// The interaction_id. /// The interaction_token. /// The type. /// The builder. /// A Task. internal async Task CreateInteractionModalResponseAsync(ulong interaction_id, string interaction_token, InteractionResponseType type, DiscordInteractionModalBuilder builder) { + if (builder.ModalComponents.Any(mc => mc.Components.Any(c => c.Type != Enums.ComponentType.InputText))) + throw new NotSupportedException("Can't send any other type then Input Text as Modal Component."); + var pld = new RestInteractionModalResponsePayload { Type = type, Data = new DiscordInteractionApplicationCommandModalCallbackData { Title = builder.Title, CustomId = builder.CustomId, ModalComponents = builder.ModalComponents } }; var values = new Dictionary(); var route = $"{Endpoints.INTERACTIONS}/:interaction_id/:interaction_token{Endpoints.CALLBACK}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { interaction_id, interaction_token }, out var path); var url = Utilities.GetApiUriBuilderFor(path, this.Discord.Configuration).AddParameter("wait", "true").Build(); await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)); } /// /// Gets the original interaction response async. /// /// The application_id. /// The interaction_token. /// A Task. internal Task GetOriginalInteractionResponseAsync(ulong application_id, string interaction_token) => this.GetWebhookMessageAsync(application_id, interaction_token, Endpoints.ORIGINAL, null); /// /// Edits the original interaction response async. /// /// The application_id. /// The interaction_token. /// The builder. /// A Task. internal Task EditOriginalInteractionResponseAsync(ulong application_id, string interaction_token, DiscordWebhookBuilder builder) => this.EditWebhookMessageAsync(application_id, interaction_token, Endpoints.ORIGINAL, builder, null); /// /// Deletes the original interaction response async. /// /// The application_id. /// The interaction_token. /// A Task. internal Task DeleteOriginalInteractionResponseAsync(ulong application_id, string interaction_token) => this.DeleteWebhookMessageAsync(application_id, interaction_token, Endpoints.ORIGINAL, null); /// /// Creates the followup message async. /// /// The application_id. /// The interaction_token. /// The builder. /// A Task. internal async Task CreateFollowupMessageAsync(ulong application_id, string interaction_token, DiscordFollowupMessageBuilder builder) { builder.Validate(); if (builder.Embeds != null) foreach (var embed in builder.Embeds) if (embed.Timestamp != null) embed.Timestamp = embed.Timestamp.Value.ToUniversalTime(); var values = new Dictionary(); var pld = new RestFollowupMessageCreatePayload { Content = builder.Content, IsTTS = builder.IsTTS, Embeds = builder.Embeds, Flags = builder.Flags, Components = builder.Components }; if (builder.Files != null && builder.Files.Count > 0) { ulong file_id = 0; List attachments = new(); foreach (var file in builder.Files) { DiscordAttachment att = new() { Id = file_id, Discord = this.Discord, Description = file.Description, FileName = file.FileName, FileSize = null }; attachments.Add(att); file_id++; } pld.Attachments = attachments; } if (builder.Mentions != null) pld.Mentions = new DiscordMentions(builder.Mentions, builder.Mentions.Any()); if (!string.IsNullOrEmpty(builder.Content) || builder.Embeds?.Count() > 0 || builder.IsTTS == true || builder.Mentions != null || builder.Files?.Count > 0) values["payload_json"] = DiscordJson.SerializeObject(pld); var route = $"{Endpoints.WEBHOOKS}/:application_id/:interaction_token"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new { application_id, interaction_token }, out var path); var url = Utilities.GetApiUriBuilderFor(path, this.Discord.Configuration).AddParameter("wait", "true").Build(); var res = await this.DoMultipartAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, values: values, files: builder.Files).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); foreach (var att in ret._attachments) { att.Discord = this.Discord; } foreach (var file in builder.Files.Where(x => x.ResetPositionTo.HasValue)) { file.Stream.Position = file.ResetPositionTo.Value; } ret.Discord = this.Discord; return ret; } /// /// Gets the followup message async. /// /// The application_id. /// The interaction_token. /// The message_id. /// A Task. internal Task GetFollowupMessageAsync(ulong application_id, string interaction_token, ulong message_id) => this.GetWebhookMessageAsync(application_id, interaction_token, message_id); /// /// Edits the followup message async. /// /// The application_id. /// The interaction_token. /// The message_id. /// The builder. /// A Task. internal Task EditFollowupMessageAsync(ulong application_id, string interaction_token, ulong message_id, DiscordWebhookBuilder builder) => this.EditWebhookMessageAsync(application_id, interaction_token, message_id.ToString(), builder, null); /// /// Deletes the followup message async. /// /// The application_id. /// The interaction_token. /// The message_id. /// A Task. internal Task DeleteFollowupMessageAsync(ulong application_id, string interaction_token, ulong message_id) => this.DeleteWebhookMessageAsync(application_id, interaction_token, message_id); #endregion #region Misc /// /// Gets the current application info async. /// /// A Task. internal Task GetCurrentApplicationInfoAsync() => this.GetApplicationInfoAsync("@me"); /// /// Gets the application info async. /// /// The application_id. /// A Task. internal Task GetApplicationInfoAsync(ulong application_id) => this.GetApplicationInfoAsync(application_id.ToString(CultureInfo.InvariantCulture)); /// /// Gets the application info async. /// /// The application_id. /// A Task. private async Task GetApplicationInfoAsync(string application_id) { var route = $"{Endpoints.OAUTH2}{Endpoints.APPLICATIONS}/:application_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { application_id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); return JsonConvert.DeserializeObject(res.Response); } /// /// Gets the application assets async. /// /// The application. /// A Task. internal async Task> GetApplicationAssetsAsync(DiscordApplication application) { var route = $"{Endpoints.OAUTH2}{Endpoints.APPLICATIONS}/:application_id{Endpoints.ASSETS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { application_id = application.Id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var assets = JsonConvert.DeserializeObject>(res.Response); foreach (var asset in assets) { asset.Discord = application.Discord; asset.Application = application; } return new ReadOnlyCollection(new List(assets)); } /// /// Gets the gateway info async. /// /// A Task. internal async Task GetGatewayInfoAsync() { var headers = Utilities.GetBaseHeaders(); var route = Endpoints.GATEWAY; if (this.Discord.Configuration.TokenType == TokenType.Bot) route += Endpoints.BOT; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route, headers).ConfigureAwait(false); var info = JObject.Parse(res.Response).ToObject(); info.SessionBucket.ResetAfter = DateTimeOffset.UtcNow + TimeSpan.FromMilliseconds(info.SessionBucket.resetAfter); return info; } #endregion #region DCS Internals /// /// Gets the DisCatSharp team. /// > internal async Task GetDisCatSharpTeamAsync() { try { var wc = new WebClient(); var dcs = await wc.DownloadStringTaskAsync(new Uri("https://dcs.aitsys.dev/api/devs/")); var dcs_guild = await wc.DownloadStringTaskAsync(new Uri("https://dcs.aitsys.dev/api/guild/")); var app = JsonConvert.DeserializeObject(dcs); var guild = JsonConvert.DeserializeObject(dcs_guild); var dcst = new DisCatSharpTeam { IconHash = app.Team.IconHash, TeamName = app.Team.Name, PrivacyPolicyUrl = app.PrivacyPolicyUrl, TermsOfServiceUrl = app.TermsOfServiceUrl, RepoUrl = "https://github.com/Aiko-IT-Systems/DisCatSharp", DocsUrl = "https://docs.dcs.aitsys.dev", Id = app.Team.Id, BannerHash = guild.BannerHash, LogoHash = guild.IconHash, GuildId = guild.Id, Guild = guild, SupportInvite = await this.GetInviteAsync("discatsharp", true, true, null) }; List team = new(); DisCatSharpTeamMember owner = new(); foreach (var mb in app.Team.Members.OrderBy(m => m.User.Username)) { var tuser = await this.GetUserAsync(mb.User.Id); var user = mb.User; if (mb.User.Id == 856780995629154305) { owner.Id = user.Id; owner.Username = user.Username; owner.Discriminator = user.Discriminator; owner.AvatarHash = user.AvatarHash; owner.BannerHash = tuser.BannerHash; owner._bannerColor = tuser._bannerColor; team.Add(owner); } else { team.Add(new() { Id = user.Id, Username = user.Username, Discriminator = user.Discriminator, AvatarHash = user.AvatarHash, BannerHash = tuser.BannerHash, _bannerColor = tuser._bannerColor }); } } dcst.Owner = owner; dcst.Developers = team; return dcst; } catch(Exception ex) { this.Discord.Logger.LogDebug(ex.Message); this.Discord.Logger.LogDebug(ex.StackTrace); return null; } } #endregion } }