diff --git a/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs b/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs index 5b8fa1c14..53ca8e217 100644 --- a/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs +++ b/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs @@ -1,1821 +1,1821 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Text; using System.Threading.Tasks; using DisCatSharp.ApplicationCommands.Attributes; using DisCatSharp.ApplicationCommands.EventArgs; using DisCatSharp.Common; using DisCatSharp.Common.Utilities; using DisCatSharp.Entities; using DisCatSharp.Enums; using DisCatSharp.EventArgs; using DisCatSharp.Exceptions; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Newtonsoft.Json; namespace DisCatSharp.ApplicationCommands; /// /// A class that handles slash commands for a client. /// public sealed class ApplicationCommandsExtension : BaseExtension { /// /// A list of methods for top level commands. /// private static List s_commandMethods { get; set; } = new(); /// /// List of groups. /// private static List s_groupCommands { get; set; } = new(); /// /// List of groups with subgroups. /// private static List s_subGroupCommands { get; set; } = new(); /// /// List of context menus. /// private static List s_contextMenuCommands { get; set; } = new(); /// /// List of global commands on discords backend. /// internal static List GlobalDiscordCommands { get; set; } /// /// List of guild commands on discords backend. /// internal static Dictionary> GuildDiscordCommands { get; set; } /// /// Singleton modules. /// private static List s_singletonModules { get; set; } = new(); /// /// List of modules to register. /// private readonly List> _updateList = new(); /// /// Configuration for Discord. /// internal static ApplicationCommandsConfiguration Configuration; /// /// Discord client. /// internal static DiscordClient ClientInternal; /// /// Set to true if anything fails when registering. /// private static bool s_errored { get; set; } /// /// Gets a list of registered commands. The key is the guild id (null if global). /// public static IReadOnlyList>> RegisteredCommands => s_registeredCommands; private static readonly List>> s_registeredCommands = new(); /// /// Gets a list of registered global commands. /// public static IReadOnlyList GlobalCommands => GlobalCommandsInternal; internal static readonly List GlobalCommandsInternal = new(); /// /// Gets a list of registered guild commands mapped by guild id. /// public static IReadOnlyDictionary> GuildCommands => GuildCommandsInternal; internal static readonly Dictionary> GuildCommandsInternal = new(); /// /// Gets the registration count. /// private static int s_registrationCount { get; set; } /// /// Gets the expected count. /// private static int s_expectedCount { get; set; } /// /// Gets the guild ids where the applications.commands scope is missing. /// private IReadOnlyList _missingScopeGuildIds; /// /// Gets whether debug is enabled. /// internal static bool DebugEnabled { get; set; } internal static LogLevel ApplicationCommandsLogLevel => DebugEnabled ? LogLevel.Debug : LogLevel.Trace; /// /// Gets whether check through all guilds is enabled. /// internal static bool CheckAllGuilds { get; set; } /// /// Gets whether the registration check should be manually overriden. /// internal static bool ManOr { get; set; } /// /// Gets whether interactions should be automatically deffered. /// internal static bool AutoDeferEnabled { get; set; } /// /// Initializes a new instance of the class. /// /// The configuration. internal ApplicationCommandsExtension(ApplicationCommandsConfiguration configuration = null) { Configuration = configuration; DebugEnabled = configuration?.DebugStartup ?? false; CheckAllGuilds = configuration?.CheckAllGuilds ?? false; ManOr = configuration?.ManualOverride ?? false; AutoDeferEnabled = configuration?.AutoDefer ?? false; } /// /// Runs setup. /// DO NOT RUN THIS MANUALLY. DO NOT DO ANYTHING WITH THIS. /// /// The client to setup on. protected internal override void Setup(DiscordClient client) { if (this.Client != null) throw new InvalidOperationException("What did I tell you?"); this.Client = client; ClientInternal = client; this._slashError = new AsyncEvent("SLASHCOMMAND_ERRORED", TimeSpan.Zero, null); this._slashExecuted = new AsyncEvent("SLASHCOMMAND_EXECUTED", TimeSpan.Zero, null); this._contextMenuErrored = new AsyncEvent("CONTEXTMENU_ERRORED", TimeSpan.Zero, null); this._contextMenuExecuted = new AsyncEvent("CONTEXTMENU_EXECUTED", TimeSpan.Zero, null); this._applicationCommandsModuleReady = new AsyncEvent("APPLICATION_COMMANDS_MODULE_READY", TimeSpan.Zero, null); this._applicationCommandsModuleStartupFinished = new AsyncEvent("APPLICATION_COMMANDS_MODULE_STARTUP_FINISHED", TimeSpan.Zero, null); this._globalApplicationCommandsRegistered = new AsyncEvent("GLOBAL_COMMANDS_REGISTERED", TimeSpan.Zero, null); this._guildApplicationCommandsRegistered = new AsyncEvent("GUILD_COMMANDS_REGISTERED", TimeSpan.Zero, null); this.Client.GuildDownloadCompleted += async (c, e) => await this.UpdateAsync(); this.Client.InteractionCreated += this.CatchInteractionsOnStartup; this.Client.ContextMenuInteractionCreated += this.CatchContextMenuInteractionsOnStartup; } private async Task CatchInteractionsOnStartup(DiscordClient sender, InteractionCreateEventArgs e) => await e.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().AsEphemeral(true).WithContent("Attention: This application is still starting up. Application commands are unavailable for now.")); private async Task CatchContextMenuInteractionsOnStartup(DiscordClient sender, ContextMenuInteractionCreateEventArgs e) => await e.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().AsEphemeral(true).WithContent("Attention: This application is still starting up. Context menu commands are unavailable for now.")); private void FinishedRegistration() { this.Client.InteractionCreated -= this.CatchInteractionsOnStartup; this.Client.ContextMenuInteractionCreated -= this.CatchContextMenuInteractionsOnStartup; this.Client.InteractionCreated += this.InteractionHandler; this.Client.ContextMenuInteractionCreated += this.ContextMenuHandler; } /// /// Cleans the module for a new start of the bot. /// DO NOT USE IF YOU DON'T KNOW WHAT IT DOES. /// public void CleanModule() { this._updateList.Clear(); s_singletonModules.Clear(); s_errored = false; s_expectedCount = 0; s_registrationCount = 0; s_commandMethods.Clear(); s_groupCommands.Clear(); s_contextMenuCommands.Clear(); s_subGroupCommands.Clear(); s_singletonModules.Clear(); s_registeredCommands.Clear(); GlobalCommandsInternal.Clear(); GuildCommandsInternal.Clear(); } /// /// Registers a command class. /// /// The command class to register. public void RegisterGlobalCommands() where T : ApplicationCommandsModule { if (this.Client.ShardId == 0) this._updateList.Add(new KeyValuePair(null, new ApplicationCommandsModuleConfiguration(typeof(T)))); } /// /// Registers a command class. /// /// The of the command class to register. public void RegisterGlobalCommands(Type type) { 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))); } /// /// Cleans all guild application commands. /// You normally don't need to execute it. /// public async Task CleanGuildCommandsAsync() { foreach (var guild in this.Client.Guilds.Values) { await this.Client.BulkOverwriteGuildApplicationCommandsAsync(guild.Id, Array.Empty()); } } /// /// Cleans the global application commands. /// You normally don't need to execute it. /// public async Task CleanGlobalCommandsAsync() => await this.Client.BulkOverwriteGlobalApplicationCommandsAsync(Array.Empty()); /// /// Registers a command class with permission and translation setup. /// /// The command class to register. /// The guild id to register it on. /// A callback to setup translations with. public void RegisterGuildCommands(ulong guildId, Action translationSetup = null) where T : ApplicationCommandsModule { if (this.Client.ShardId == 0) this._updateList.Add(new KeyValuePair(guildId, new ApplicationCommandsModuleConfiguration(typeof(T), 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 translations with. public void RegisterGuildCommands(Type type, ulong guildId, Action translationSetup = null) { if (!typeof(ApplicationCommandsModule).IsAssignableFrom(type)) throw new ArgumentException("Command classes have to inherit from ApplicationCommandsModule", nameof(type)); //If sharding, only register for shard 0 if (this.Client.ShardId == 0) this._updateList.Add(new KeyValuePair(guildId, new ApplicationCommandsModuleConfiguration(type, translationSetup))); } /// /// Registers a command class with permission setup but without a guild id. /// /// The command class to register. /// A callback to setup translations with. public void RegisterGlobalCommands(Action translationSetup = null) where T : ApplicationCommandsModule - { - if (this.Client.ShardId == 0) - this._updateList.Add(new KeyValuePair(null, new ApplicationCommandsModuleConfiguration(typeof(T), translationSetup))); - } + { + if (this.Client.ShardId == 0) + this._updateList.Add(new KeyValuePair(null, new ApplicationCommandsModuleConfiguration(typeof(T), translationSetup))); + } /// /// Registers a command class with permission setup but without a guild id. /// /// The of the command class to register. /// A callback to setup translations with. public void RegisterGlobalCommands(Type type, Action translationSetup = null) - { - if (!typeof(ApplicationCommandsModule).IsAssignableFrom(type)) - throw new ArgumentException("Command classes have to inherit from ApplicationCommandsModule", nameof(type)); - //If sharding, only register for shard 0 - if (this.Client.ShardId == 0) - this._updateList.Add(new KeyValuePair(null, new ApplicationCommandsModuleConfiguration(type, translationSetup))); - } + { + 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, translationSetup))); + } /// /// Fired when the application commands module is ready. /// public event AsyncEventHandler ApplicationCommandsModuleReady { add => this._applicationCommandsModuleReady.Register(value); remove => this._applicationCommandsModuleReady.Unregister(value); } private AsyncEvent _applicationCommandsModuleReady; /// /// Fired when the application commands modules startup is finished. /// public event AsyncEventHandler ApplicationCommandsModuleStartupFinished { add => this._applicationCommandsModuleStartupFinished.Register(value); remove => this._applicationCommandsModuleStartupFinished.Unregister(value); } private AsyncEvent _applicationCommandsModuleStartupFinished; /// /// Fired when guild commands are registered on a guild. /// public event AsyncEventHandler GuildApplicationCommandsRegistered { add => this._guildApplicationCommandsRegistered.Register(value); remove => this._guildApplicationCommandsRegistered.Unregister(value); } private AsyncEvent _guildApplicationCommandsRegistered; /// /// Fired when the global commands are registered. /// public event AsyncEventHandler GlobalApplicationCommandsRegistered { add => this._globalApplicationCommandsRegistered.Register(value); remove => this._globalApplicationCommandsRegistered.Unregister(value); } private AsyncEvent _globalApplicationCommandsRegistered; /// /// Used for RegisterCommands and the event. /// internal async Task UpdateAsync() { //Only update for shard 0 if (this.Client.ShardId == 0) { GlobalDiscordCommands = new(); GuildDiscordCommands = new(); var commandsPending = this._updateList.Select(x => x.Key).Distinct(); s_expectedCount = commandsPending.Count(); this.Client.Logger.Log(ApplicationCommandsLogLevel, $"Expected Count: {s_expectedCount}"); List failedGuilds = new(); IEnumerable globalCommands = null; globalCommands = await this.Client.GetGlobalApplicationCommandsAsync(Configuration?.EnableLocalization ?? false) ?? null; var guilds = CheckAllGuilds ? this.Client.Guilds?.Keys : this._updateList.Select(x => x.Key)?.Distinct().Where(x => x != null)?.Select(x => x.Value); foreach (var guild in guilds) { IEnumerable commands = null; var unauthorized = false; try { commands = await this.Client.GetGuildApplicationCommandsAsync(guild, Configuration?.EnableLocalization ?? false) ?? null; } catch (UnauthorizedException) { unauthorized = true; } finally { if (!unauthorized && commands != null && commands.Any()) GuildDiscordCommands.Add(guild, commands.ToList()); else if (!unauthorized) GuildDiscordCommands.Add(guild, null); else failedGuilds.Add(guild); } } //Default should be to add the help and slash commands can be added without setting any configuration //so this should still add the default help if (Configuration is null || (Configuration is not null && Configuration.EnableDefaultHelp)) { this._updateList.Add(new KeyValuePair (null, new ApplicationCommandsModuleConfiguration(typeof(DefaultHelpModule)))); commandsPending = this._updateList.Select(x => x.Key).Distinct(); } if (globalCommands != null && globalCommands.Any()) GlobalDiscordCommands.AddRange(globalCommands); foreach (var key in commandsPending.ToList()) { this.Client.Logger.LogInformation(key.HasValue ? $"Registering commands in guild {key.Value}" : "Registering global commands."); await this.RegisterCommands(this._updateList.Where(x => x.Key == key).Select(x => x.Value), key); } this._missingScopeGuildIds = failedGuilds; await this._applicationCommandsModuleReady.InvokeAsync(this, new ApplicationCommandsModuleReadyEventArgs(Configuration?.ServiceProvider) { Handled = true, GuildsWithoutScope = failedGuilds }); } } /// /// Method for registering commands for a target from modules. /// /// The types. /// The optional guild id. private async Task 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>(); //Iterates over all the modules foreach (var config in types) { var type = config.Type; try { var module = type.GetTypeInfo(); var classes = new List(); var ctx = new ApplicationCommandsTranslationContext(type, module.FullName); config.Translations?.Invoke(ctx); //Add module to classes list if it's a group if (module.GetCustomAttribute() != null) { classes.Add(module); } else { //Otherwise add the nested groups classes = module.DeclaredNestedTypes.Where(x => x.GetCustomAttribute() != null).ToList(); } List groupTranslations = null; if (!string.IsNullOrEmpty(ctx.Translations)) { groupTranslations = JsonConvert.DeserializeObject>(ctx.Translations); } var slashGroupsTuple = NestedCommandWorker.ParseSlashGroupsAsync(type, classes, guildId, groupTranslations).Result; if (slashGroupsTuple.applicationCommands != null && slashGroupsTuple.applicationCommands.Any()) updateList.AddRange(slashGroupsTuple.applicationCommands); if (slashGroupsTuple.commandTypeSources != null && slashGroupsTuple.commandTypeSources.Any()) commandTypeSources.AddRange(slashGroupsTuple.commandTypeSources); if (slashGroupsTuple.singletonModules != null && slashGroupsTuple.singletonModules.Any()) s_singletonModules.AddRange(slashGroupsTuple.singletonModules); if (slashGroupsTuple.groupCommands != null && slashGroupsTuple.groupCommands.Any()) groupCommands.AddRange(slashGroupsTuple.groupCommands); if (slashGroupsTuple.subGroupCommands != null && slashGroupsTuple.subGroupCommands.Any()) subGroupCommands.AddRange(slashGroupsTuple.subGroupCommands); //Handles methods and context menus, only if the module isn't a group itself if (module.GetCustomAttribute() == null) { List commandTranslations = null; if (!string.IsNullOrEmpty(ctx.Translations)) { commandTranslations = JsonConvert.DeserializeObject>(ctx.Translations); } //Slash commands var methods = module.DeclaredMethods.Where(x => x.GetCustomAttribute() != null); var slashCommands = CommandWorker.ParseBasicSlashCommandsAsync(type, methods, guildId, commandTranslations).Result; if (slashCommands.applicationCommands != null && slashCommands.applicationCommands.Any()) updateList.AddRange(slashCommands.applicationCommands); if (slashCommands.commandTypeSources != null && slashCommands.commandTypeSources.Any()) commandTypeSources.AddRange(slashCommands.commandTypeSources); if (slashCommands.commandMethods != null && slashCommands.commandMethods.Any()) commandMethods.AddRange(slashCommands.commandMethods); //Context Menus var contextMethods = module.DeclaredMethods.Where(x => x.GetCustomAttribute() != null); var contextCommands = CommandWorker.ParseContextMenuCommands(type, contextMethods, commandTranslations).Result; if (contextCommands.applicationCommands != null && contextCommands.applicationCommands.Any()) updateList.AddRange(contextCommands.applicationCommands); if (contextCommands.commandTypeSources != null && contextCommands.commandTypeSources.Any()) commandTypeSources.AddRange(contextCommands.commandTypeSources); if (contextCommands.contextMenuCommands != null && contextCommands.contextMenuCommands.Any()) contextMenuCommands.AddRange(contextCommands.contextMenuCommands); //Accounts for lifespans if (module.GetCustomAttribute() != null && module.GetCustomAttribute().Lifespan == ApplicationCommandModuleLifespan.Singleton) { s_singletonModules.Add(CreateInstance(module, Configuration?.ServiceProvider)); } } } catch (Exception ex) { if (ex is BadRequestException brex) this.Client.Logger.LogCritical(brex, $"There was an error registering application commands: {brex.JsonMessage}"); else this.Client.Logger.LogCritical(ex, $"There was an error parsing the application commands"); s_errored = true; } } if (!s_errored) { try { List commands = new(); try { if (guildId == null) { if (updateList != null && updateList.Any()) { var regCommands = RegistrationWorker.RegisterGlobalCommandsAsync(updateList).Result; var actualCommands = regCommands.Distinct().ToList(); commands.AddRange(actualCommands); GlobalCommandsInternal.AddRange(actualCommands); } else { foreach (var cmd in GlobalDiscordCommands) { try { await this.Client.DeleteGlobalApplicationCommandAsync(cmd.Id); } catch (NotFoundException) { this.Client.Logger.Log(ApplicationCommandsLogLevel, $"Could not delete global command {cmd.Id}. Please clean up manually"); } } } } else { if (updateList != null && updateList.Any()) { var regCommands = RegistrationWorker.RegisterGuilldCommandsAsync(guildId.Value, updateList).Result; var actualCommands = regCommands.Distinct().ToList(); commands.AddRange(actualCommands); GuildCommandsInternal.Add(guildId.Value, actualCommands); if (this.Client.Guilds.TryGetValue(guildId.Value, out var guild)) { guild.InternalRegisteredApplicationCommands = new(); guild.InternalRegisteredApplicationCommands.AddRange(actualCommands); } } else { foreach (var cmd in GuildDiscordCommands[guildId.Value]) { try { await this.Client.DeleteGuildApplicationCommandAsync(guildId.Value, cmd.Id); } catch (NotFoundException) { this.Client.Logger.Log(ApplicationCommandsLogLevel, $"Could not delete guild command {cmd.Id} in guild {guildId.Value}. Please clean up manually"); } } } } } catch (UnauthorizedException ex) { this.Client.Logger.LogError($"Could not register application commands for guild {guildId}.\nError: {ex.JsonMessage}"); return; } //Creates a guild command if a guild id is specified, otherwise global //Checks against the ids and adds them to the command method lists foreach (var command in commands) { if (commandMethods.GetFirstValueWhere(x => x.Name == command.Name, out var com)) com.CommandId = command.Id; else if (groupCommands.GetFirstValueWhere(x => x.Name == command.Name, out var groupCom)) groupCom.CommandId = command.Id; else if (subGroupCommands.GetFirstValueWhere(x => x.Name == command.Name, out var subCom)) subCom.CommandId = command.Id; else if (contextMenuCommands.GetFirstValueWhere(x => x.Name == command.Name, out var cmCom)) cmCom.CommandId = command.Id; } //Adds to the global lists finally s_commandMethods.AddRange(commandMethods); s_groupCommands.AddRange(groupCommands); s_subGroupCommands.AddRange(subGroupCommands); s_contextMenuCommands.AddRange(contextMenuCommands); s_registeredCommands.Add(new KeyValuePair>(guildId, commands.ToList())); foreach (var command in commandMethods) { var app = types.First(t => t.Type == command.Method.DeclaringType); } this.Client.Logger.Log(ApplicationCommandsLogLevel, $"Expected Count: {s_expectedCount}\nCurrent Count: {s_registrationCount}"); if (guildId.HasValue) { await this._guildApplicationCommandsRegistered.InvokeAsync(this, new GuildApplicationCommandsRegisteredEventArgs(Configuration?.ServiceProvider) { Handled = true, GuildId = guildId.Value, RegisteredCommands = GuildCommandsInternal.Any(c => c.Key == guildId.Value) ? GuildCommandsInternal.FirstOrDefault(c => c.Key == guildId.Value).Value : null }); } else { await this._globalApplicationCommandsRegistered.InvokeAsync(this, new GlobalApplicationCommandsRegisteredEventArgs(Configuration?.ServiceProvider) { Handled = true, RegisteredCommands = GlobalCommandsInternal }); } s_registrationCount++; this.CheckRegistrationStartup(ManOr); } catch (Exception ex) { if (ex is BadRequestException brex) this.Client.Logger.LogCritical(brex, $"There was an error registering application commands: {brex.JsonMessage}"); else this.Client.Logger.LogCritical(ex, $"There was an general error registering application commands"); s_errored = true; } } } private async void CheckRegistrationStartup(bool man = false) { this.Client.Logger.Log(ApplicationCommandsLogLevel, $"Checking counts...\n\nExpected Count: {s_expectedCount}\nCurrent Count: {s_registrationCount}"); if ((s_registrationCount == s_expectedCount) || man) { await this._applicationCommandsModuleStartupFinished.InvokeAsync(this, new ApplicationCommandsModuleStartupFinishedEventArgs(Configuration?.ServiceProvider) { Handled = true, RegisteredGlobalCommands = GlobalCommandsInternal, RegisteredGuildCommands = GuildCommandsInternal, GuildsWithoutScope = this._missingScopeGuildIds }); this.FinishedRegistration(); } } /// /// Interaction handler. /// /// The client. /// The event args. private Task InteractionHandler(DiscordClient client, InteractionCreateEventArgs e) { _ = Task.Run(async () => { if (e.Interaction.Type == InteractionType.ApplicationCommand) { //Creates the context var context = new InteractionContext { Interaction = e.Interaction, Channel = e.Interaction.Channel, Guild = e.Interaction.Guild, User = e.Interaction.User, Client = client, ApplicationCommandsExtension = this, CommandName = e.Interaction.Data.Name, InteractionId = e.Interaction.Id, Token = e.Interaction.Token, Services = Configuration?.ServiceProvider, ResolvedUserMentions = e.Interaction.Data.Resolved?.Users?.Values.ToList(), ResolvedRoleMentions = e.Interaction.Data.Resolved?.Roles?.Values.ToList(), ResolvedChannelMentions = e.Interaction.Data.Resolved?.Channels?.Values.ToList(), ResolvedAttachments = e.Interaction.Data.Resolved?.Attachments?.Values.ToList(), Type = ApplicationCommandType.ChatInput, Locale = e.Interaction.Locale, GuildLocale = e.Interaction.GuildLocale }; try { if (s_errored) throw new InvalidOperationException("Slash commands failed to register properly on startup."); var methods = s_commandMethods.Where(x => x.CommandId == e.Interaction.Data.Id); var groups = s_groupCommands.Where(x => x.CommandId == e.Interaction.Data.Id); var subgroups = s_subGroupCommands.Where(x => x.CommandId == e.Interaction.Data.Id); if (!methods.Any() && !groups.Any() && !subgroups.Any()) throw new InvalidOperationException("A slash command was executed, but no command was registered for it."); if (methods.Any()) { var method = methods.First().Method; var args = await this.ResolveInteractionCommandParameters(e, context, method, e.Interaction.Data.Options); await this.RunCommandAsync(context, method, args); } else if (groups.Any()) { var command = e.Interaction.Data.Options.First(); var method = groups.First().Methods.First(x => x.Key == command.Name).Value; var args = await this.ResolveInteractionCommandParameters(e, context, method, e.Interaction.Data.Options.First().Options); await this.RunCommandAsync(context, method, args); } else if (subgroups.Any()) { var command = e.Interaction.Data.Options.First(); var group = subgroups.First().SubCommands.First(x => x.Name == command.Name); var method = group.Methods.First(x => x.Key == command.Options.First().Name).Value; var args = await this.ResolveInteractionCommandParameters(e, context, method, e.Interaction.Data.Options.First().Options.First().Options); await this.RunCommandAsync(context, method, args); } await this._slashExecuted.InvokeAsync(this, new SlashCommandExecutedEventArgs(this.Client.ServiceProvider) { Context = context }); } catch (Exception ex) { await this._slashError.InvokeAsync(this, new SlashCommandErrorEventArgs(this.Client.ServiceProvider) { Context = context, Exception = ex }); } } else if (e.Interaction.Type == InteractionType.AutoComplete) { if (s_errored) throw new InvalidOperationException("Slash commands failed to register properly on startup."); var methods = s_commandMethods.Where(x => x.CommandId == e.Interaction.Data.Id); var groups = s_groupCommands.Where(x => x.CommandId == e.Interaction.Data.Id); var subgroups = s_subGroupCommands.Where(x => x.CommandId == e.Interaction.Data.Id); if (!methods.Any() && !groups.Any() && !subgroups.Any()) throw new InvalidOperationException("An autocomplete interaction was created, but no command was registered for it."); try { if (methods.Any()) { var focusedOption = e.Interaction.Data.Options.First(o => o.Focused); var method = methods.First().Method; var option = method.GetParameters().Skip(1).First(p => p.GetCustomAttribute().Name == focusedOption.Name); var provider = option.GetCustomAttribute().ProviderType; var providerMethod = provider.GetMethod(nameof(IAutocompleteProvider.Provider)); var providerInstance = Activator.CreateInstance(provider); var context = new AutocompleteContext { Interaction = e.Interaction, Client = this.Client, Services = Configuration?.ServiceProvider, ApplicationCommandsExtension = this, Guild = e.Interaction.Guild, Channel = e.Interaction.Channel, User = e.Interaction.User, Options = e.Interaction.Data.Options.ToList(), FocusedOption = focusedOption, Locale = e.Interaction.Locale, GuildLocale = e.Interaction.GuildLocale }; var choices = await (Task>) providerMethod.Invoke(providerInstance, new[] { context }); await e.Interaction.CreateResponseAsync(InteractionResponseType.AutoCompleteResult, new DiscordInteractionResponseBuilder().AddAutoCompleteChoices(choices)); } else if (groups.Any()) { var command = e.Interaction.Data.Options.First(); var group = groups.First().Methods.First(x => x.Key == command.Name).Value; var focusedOption = command.Options.First(o => o.Focused); var option = group.GetParameters().Skip(1).First(p => p.GetCustomAttribute().Name == focusedOption.Name); var provider = option.GetCustomAttribute().ProviderType; var providerMethod = provider.GetMethod(nameof(IAutocompleteProvider.Provider)); var providerInstance = Activator.CreateInstance(provider); var context = new AutocompleteContext { Interaction = e.Interaction, Services = Configuration?.ServiceProvider, ApplicationCommandsExtension = this, Guild = e.Interaction.Guild, Channel = e.Interaction.Channel, User = e.Interaction.User, Options = command.Options.ToList(), FocusedOption = focusedOption, Locale = e.Interaction.Locale, GuildLocale = e.Interaction.GuildLocale }; var choices = await (Task>) providerMethod.Invoke(providerInstance, new[] { context }); await e.Interaction.CreateResponseAsync(InteractionResponseType.AutoCompleteResult, new DiscordInteractionResponseBuilder().AddAutoCompleteChoices(choices)); } else if (subgroups.Any()) - { - var command = e.Interaction.Data.Options.First(); - var group = subgroups.First().SubCommands.First(x => x.Name == command.Name).Methods.First(x => x.Key == command.Options.First().Name).Value; - - var focusedOption = command.Options.First().Options.First(o => o.Focused); - - var option = group.GetParameters().Skip(1).First(p => p.GetCustomAttribute().Name == focusedOption.Name); - var provider = option.GetCustomAttribute().ProviderType; - var providerMethod = provider.GetMethod(nameof(IAutocompleteProvider.Provider)); - var providerInstance = Activator.CreateInstance(provider); - - var context = new AutocompleteContext - { - Interaction = e.Interaction, - Services = Configuration?.ServiceProvider, - ApplicationCommandsExtension = this, - Guild = e.Interaction.Guild, - Channel = e.Interaction.Channel, - User = e.Interaction.User, - Options = command.Options.First().Options.ToList(), - FocusedOption = focusedOption, + { + var command = e.Interaction.Data.Options.First(); + var group = subgroups.First().SubCommands.First(x => x.Name == command.Name).Methods.First(x => x.Key == command.Options.First().Name).Value; + + var focusedOption = command.Options.First().Options.First(o => o.Focused); + + var option = group.GetParameters().Skip(1).First(p => p.GetCustomAttribute().Name == focusedOption.Name); + var provider = option.GetCustomAttribute().ProviderType; + var providerMethod = provider.GetMethod(nameof(IAutocompleteProvider.Provider)); + var providerInstance = Activator.CreateInstance(provider); + + var context = new AutocompleteContext + { + Interaction = e.Interaction, + Services = Configuration?.ServiceProvider, + ApplicationCommandsExtension = this, + Guild = e.Interaction.Guild, + Channel = e.Interaction.Channel, + User = e.Interaction.User, + Options = command.Options.First().Options.ToList(), + FocusedOption = focusedOption, Locale = e.Interaction.Locale, GuildLocale = e.Interaction.GuildLocale }; - var choices = await (Task>) providerMethod.Invoke(providerInstance, new[] { context }); - await e.Interaction.CreateResponseAsync(InteractionResponseType.AutoCompleteResult, new DiscordInteractionResponseBuilder().AddAutoCompleteChoices(choices)); - } + var choices = await (Task>) providerMethod.Invoke(providerInstance, new[] { context }); + await e.Interaction.CreateResponseAsync(InteractionResponseType.AutoCompleteResult, new DiscordInteractionResponseBuilder().AddAutoCompleteChoices(choices)); + } } catch (Exception ex) { this.Client.Logger.LogError(ex, "Error in autocomplete interaction"); } } }); return Task.CompletedTask; } /// /// Context menu handler. /// /// The client. /// The event args. private Task ContextMenuHandler(DiscordClient client, ContextMenuInteractionCreateEventArgs e) { _ = Task.Run(async () => { //Creates the context var context = new ContextMenuContext { Interaction = e.Interaction, Channel = e.Interaction.Channel, Client = client, Services = Configuration?.ServiceProvider, CommandName = e.Interaction.Data.Name, ApplicationCommandsExtension = this, Guild = e.Interaction.Guild, InteractionId = e.Interaction.Id, User = e.Interaction.User, Token = e.Interaction.Token, TargetUser = e.TargetUser, TargetMessage = e.TargetMessage, Type = e.Type, Locale = e.Interaction.Locale, GuildLocale = e.Interaction.GuildLocale }; try { if (s_errored) throw new InvalidOperationException("Context menus failed to register properly on startup."); //Gets the method for the command var method = s_contextMenuCommands.FirstOrDefault(x => x.CommandId == e.Interaction.Data.Id); if (method == null) throw new InvalidOperationException("A context menu was executed, but no command was registered for it."); await this.RunCommandAsync(context, method.Method, new[] { context }); await this._contextMenuExecuted.InvokeAsync(this, new ContextMenuExecutedEventArgs(this.Client.ServiceProvider) { Context = context }); } catch (Exception ex) { await this._contextMenuErrored.InvokeAsync(this, new ContextMenuErrorEventArgs(this.Client.ServiceProvider) { Context = context, Exception = ex }); } }); return Task.CompletedTask; } /// /// Runs a command. /// /// The base context. /// The method info. /// The arguments. [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0066:Convert switch statement to expression", Justification = "")] internal async Task RunCommandAsync(BaseContext context, MethodInfo method, IEnumerable args) { object classInstance; //Accounts for lifespans var moduleLifespan = (method.DeclaringType.GetCustomAttribute() != null ? method.DeclaringType.GetCustomAttribute()?.Lifespan : ApplicationCommandModuleLifespan.Transient) ?? ApplicationCommandModuleLifespan.Transient; switch (moduleLifespan) { case ApplicationCommandModuleLifespan.Scoped: //Accounts for static methods and adds DI classInstance = method.IsStatic ? ActivatorUtilities.CreateInstance(Configuration?.ServiceProvider.CreateScope().ServiceProvider, method.DeclaringType) : CreateInstance(method.DeclaringType, Configuration?.ServiceProvider.CreateScope().ServiceProvider); break; case ApplicationCommandModuleLifespan.Transient: //Accounts for static methods and adds DI classInstance = method.IsStatic ? ActivatorUtilities.CreateInstance(Configuration?.ServiceProvider, method.DeclaringType) : CreateInstance(method.DeclaringType, Configuration?.ServiceProvider); break; //If singleton, gets it from the singleton list case ApplicationCommandModuleLifespan.Singleton: classInstance = s_singletonModules.First(x => ReferenceEquals(x.GetType(), method.DeclaringType)); break; default: throw new Exception($"An unknown {nameof(ApplicationCommandModuleLifespanAttribute)} scope was specified on command {context.CommandName}"); } ApplicationCommandsModule module = null; if (classInstance is ApplicationCommandsModule mod) module = mod; // Slash commands if (context is InteractionContext slashContext) { await this.RunPreexecutionChecksAsync(method, slashContext); var shouldExecute = await (module?.BeforeSlashExecutionAsync(slashContext) ?? Task.FromResult(true)); if (shouldExecute) { if (AutoDeferEnabled) await context.CreateResponseAsync(InteractionResponseType.DeferredChannelMessageWithSource); await (Task)method.Invoke(classInstance, args.ToArray()); await (module?.AfterSlashExecutionAsync(slashContext) ?? Task.CompletedTask); } } // Context menus if (context is ContextMenuContext contextMenuContext) { await this.RunPreexecutionChecksAsync(method, contextMenuContext); var shouldExecute = await (module?.BeforeContextMenuExecutionAsync(contextMenuContext) ?? Task.FromResult(true)); if (shouldExecute) { if (AutoDeferEnabled) await context.CreateResponseAsync(InteractionResponseType.DeferredChannelMessageWithSource); await (Task)method.Invoke(classInstance, args.ToArray()); await (module?.AfterContextMenuExecutionAsync(contextMenuContext) ?? Task.CompletedTask); } } } /// /// Property injection /// /// The type. /// The services. internal static object CreateInstance(Type t, IServiceProvider services) { var ti = t.GetTypeInfo(); var constructors = ti.DeclaredConstructors .Where(xci => xci.IsPublic) .ToArray(); if (constructors.Length != 1) throw new ArgumentException("Specified type does not contain a public constructor or contains more than one public constructor."); var constructor = constructors[0]; var constructorArgs = constructor.GetParameters(); var args = new object[constructorArgs.Length]; if (constructorArgs.Length != 0 && services == null) throw new InvalidOperationException("Dependency collection needs to be specified for parameterized constructors."); // inject via constructor if (constructorArgs.Length != 0) for (var i = 0; i < args.Length; i++) args[i] = services.GetRequiredService(constructorArgs[i].ParameterType); var moduleInstance = Activator.CreateInstance(t, args); // inject into properties var props = t.GetRuntimeProperties().Where(xp => xp.CanWrite && xp.SetMethod != null && !xp.SetMethod.IsStatic && xp.SetMethod.IsPublic); foreach (var prop in props) { if (prop.GetCustomAttribute() != null) continue; var service = services.GetService(prop.PropertyType); if (service == null) continue; prop.SetValue(moduleInstance, service); } // inject into fields var fields = t.GetRuntimeFields().Where(xf => !xf.IsInitOnly && !xf.IsStatic && xf.IsPublic); foreach (var field in fields) { if (field.GetCustomAttribute() != null) continue; var service = services.GetService(field.FieldType); if (service == null) continue; field.SetValue(moduleInstance, service); } return moduleInstance; } /// /// Resolves the slash command parameters. /// /// The event arguments. /// The interaction context. /// The method info. /// The options. private async Task> ResolveInteractionCommandParameters(InteractionCreateEventArgs e, InteractionContext context, MethodInfo method, IEnumerable options) { var args = new List { context }; var parameters = method.GetParameters().Skip(1); for (var i = 0; i < parameters.Count(); i++) { var parameter = parameters.ElementAt(i); //Accounts for optional arguments without values given if (parameter.IsOptional && (options == null || (!options?.Any(x => x.Name == parameter.GetCustomAttribute().Name.ToLower()) ?? true))) args.Add(parameter.DefaultValue); else { var option = options.Single(x => x.Name == parameter.GetCustomAttribute().Name.ToLower()); if (parameter.ParameterType == typeof(string)) args.Add(option.Value.ToString()); else if (parameter.ParameterType.IsEnum) args.Add(Enum.Parse(parameter.ParameterType, (string)option.Value)); else if (parameter.ParameterType == typeof(long) || parameter.ParameterType == typeof(long?)) args.Add((long?)option.Value); else if (parameter.ParameterType == typeof(bool) || parameter.ParameterType == typeof(bool?)) args.Add((bool?)option.Value); else if (parameter.ParameterType == typeof(double) || parameter.ParameterType == typeof(double?)) args.Add((double?)option.Value); else if (parameter.ParameterType == typeof(int) || parameter.ParameterType == typeof(int?)) args.Add((int?)option.Value); else if (parameter.ParameterType == typeof(DiscordAttachment)) { //Checks through resolved if (e.Interaction.Data.Resolved.Attachments != null && e.Interaction.Data.Resolved.Attachments.TryGetValue((ulong)option.Value, out var attachment)) args.Add(attachment); else args.Add(new DiscordAttachment() { Id = (ulong)option.Value, Discord = this.Client.ApiClient.Discord }); } else if (parameter.ParameterType == typeof(DiscordUser)) { //Checks through resolved if (e.Interaction.Data.Resolved.Members != null && e.Interaction.Data.Resolved.Members.TryGetValue((ulong)option.Value, out var member)) args.Add(member); else if (e.Interaction.Data.Resolved.Users != null && e.Interaction.Data.Resolved.Users.TryGetValue((ulong)option.Value, out var user)) args.Add(user); else args.Add(await this.Client.GetUserAsync((ulong)option.Value)); } else if (parameter.ParameterType == typeof(DiscordChannel)) { //Checks through resolved if (e.Interaction.Data.Resolved.Channels != null && e.Interaction.Data.Resolved.Channels.TryGetValue((ulong)option.Value, out var channel)) args.Add(channel); else args.Add(e.Interaction.Guild.GetChannel((ulong)option.Value)); } else if (parameter.ParameterType == typeof(DiscordRole)) { //Checks through resolved if (e.Interaction.Data.Resolved.Roles != null && e.Interaction.Data.Resolved.Roles.TryGetValue((ulong)option.Value, out var role)) args.Add(role); else args.Add(e.Interaction.Guild.GetRole((ulong)option.Value)); } else if (parameter.ParameterType == typeof(SnowflakeObject)) { //Checks through resolved if (e.Interaction.Data.Resolved.Roles != null && e.Interaction.Data.Resolved.Roles.TryGetValue((ulong)option.Value, out var role)) args.Add(role); else if (e.Interaction.Data.Resolved.Members != null && e.Interaction.Data.Resolved.Members.TryGetValue((ulong)option.Value, out var member)) args.Add(member); else if (e.Interaction.Data.Resolved.Users != null && e.Interaction.Data.Resolved.Users.TryGetValue((ulong)option.Value, out var user)) args.Add(user); else throw new ArgumentException("Error resolving mentionable option."); } else throw new ArgumentException($"Error resolving interaction."); } } return args; } /// /// Runs the pre-execution checks. /// /// The method info. /// The base context. private async Task RunPreexecutionChecksAsync(MethodInfo method, BaseContext context) { if (context is InteractionContext ctx) { //Gets all attributes from parent classes as well and stuff var attributes = new List(); attributes.AddRange(method.GetCustomAttributes(true)); attributes.AddRange(method.DeclaringType.GetCustomAttributes()); if (method.DeclaringType.DeclaringType != null) { attributes.AddRange(method.DeclaringType.DeclaringType.GetCustomAttributes()); if (method.DeclaringType.DeclaringType.DeclaringType != null) { attributes.AddRange(method.DeclaringType.DeclaringType.DeclaringType.GetCustomAttributes()); } } var dict = new Dictionary(); foreach (var att in attributes) { //Runs the check and adds the result to a list var result = await att.ExecuteChecksAsync(ctx); dict.Add(att, result); } //Checks if any failed, and throws an exception if (dict.Any(x => x.Value == false)) throw new SlashExecutionChecksFailedException { FailedChecks = dict.Where(x => x.Value == false).Select(x => x.Key).ToList() }; } if (context is ContextMenuContext cMctx) { var attributes = new List(); attributes.AddRange(method.GetCustomAttributes(true)); attributes.AddRange(method.DeclaringType.GetCustomAttributes()); if (method.DeclaringType.DeclaringType != null) { attributes.AddRange(method.DeclaringType.DeclaringType.GetCustomAttributes()); if (method.DeclaringType.DeclaringType.DeclaringType != null) { attributes.AddRange(method.DeclaringType.DeclaringType.DeclaringType.GetCustomAttributes()); } } var dict = new Dictionary(); foreach (var att in attributes) { //Runs the check and adds the result to a list var result = await att.ExecuteChecksAsync(cMctx); dict.Add(att, result); } //Checks if any failed, and throws an exception if (dict.Any(x => x.Value == false)) throw new ContextMenuExecutionChecksFailedException { FailedChecks = dict.Where(x => x.Value == false).Select(x => x.Key).ToList() }; } } /// /// Gets the choice attributes from choice provider. /// /// The custom attributes. /// The optional guild id private static async Task> GetChoiceAttributesFromProvider(IEnumerable customAttributes, ulong? guildId = null) { var choices = new List(); foreach (var choiceProviderAttribute in customAttributes) { var method = choiceProviderAttribute.ProviderType.GetMethod(nameof(IChoiceProvider.Provider)); if (method == null) throw new ArgumentException("ChoiceProviders must inherit from IChoiceProvider."); else { var instance = Activator.CreateInstance(choiceProviderAttribute.ProviderType); // Abstract class offers more properties that can be set if (choiceProviderAttribute.ProviderType.IsSubclassOf(typeof(ChoiceProvider))) { choiceProviderAttribute.ProviderType.GetProperty(nameof(ChoiceProvider.GuildId)) ?.SetValue(instance, guildId); choiceProviderAttribute.ProviderType.GetProperty(nameof(ChoiceProvider.Services)) ?.SetValue(instance, Configuration.ServiceProvider); } //Gets the choices from the method var result = await (Task>)method.Invoke(instance, null); if (result.Any()) { choices.AddRange(result); } } } return choices; } /// /// Gets the choice attributes from enum parameter. /// /// The enum parameter. private static List GetChoiceAttributesFromEnumParameter(Type enumParam) { var choices = new List(); foreach (Enum enumValue in Enum.GetValues(enumParam)) { choices.Add(new DiscordApplicationCommandOptionChoice(enumValue.GetName(), enumValue.ToString())); } return choices; } /// /// Gets the parameter type. /// /// The type. private static ApplicationCommandOptionType GetParameterType(Type type) { var parameterType = type == typeof(string) ? ApplicationCommandOptionType.String : type == typeof(long) || type == typeof(long?) || type == typeof(int) || type == typeof(int?) ? ApplicationCommandOptionType.Integer : type == typeof(bool) || type == typeof(bool?) ? ApplicationCommandOptionType.Boolean : type == typeof(double) || type == typeof(double?) ? ApplicationCommandOptionType.Number : type == typeof(DiscordAttachment) ? ApplicationCommandOptionType.Attachment : type == typeof(DiscordChannel) ? ApplicationCommandOptionType.Channel : type == typeof(DiscordUser) ? ApplicationCommandOptionType.User : type == typeof(DiscordRole) ? ApplicationCommandOptionType.Role : type == typeof(SnowflakeObject) ? ApplicationCommandOptionType.Mentionable : type.IsEnum ? ApplicationCommandOptionType.String : throw new ArgumentException("Cannot convert type! Argument types must be string, int, long, bool, double, DiscordChannel, DiscordUser, DiscordRole, SnowflakeObject, DiscordAttachment or an Enum."); return parameterType; } /// /// Gets the choice attributes from parameter. /// /// The choice attributes. private static List GetChoiceAttributesFromParameter(IEnumerable choiceAttributes) => !choiceAttributes.Any() ? null : choiceAttributes.Select(att => new DiscordApplicationCommandOptionChoice(att.Name, att.Value)).ToList(); /// /// Parses the parameters. /// /// The parameters. /// The optional guild id. internal static async Task> ParseParametersAsync(ParameterInfo[] parameters, ulong? guildId) { var options = new List(); foreach (var parameter in parameters) { //Gets the attribute var optionAttribute = parameter.GetCustomAttribute(); if (optionAttribute == null) throw new ArgumentException("Arguments must have the Option attribute!"); var minimumValue = parameter.GetCustomAttribute()?.Value ?? null; var maximumValue = parameter.GetCustomAttribute()?.Value ?? null; var autocompleteAttribute = parameter.GetCustomAttribute(); if (optionAttribute.Autocomplete && autocompleteAttribute == null) throw new ArgumentException("Autocomplete options must have the Autocomplete attribute!"); if (!optionAttribute.Autocomplete && autocompleteAttribute != null) throw new ArgumentException("Setting an autocomplete provider requires the option to have autocomplete set to true!"); //Sets the type var type = parameter.ParameterType; var parameterType = GetParameterType(type); //Handles choices //From attributes var choices = GetChoiceAttributesFromParameter(parameter.GetCustomAttributes()); //From enums if (parameter.ParameterType.IsEnum) { choices = GetChoiceAttributesFromEnumParameter(parameter.ParameterType); } //From choice provider var choiceProviders = parameter.GetCustomAttributes(); if (choiceProviders.Any()) { choices = await GetChoiceAttributesFromProvider(choiceProviders, guildId); } var channelTypes = parameter.GetCustomAttribute()?.ChannelTypes ?? null; options.Add(new DiscordApplicationCommandOption(optionAttribute.Name, optionAttribute.Description, parameterType, !parameter.IsOptional, choices, null, channelTypes, optionAttribute.Autocomplete, minimumValue, maximumValue)); } return options; } /// /// Refreshes your commands, used for refreshing choice providers or applying commands registered after the ready event on the discord client. /// Should only be run on the slash command extension linked to shard 0 if sharding. /// Not recommended and should be avoided since it can make slash commands be unresponsive for a while. /// public async Task RefreshCommandsAsync() { s_commandMethods.Clear(); s_groupCommands.Clear(); s_subGroupCommands.Clear(); s_registeredCommands.Clear(); s_contextMenuCommands.Clear(); GlobalDiscordCommands.Clear(); GuildDiscordCommands.Clear(); GuildCommandsInternal.Clear(); GlobalCommandsInternal.Clear(); GlobalDiscordCommands = null; GuildDiscordCommands = null; s_errored = false; if (Configuration != null && Configuration.EnableDefaultHelp) { this._updateList.RemoveAll(x => x.Value.Type == typeof(DefaultHelpModule)); } await this.UpdateAsync(); } /// /// Fires when the execution of a slash command fails. /// public event AsyncEventHandler SlashCommandErrored { add => this._slashError.Register(value); remove => this._slashError.Unregister(value); } private AsyncEvent _slashError; /// /// Fires when the execution of a slash command is successful. /// public event AsyncEventHandler SlashCommandExecuted { add => this._slashExecuted.Register(value); remove => this._slashExecuted.Unregister(value); } private AsyncEvent _slashExecuted; /// /// Fires when the execution of a context menu fails. /// public event AsyncEventHandler ContextMenuErrored { add => this._contextMenuErrored.Register(value); remove => this._contextMenuErrored.Unregister(value); } private AsyncEvent _contextMenuErrored; /// /// Fire when the execution of a context menu is successful. /// public event AsyncEventHandler ContextMenuExecuted { add => this._contextMenuExecuted.Register(value); remove => this._contextMenuExecuted.Unregister(value); } private AsyncEvent _contextMenuExecuted; } /// /// Holds configuration data for setting up an application command. /// internal class ApplicationCommandsModuleConfiguration { /// /// The type of the command module. /// public Type Type { get; } /// /// The translation setup. /// public Action Translations { get; } /// /// Creates a new command configuration. /// /// The type of the command module. /// The translation setup callback. public ApplicationCommandsModuleConfiguration(Type type, Action translations = null) { this.Type = type; this.Translations = translations; } } /// /// Links a command to its original command module. /// internal class ApplicationCommandSourceLink { /// /// The command. /// public DiscordApplicationCommand ApplicationCommand { get; set; } /// /// The base/root module the command is contained in. /// public Type RootCommandContainerType { get; set; } /// /// The direct group the command is contained in. /// public Type CommandContainerType { get; set; } } /// /// The command method. /// internal class CommandMethod { /// /// Gets or sets the command id. /// public ulong CommandId { get; set; } /// /// Gets or sets the name. /// public string Name { get; set; } /// /// Gets or sets the method. /// public MethodInfo Method { get; set; } } /// /// The group command. /// internal class GroupCommand { /// /// Gets or sets the command id. /// public ulong CommandId { get; set; } /// /// Gets or sets the name. /// public string Name { get; set; } /// /// Gets or sets the methods. /// public List> Methods { get; set; } = null; } /// /// The sub group command. /// internal class SubGroupCommand { /// /// Gets or sets the command id. /// public ulong CommandId { get; set; } /// /// Gets or sets the name. /// public string Name { get; set; } /// /// Gets or sets the sub commands. /// public List SubCommands { get; set; } = new(); } /// /// The context menu command. /// internal class ContextMenuCommand { /// /// Gets or sets the command id. /// public ulong CommandId { get; set; } /// /// Gets or sets the name. /// public string Name { get; set; } /// /// Gets or sets the method. /// public MethodInfo Method { get; set; } } #region Default Help /// /// Represents the default help module. /// internal class DefaultHelpModule : ApplicationCommandsModule { public class DefaultHelpAutoCompleteProvider : IAutocompleteProvider { public async Task> Provider(AutocompleteContext context) { var options = new List(); IEnumerable slashCommands = null; var globalCommandsTask = context.Client.GetGlobalApplicationCommandsAsync(); if (context.Guild != null) { var guildCommandsTask = context.Client.GetGuildApplicationCommandsAsync(context.Guild.Id); await Task.WhenAll(globalCommandsTask, guildCommandsTask); slashCommands = globalCommandsTask.Result.Concat(guildCommandsTask.Result) .Where(ac => !ac.Name.Equals("help", StringComparison.OrdinalIgnoreCase)) .GroupBy(ac => ac.Name).Select(x => x.First()) .Where(ac => ac.Name.StartsWith(context.Options[0].Value.ToString(), StringComparison.OrdinalIgnoreCase)) .ToList(); } else { await Task.WhenAll(globalCommandsTask); slashCommands = globalCommandsTask.Result .Where(ac => !ac.Name.Equals("help", StringComparison.OrdinalIgnoreCase)) .GroupBy(ac => ac.Name).Select(x => x.First()) .Where(ac => ac.Name.StartsWith(context.Options[0].Value.ToString(), StringComparison.OrdinalIgnoreCase)) .ToList(); } foreach (var sc in slashCommands.Take(25)) { options.Add(new DiscordApplicationCommandAutocompleteChoice(sc.Name, sc.Name.Trim())); } return options.AsEnumerable(); } } public class DefaultHelpAutoCompleteLevelOneProvider : IAutocompleteProvider { public async Task> Provider(AutocompleteContext context) { var options = new List(); IEnumerable slashCommands = null; var globalCommandsTask = context.Client.GetGlobalApplicationCommandsAsync(); if (context.Guild != null) { var guildCommandsTask = context.Client.GetGuildApplicationCommandsAsync(context.Guild.Id); await Task.WhenAll(globalCommandsTask, guildCommandsTask); slashCommands = globalCommandsTask.Result.Concat(guildCommandsTask.Result) .Where(ac => !ac.Name.Equals("help", StringComparison.OrdinalIgnoreCase)) .GroupBy(ac => ac.Name).Select(x => x.First()); } else { await Task.WhenAll(globalCommandsTask); slashCommands = globalCommandsTask.Result .Where(ac => !ac.Name.Equals("help", StringComparison.OrdinalIgnoreCase)) .GroupBy(ac => ac.Name).Select(x => x.First()); } var command = slashCommands.FirstOrDefault(ac => ac.Name.Equals(context.Options[0].Value.ToString().Trim(),StringComparison.OrdinalIgnoreCase)); if (command is null || command.Options is null) { options.Add(new DiscordApplicationCommandAutocompleteChoice("no_options_for_this_command", "no_options_for_this_command")); } else { var opt = command.Options.Where(c => c.Type is ApplicationCommandOptionType.SubCommandGroup or ApplicationCommandOptionType.SubCommand && c.Name.StartsWith(context.Options[1].Value.ToString(), StringComparison.InvariantCultureIgnoreCase)).ToList(); foreach (var option in opt.Take(25)) { options.Add(new DiscordApplicationCommandAutocompleteChoice(option.Name, option.Name.Trim())); } } return options.AsEnumerable(); } } public class DefaultHelpAutoCompleteLevelTwoProvider : IAutocompleteProvider { public async Task> Provider(AutocompleteContext context) { var options = new List(); IEnumerable slashCommands = null; var globalCommandsTask = context.Client.GetGlobalApplicationCommandsAsync(); if (context.Guild != null) { var guildCommandsTask = context.Client.GetGuildApplicationCommandsAsync(context.Guild.Id); await Task.WhenAll(globalCommandsTask, guildCommandsTask); slashCommands = globalCommandsTask.Result.Concat(guildCommandsTask.Result) .Where(ac => !ac.Name.Equals("help", StringComparison.OrdinalIgnoreCase)) .GroupBy(ac => ac.Name).Select(x => x.First()); } else { await Task.WhenAll(globalCommandsTask); slashCommands = globalCommandsTask.Result .Where(ac => !ac.Name.Equals("help", StringComparison.OrdinalIgnoreCase)) .GroupBy(ac => ac.Name).Select(x => x.First()); } var command = slashCommands.FirstOrDefault(ac => ac.Name.Equals(context.Options[0].Value.ToString().Trim(), StringComparison.OrdinalIgnoreCase)); if (command.Options is null) { options.Add(new DiscordApplicationCommandAutocompleteChoice("no_options_for_this_command", "no_options_for_this_command")); return options.AsEnumerable(); } var foundCommand = command.Options.FirstOrDefault(op => op.Name.Equals(context.Options[1].Value.ToString().Trim(), StringComparison.OrdinalIgnoreCase)); if (foundCommand is null || foundCommand.Options is null) { options.Add(new DiscordApplicationCommandAutocompleteChoice("no_options_for_this_command", "no_options_for_this_command")); } else { var opt = foundCommand.Options.Where(x => x.Type == ApplicationCommandOptionType.SubCommand && x.Name.StartsWith(context.Options[2].Value.ToString(), StringComparison.OrdinalIgnoreCase)).ToList(); foreach (var option in opt.Take(25)) { options.Add(new DiscordApplicationCommandAutocompleteChoice(option.Name, option.Name.Trim())); } } return options.AsEnumerable(); } } [SlashCommand("help", "Displays command help")] internal async Task DefaultHelpAsync(InteractionContext ctx, [Autocomplete(typeof(DefaultHelpAutoCompleteProvider))] [Option("option_one", "top level command to provide help for", true)] string commandName, [Autocomplete(typeof(DefaultHelpAutoCompleteLevelOneProvider))] [Option("option_two", "subgroup or command to provide help for", true)] string commandOneName = null, [Autocomplete(typeof(DefaultHelpAutoCompleteLevelTwoProvider))] [Option("option_three", "command to provide help for", true)] string commandTwoName = null) { var globalCommandsTask = ctx.Client.GetGlobalApplicationCommandsAsync(); var guildCommandsTask= ctx.Client.GetGuildApplicationCommandsAsync(ctx.Guild.Id); await Task.WhenAll(globalCommandsTask, guildCommandsTask); var applicationCommands = globalCommandsTask.Result.Concat(guildCommandsTask.Result) .Where(ac => !ac.Name.Equals("help", StringComparison.OrdinalIgnoreCase)) .GroupBy(ac => ac.Name).Select(x => x.First()) .ToList(); if (applicationCommands.Count < 1) { await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder() .WithContent($"There are no slash commands for guild {ctx.Guild.Name}").AsEphemeral(true)); return; } if (commandTwoName is not null && !commandTwoName.Equals("no_options_for_this_command")) { var commandsWithSubCommands = applicationCommands.FindAll(ac => ac.Options is not null && ac.Options.Any(op => op.Type == ApplicationCommandOptionType.SubCommandGroup)); var cmdParent = commandsWithSubCommands.FirstOrDefault(cm => cm.Options.Any(op => op.Name.Equals(commandOneName))).Options .FirstOrDefault(opt => opt.Name.Equals(commandOneName,StringComparison.OrdinalIgnoreCase)); var cmd = cmdParent.Options.FirstOrDefault(op => op.Name.Equals(commandTwoName,StringComparison.OrdinalIgnoreCase)); var discordEmbed = new DiscordEmbedBuilder { Title = "Help", Description = $"{Formatter.InlineCode(cmd.Name)}: {cmd.Description ?? "No description provided."}" }; if (cmd.Options is not null) { var commandOptions = cmd.Options.ToList(); var sb = new StringBuilder(); foreach (var option in commandOptions) sb.Append('`').Append(option.Name).Append(" (").Append(")`: ").Append(option.Description ?? "No description provided.").Append('\n'); sb.Append('\n'); discordEmbed.AddField(new DiscordEmbedField("Arguments", sb.ToString().Trim())); } await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().AddEmbed(discordEmbed).AsEphemeral(true)); } else if (commandOneName is not null && commandTwoName is null && !commandOneName.Equals("no_options_for_this_command")) { var commandsWithOptions = applicationCommands.FindAll(ac => ac.Options is not null && ac.Options.All(op => op.Type == ApplicationCommandOptionType.SubCommand)); var subCommandParent = commandsWithOptions.FirstOrDefault(cm => cm.Name.Equals(commandName,StringComparison.OrdinalIgnoreCase)); var subCommand = subCommandParent.Options.FirstOrDefault(op => op.Name.Equals(commandOneName,StringComparison.OrdinalIgnoreCase)); var discordEmbed = new DiscordEmbedBuilder { Title = "Help", Description = $"{Formatter.InlineCode(subCommand.Name)}: {subCommand.Description ?? "No description provided."}" }; if (subCommand.Options is not null) { var commandOptions = subCommand.Options.ToList(); var sb = new StringBuilder(); foreach (var option in commandOptions) sb.Append('`').Append(option.Name).Append(" (").Append(")`: ").Append(option.Description ?? "No description provided.").Append('\n'); sb.Append('\n'); discordEmbed.AddField(new DiscordEmbedField("Arguments", sb.ToString().Trim())); } await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().AddEmbed(discordEmbed).AsEphemeral(true)); } else { var command = applicationCommands.FirstOrDefault(cm => cm.Name.Equals(commandName, StringComparison.OrdinalIgnoreCase)); if (command is null) { await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder() .WithContent($"No command called {commandName} in guild {ctx.Guild.Name}").AsEphemeral(true)); return; } var discordEmbed = new DiscordEmbedBuilder { Title = "Help", Description = $"{Formatter.InlineCode(command.Name)}: {command.Description ?? "No description provided."}" }; if (command.Options is not null) { var commandOptions = command.Options.ToList(); var sb = new StringBuilder(); foreach (var option in commandOptions) sb.Append('`').Append(option.Name).Append(" (").Append(")`: ").Append(option.Description ?? "No description provided.").Append('\n'); sb.Append('\n'); discordEmbed.AddField(new DiscordEmbedField("Arguments", sb.ToString().Trim())); } await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().AddEmbed(discordEmbed).AsEphemeral(true)); } } } #endregion diff --git a/DisCatSharp.ApplicationCommands/Attributes/ContextMenu/ContextMenuAttribute.cs b/DisCatSharp.ApplicationCommands/Attributes/ContextMenu/ContextMenuAttribute.cs index 00c18d671..810bb9ba3 100644 --- a/DisCatSharp.ApplicationCommands/Attributes/ContextMenu/ContextMenuAttribute.cs +++ b/DisCatSharp.ApplicationCommands/Attributes/ContextMenu/ContextMenuAttribute.cs @@ -1,88 +1,88 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using DisCatSharp.Enums; namespace DisCatSharp.ApplicationCommands; /// /// Marks this method as a context menu. /// [AttributeUsage(AttributeTargets.Method)] public sealed class ContextMenuAttribute : Attribute { /// /// Gets the name of this context menu. /// public string Name { get; internal set; } /// /// Gets the type of this context menu. /// public ApplicationCommandType Type { get; internal set; } /// /// Gets the commands needed permissions. /// public Permissions? DefaultMemberPermissions { get; internal set; } /// /// Gets whether the command can be used in direct messages. /// internal bool? DmPermission { get; set; } /// /// Marks this method as a context menu. /// /// The type of the context menu. /// The name of the context menu. public ContextMenuAttribute(ApplicationCommandType type, string name) { if (type == ApplicationCommandType.ChatInput) throw new ArgumentException("Context menus cannot be of type ChatInput (Slash)."); - + this.Type = type; this.Name = name; this.DefaultMemberPermissions = null; this.DmPermission = null; } /// /// Marks this method as a context menu. /// /// The type of the context menu. /// The name of the context menu. /// The default member permissions. public ContextMenuAttribute(ApplicationCommandType type, string name, long defaultMemberPermissions) { if (type == ApplicationCommandType.ChatInput) throw new ArgumentException("Context menus cannot be of type ChatInput (Slash)."); this.Type = type; this.Name = name; this.DefaultMemberPermissions = (Permissions)defaultMemberPermissions; this.DmPermission = null; } } diff --git a/DisCatSharp.ApplicationCommands/Checks/ApplicationCommandEqualityChecks.cs b/DisCatSharp.ApplicationCommands/Checks/ApplicationCommandEqualityChecks.cs index 94add1c26..d6817a28e 100644 --- a/DisCatSharp.ApplicationCommands/Checks/ApplicationCommandEqualityChecks.cs +++ b/DisCatSharp.ApplicationCommands/Checks/ApplicationCommandEqualityChecks.cs @@ -1,276 +1,278 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. +using System.Collections.Generic; using System.Linq; using DisCatSharp.Entities; using DisCatSharp.Enums; -using System.Collections.Generic; using Microsoft.Extensions.Logging; + using Newtonsoft.Json; namespace DisCatSharp.ApplicationCommands; internal static class ApplicationCommandEqualityChecks { /// /// Whether two application commands are equal. /// /// Source command. /// Command to check against. internal static bool IsEqualTo(this DiscordApplicationCommand ac1, DiscordApplicationCommand targetApplicationCommand) { if (targetApplicationCommand is null || ac1 is null) return false; DiscordApplicationCommand sourceApplicationCommand = new( ac1.Name, ac1.Description, ac1.Options, ac1.Type, ac1.NameLocalizations, ac1.DescriptionLocalizations ); ApplicationCommandsExtension.ClientInternal.Logger.Log(ApplicationCommandsExtension.ApplicationCommandsLogLevel, $"[AC Change Check] Command {ac1.Name}\n\n[{JsonConvert.SerializeObject(sourceApplicationCommand)},{JsonConvert.SerializeObject(targetApplicationCommand)}]\n\n"); return ac1.Type == targetApplicationCommand.Type && sourceApplicationCommand.SoftEqual(targetApplicationCommand, ac1.Type, ApplicationCommandsExtension.Configuration?.EnableLocalization ?? false); } /// /// Checks softly whether two s are the same. /// Excluding id, application id and version here. /// /// Source application command. /// Application command to check against. /// The application command type. /// Whether localization is enabled. internal static bool SoftEqual(this DiscordApplicationCommand source, DiscordApplicationCommand target, ApplicationCommandType type, bool localizationEnabled = false) { return localizationEnabled ? type switch { ApplicationCommandType.ChatInput => DeepEqual(source, target, localizationEnabled), _ => (source.Name == target.Name) && (source.Type == target.Type) && (source.NameLocalizations == target.NameLocalizations) && (source.DefaultMemberPermissions == target.DefaultMemberPermissions) && (source.DmPermission == target.DmPermission) } - : type switch { + : type switch + { ApplicationCommandType.ChatInput => DeepEqual(source, target), _ => (source.Name == target.Name) && (source.Type == target.Type) && (source.DefaultMemberPermissions == target.DefaultMemberPermissions) && (source.DmPermission == target.DmPermission) }; } /// /// Checks deeply whether two s are the same. /// Excluding id, application id and version here. /// /// Source application command. /// Application command to check against. /// Whether localization is enabled. internal static bool DeepEqual(DiscordApplicationCommand source, DiscordApplicationCommand target, bool localizationEnabled = false) { var rootCheck = (source.Name == target.Name) && (source.Description == target.Description) && (source.Type == target.Type) && (source.DefaultMemberPermissions == target.DefaultMemberPermissions) && (source.DmPermission == target.DmPermission); if (localizationEnabled) rootCheck = rootCheck && (source.NameLocalizations == target.NameLocalizations) && (source.DescriptionLocalizations == target.DescriptionLocalizations); if (source.Options == null && target.Options == null) return rootCheck; else if ((source.Options != null && target.Options == null) || (source.Options == null && target.Options != null)) return false; else if (source.Options.Any(o => o.Type == ApplicationCommandOptionType.SubCommandGroup) && target.Options.Any(o => o.Type == ApplicationCommandOptionType.SubCommandGroup)) { List minimalSourceOptions = new(); List minimalTargetOptions = new(); foreach (var option in source.Options) { List minimalSubSourceOptions = new(); foreach (var subOption in option.Options) { List minimalSubSubSourceOptions = null; if (subOption.Options != null) { minimalSubSubSourceOptions = new(); foreach (var subSubOption in subOption.Options) { minimalSubSubSourceOptions.Add(new DiscordApplicationCommandOption( subSubOption.Name, subSubOption.Description, subSubOption.Type, subSubOption.Required ?? false, subSubOption.Choices, null, subSubOption.ChannelTypes, subSubOption.AutoComplete ?? false, subSubOption.MinimumValue, subSubOption.MaximumValue, localizationEnabled ? subSubOption.NameLocalizations : null, localizationEnabled ? subSubOption.DescriptionLocalizations : null )); } minimalSubSourceOptions.Add(new DiscordApplicationCommandOption( subOption.Name, subOption.Description, subOption.Type, options: minimalSubSubSourceOptions, nameLocalizations: localizationEnabled ? subOption.NameLocalizations : null, descriptionLocalizations: localizationEnabled ? subOption.DescriptionLocalizations : null )); } } minimalSourceOptions.Add(new DiscordApplicationCommandOption( option.Name, option.Description, option.Type, options: minimalSubSourceOptions, nameLocalizations: localizationEnabled ? option.NameLocalizations : null, descriptionLocalizations: localizationEnabled ? option.DescriptionLocalizations : null )); } foreach (var option in target.Options) { List minimalSubTargetOptions = new(); foreach (var subOption in option.Options) { List minimalSubSubTargetOptions = null; if (subOption.Options != null && subOption.Options.Any()) { minimalSubSubTargetOptions = new(); foreach (var subSubOption in subOption.Options) { minimalSubSubTargetOptions.Add(new DiscordApplicationCommandOption( subSubOption.Name, subSubOption.Description, subSubOption.Type, subSubOption.Required ?? false, subSubOption.Choices, null, subSubOption.ChannelTypes, subSubOption.AutoComplete ?? false, subSubOption.MinimumValue, subSubOption.MaximumValue, localizationEnabled ? subSubOption.NameLocalizations : null, localizationEnabled ? subSubOption.DescriptionLocalizations : null )); } minimalSubTargetOptions.Add(new DiscordApplicationCommandOption( subOption.Name, subOption.Description, subOption.Type, options: minimalSubSubTargetOptions, nameLocalizations: localizationEnabled ? subOption.NameLocalizations : null, descriptionLocalizations: localizationEnabled ? subOption.DescriptionLocalizations : null )); } } minimalTargetOptions.Add(new DiscordApplicationCommandOption( option.Name, option.Description, option.Type, options: minimalSubTargetOptions, nameLocalizations: localizationEnabled ? option.NameLocalizations : null, descriptionLocalizations: localizationEnabled ? option.DescriptionLocalizations : null )); } return rootCheck && JsonConvert.SerializeObject(minimalSourceOptions) == JsonConvert.SerializeObject(minimalTargetOptions); } else if (source.Options.Any(o => o.Type == ApplicationCommandOptionType.SubCommand) && target.Options.Any(o => o.Type == ApplicationCommandOptionType.SubCommand)) { List minimalSourceOptions = new(); List minimalTargetOptions = new(); foreach (var option in source.Options) { List minimalSubSourceOptions =null; if (option.Options != null) { minimalSubSourceOptions = new(); foreach (var subOption in option.Options) { minimalSubSourceOptions.Add(new DiscordApplicationCommandOption( subOption.Name, subOption.Description, subOption.Type, subOption.Required ?? false, subOption.Choices, null, subOption.ChannelTypes, subOption.AutoComplete ?? false, subOption.MinimumValue, subOption.MaximumValue, localizationEnabled ? subOption.NameLocalizations : null, localizationEnabled ? subOption.DescriptionLocalizations : null )); } } minimalSourceOptions.Add(new DiscordApplicationCommandOption( option.Name, option.Description, option.Type, options: minimalSubSourceOptions, nameLocalizations: localizationEnabled ? option.NameLocalizations : null, descriptionLocalizations: localizationEnabled ? option.DescriptionLocalizations : null )); } foreach (var option in target.Options) { List minimalSubTargetOptions = null; if (option.Options != null && option.Options.Any()) { minimalSubTargetOptions = new(); foreach (var subOption in option.Options) { minimalSubTargetOptions.Add(new DiscordApplicationCommandOption( subOption.Name, subOption.Description, subOption.Type, subOption.Required ?? false, subOption.Choices, null, subOption.ChannelTypes, subOption.AutoComplete ?? false, subOption.MinimumValue, subOption.MaximumValue, localizationEnabled ? subOption.NameLocalizations : null, localizationEnabled ? subOption.DescriptionLocalizations : null )); } } minimalTargetOptions.Add(new DiscordApplicationCommandOption( option.Name, option.Description, option.Type, options: minimalSubTargetOptions, nameLocalizations: localizationEnabled ? option.NameLocalizations : null, descriptionLocalizations: localizationEnabled ? option.DescriptionLocalizations : null )); } return rootCheck && JsonConvert.SerializeObject(minimalSourceOptions) == JsonConvert.SerializeObject(minimalTargetOptions); } else { List minimalSourceOptions = new(); List minimalTargetOptions = new(); foreach (var option in source.Options) minimalSourceOptions.Add(new DiscordApplicationCommandOption( option.Name, option.Description, option.Type, option.Required ?? false, option.Choices, null, option.ChannelTypes, option.AutoComplete ?? false, option.MinimumValue, option.MaximumValue, localizationEnabled ? option.NameLocalizations : null, localizationEnabled ? option.DescriptionLocalizations : null )); foreach (var option in target.Options) minimalTargetOptions.Add(new DiscordApplicationCommandOption( option.Name, option.Description, option.Type, option.Required ?? false, option.Choices, null, option.ChannelTypes, option.AutoComplete ?? false, option.MinimumValue, option.MaximumValue, localizationEnabled ? option.NameLocalizations : null, localizationEnabled ? option.DescriptionLocalizations : null )); return rootCheck && JsonConvert.SerializeObject(minimalSourceOptions) == JsonConvert.SerializeObject(minimalTargetOptions); } } } diff --git a/DisCatSharp.CommandsNext/Converters/EntityConverters.cs b/DisCatSharp.CommandsNext/Converters/EntityConverters.cs index acc4ea3a4..09872254d 100644 --- a/DisCatSharp.CommandsNext/Converters/EntityConverters.cs +++ b/DisCatSharp.CommandsNext/Converters/EntityConverters.cs @@ -1,456 +1,455 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Globalization; using System.Linq; -using System.Text.RegularExpressions; using System.Threading.Tasks; using DisCatSharp.Common.RegularExpressions; using DisCatSharp.Entities; namespace DisCatSharp.CommandsNext.Converters; /// /// Represents a discord user converter. /// public class DiscordUserConverter : IArgumentConverter { /// /// Converts a string. /// /// The string to convert. /// The command context. async Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) { if (ulong.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var uid)) { var result = await ctx.Client.GetUserAsync(uid).ConfigureAwait(false); return Optional.FromNullable(result); } var m = DiscordRegEx.User.Match(value); if (m.Success && ulong.TryParse(m.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out uid)) { var result = await ctx.Client.GetUserAsync(uid).ConfigureAwait(false); return Optional.FromNullable(result); } var cs = ctx.Config.CaseSensitive; if (!cs) value = value.ToLowerInvariant(); var di = value.IndexOf('#'); var un = di != -1 ? value[..di] : value; var dv = di != -1 ? value[(di + 1)..] : null; var us = ctx.Client.Guilds.Values .SelectMany(xkvp => xkvp.Members.Values) .Where(xm => (cs ? xm.Username : xm.Username.ToLowerInvariant()) == un && ((dv != null && xm.Discriminator == dv) || dv == null)); var usr = us.FirstOrDefault(); return Optional.FromNullable(usr); } } /// /// Represents a discord member converter. /// public class DiscordMemberConverter : IArgumentConverter { /// /// Converts a string. /// /// The string to convert. /// The command context. async Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) { if (ctx.Guild == null) return Optional.None; if (ulong.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var uid)) { var result = await ctx.Guild.GetMemberAsync(uid).ConfigureAwait(false); return Optional.FromNullable(result); } var m = DiscordRegEx.User.Match(value); if (m.Success && ulong.TryParse(m.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out uid)) { var result = await ctx.Guild.GetMemberAsync(uid).ConfigureAwait(false); return Optional.FromNullable(result); } var searchResult = await ctx.Guild.SearchMembersAsync(value).ConfigureAwait(false); if (searchResult.Any()) return Optional.Some(searchResult.First()); var cs = ctx.Config.CaseSensitive; if (!cs) value = value.ToLowerInvariant(); var di = value.IndexOf('#'); var un = di != -1 ? value[..di] : value; var dv = di != -1 ? value[(di + 1)..] : null; var us = ctx.Guild.Members.Values .Where(xm => ((cs ? xm.Username : xm.Username.ToLowerInvariant()) == un && ((dv != null && xm.Discriminator == dv) || dv == null)) || (cs ? xm.Nickname : xm.Nickname?.ToLowerInvariant()) == value); return Optional.FromNullable(us.FirstOrDefault()); } } /// /// Represents a discord channel converter. /// public class DiscordChannelConverter : IArgumentConverter { /// /// Converts a string. /// /// The string to convert. /// The command context. async Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) { if (ulong.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var cid)) { var result = await ctx.Client.GetChannelAsync(cid).ConfigureAwait(false); return Optional.FromNullable(result); } var m = DiscordRegEx.Channel.Match(value); if (m.Success && ulong.TryParse(m.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out cid)) { var result = await ctx.Client.GetChannelAsync(cid).ConfigureAwait(false); return Optional.FromNullable(result); } var cs = ctx.Config.CaseSensitive; if (!cs) value = value.ToLowerInvariant(); var chn = ctx.Guild?.Channels.Values.FirstOrDefault(xc => (cs ? xc.Name : xc.Name.ToLowerInvariant()) == value); return Optional.FromNullable(chn); } } /// /// Represents a discord thread channel converter. /// public class DiscordThreadChannelConverter : IArgumentConverter { /// /// Converts a string. /// /// The string to convert. /// The command context. async Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) { if (ulong.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var tid)) { var result = await ctx.Client.GetThreadAsync(tid).ConfigureAwait(false); return Optional.FromNullable(result); } var m = DiscordRegEx.Channel.Match(value); if (m.Success && ulong.TryParse(m.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out tid)) { var result = await ctx.Client.GetThreadAsync(tid).ConfigureAwait(false); return Optional.FromNullable(result); } var cs = ctx.Config.CaseSensitive; if (!cs) value = value.ToLowerInvariant(); var tchn = ctx.Guild?.Threads.Values.FirstOrDefault(xc => (cs ? xc.Name : xc.Name.ToLowerInvariant()) == value); return Optional.FromNullable(tchn); } } /// /// Represents a discord role converter. /// public class DiscordRoleConverter : IArgumentConverter { /// /// Converts a string. /// /// The string to convert. /// The command context. Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) { if (ctx.Guild == null) return Task.FromResult(Optional.None); if (ulong.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var rid)) { var result = ctx.Guild.GetRole(rid); return Task.FromResult(Optional.FromNullable(result)); } var m = DiscordRegEx.Role.Match(value); if (m.Success && ulong.TryParse(m.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out rid)) { var result = ctx.Guild.GetRole(rid); return Task.FromResult(Optional.FromNullable(result)); } var cs = ctx.Config.CaseSensitive; if (!cs) value = value.ToLowerInvariant(); var rol = ctx.Guild.Roles.Values.FirstOrDefault(xr => (cs ? xr.Name : xr.Name.ToLowerInvariant()) == value); return Task.FromResult(Optional.FromNullable(rol)); } } /// /// Represents a discord guild converter. /// public class DiscordGuildConverter : IArgumentConverter { /// /// Converts a string. /// /// The string to convert. /// The command context. Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) { if (ulong.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var gid)) { return ctx.Client.Guilds.TryGetValue(gid, out var result) ? Task.FromResult(Optional.Some(result)) : Task.FromResult(Optional.None); } var cs = ctx.Config.CaseSensitive; if (!cs) value = value?.ToLowerInvariant(); var gld = ctx.Client.Guilds.Values.FirstOrDefault(xg => (cs ? xg.Name : xg.Name.ToLowerInvariant()) == value); return Task.FromResult(Optional.FromNullable(gld)); } } /// /// Represents a discord invite converter. /// public class DiscordInviteConverter : IArgumentConverter { /// /// Converts a string. /// /// The string to convert. /// The command context. async Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) { var m = DiscordRegEx.Invite.Match(value); if (m.Success) { ulong? eventId = ulong.TryParse(m.Groups["event"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var eid) ? eid : null; var result = await ctx.Client.GetInviteByCodeAsync(m.Groups["code"].Value, scheduledEventId: eventId).ConfigureAwait(false); return Optional.FromNullable(result); } var inv = await ctx.Client.GetInviteByCodeAsync(value); return Optional.FromNullable(inv); } } /// /// Represents a discord message converter. /// public class DiscordMessageConverter : IArgumentConverter { /// /// Converts a string. /// /// The string to convert. /// The command context. async Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) { if (string.IsNullOrWhiteSpace(value)) return Optional.None; var msguri = value.StartsWith("<") && value.EndsWith(">") ? value[1..^1] : value; ulong mid; if (Uri.TryCreate(msguri, UriKind.Absolute, out var uri)) { var uripath = DiscordRegEx.MessageLink.Match(uri.AbsoluteUri); if (!uripath.Success || !ulong.TryParse(uripath.Groups["channel"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var cid) || !ulong.TryParse(uripath.Groups["message"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out mid)) return Optional.None; var chn = await ctx.Client.GetChannelAsync(cid).ConfigureAwait(false); if (chn == null) return Optional.None; var msg = await chn.GetMessageAsync(mid).ConfigureAwait(false); return Optional.FromNullable(msg); } if (ulong.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out mid)) { var result = await ctx.Channel.GetMessageAsync(mid).ConfigureAwait(false); return Optional.FromNullable(result); } return Optional.None; } } /// /// Represents a discord scheduled event converter. /// public class DiscordScheduledEventConverter : IArgumentConverter { /// /// Converts a string. /// /// The string to convert. /// The command context. async Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) { if (string.IsNullOrWhiteSpace(value)) return Optional.None; var msguri = value.StartsWith("<") && value.EndsWith(">") ? value[1..^1] : value; ulong seid; if (Uri.TryCreate(msguri, UriKind.Absolute, out var uri)) { var uripath = DiscordRegEx.Event.Match(uri.AbsoluteUri); if (uripath.Success - && ulong.TryParse(uripath.Groups["guild"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, - out var gid) - && ulong.TryParse(uripath.Groups["event"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, - out seid)) + && ulong.TryParse(uripath.Groups["guild"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, + out var gid) + && ulong.TryParse(uripath.Groups["event"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, + out seid)) { var guild = await ctx.Client.GetGuildAsync(gid).ConfigureAwait(false); if (guild == null) return Optional.None; var ev = await guild.GetScheduledEventAsync(seid).ConfigureAwait(false); return Optional.FromNullable(ev); } try { var invite = await ctx.CommandsNext.ConvertArgument(value, ctx).ConfigureAwait(false); return Optional.FromNullable(invite.GuildScheduledEvent); } catch { return Optional.None; } } if (ulong.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out seid)) { var result = await ctx.Guild.GetScheduledEventAsync(seid).ConfigureAwait(false); return Optional.FromNullable(result); } return Optional.None; } } /// /// Represents a discord emoji converter. /// public class DiscordEmojiConverter : IArgumentConverter { /// /// Converts a string. /// /// The string to convert. /// The command context. Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) { if (DiscordEmoji.TryFromUnicode(ctx.Client, value, out var emoji)) { var result = emoji; return Task.FromResult(Optional.Some(result)); } var m = DiscordRegEx.Emoji.Match(value); if (m.Success) { var sid = m.Groups["id"].Value; var name = m.Groups["name"].Value; var anim = m.Groups["animated"].Success; return !ulong.TryParse(sid, NumberStyles.Integer, CultureInfo.InvariantCulture, out var id) ? Task.FromResult(Optional.None) : DiscordEmoji.TryFromGuildEmote(ctx.Client, id, out emoji) ? Task.FromResult(Optional.Some(emoji)) : Task.FromResult(Optional.Some(new DiscordEmoji { Discord = ctx.Client, Id = id, Name = name, IsAnimated = anim, RequiresColons = true, IsManaged = false })); } return Task.FromResult(Optional.None); } } /// /// Represents a discord color converter. /// public class DiscordColorConverter : IArgumentConverter { /// /// Converts a string. /// /// The string to convert. /// The command context. Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) { var m = CommonRegEx.HexColorString.Match(value); if (m.Success && int.TryParse(m.Groups[1].Value, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var clr)) return Task.FromResult(Optional.Some(clr)); m = CommonRegEx.RgbColorString.Match(value); if (m.Success) { var p1 = byte.TryParse(m.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var r); var p2 = byte.TryParse(m.Groups[2].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var g); var p3 = byte.TryParse(m.Groups[3].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var b); return !(p1 && p2 && p3) ? Task.FromResult(Optional.None) : Task.FromResult(Optional.Some(new DiscordColor(r, g, b))); } return Task.FromResult(Optional.None); } } diff --git a/DisCatSharp.Common/Types/Serialization/ComplexDecomposer.cs b/DisCatSharp.Common/Types/Serialization/ComplexDecomposer.cs index 16764cb8e..dc7f30976 100644 --- a/DisCatSharp.Common/Types/Serialization/ComplexDecomposer.cs +++ b/DisCatSharp.Common/Types/Serialization/ComplexDecomposer.cs @@ -1,115 +1,114 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Generic; -using System.Linq; using System.Numerics; namespace DisCatSharp.Common.Serialization; /// /// Decomposes numbers into tuples (arrays of 2). /// public sealed class ComplexDecomposer : IDecomposer { /// /// Gets the t complex. /// private static Type s_complex { get; } = typeof(Complex); /// /// Gets the t double array. /// private static Type s_doubleArray { get; } = typeof(double[]); /// /// Gets the t double enumerable. /// private static Type s_doubleEnumerable { get; } = typeof(IEnumerable); /// /// Gets the t object array. /// private static Type s_objectArray { get; } = typeof(object[]); /// /// Gets the t object enumerable. /// private static Type s_objectEnumerable { get; } = typeof(IEnumerable); /// public bool CanDecompose(Type t) => t == s_complex; /// public bool CanRecompose(Type t) => t == s_doubleArray || t == s_objectArray || s_doubleEnumerable.IsAssignableFrom(t) || s_objectEnumerable.IsAssignableFrom(t); /// public bool TryDecompose(object obj, Type tobj, out object decomposed, out Type tdecomposed) { decomposed = null; tdecomposed = s_doubleArray; if (tobj != s_complex || obj is not Complex c) return false; decomposed = new[] { c.Real, c.Imaginary }; return true; } /// public bool TryRecompose(object obj, Type tobj, Type trecomposed, out object recomposed) { recomposed = null; if (trecomposed != s_complex) return false; // ie if (s_doubleEnumerable.IsAssignableFrom(tobj) && obj is IEnumerable ied) { if (!ied.TryFirstTwo(out var values)) return false; var (real, imag) = values; recomposed = new Complex(real, imag); return true; } // ie if (s_objectEnumerable.IsAssignableFrom(tobj) && obj is IEnumerable ieo) { if (!ieo.TryFirstTwo(out var values)) return false; var (real, imag) = values; if (real is not double dreal || imag is not double dimag) return false; recomposed = new Complex(dreal, dimag); return true; } return false; } } diff --git a/DisCatSharp.Interactivity/EventHandling/Components/ComponentPaginator.cs b/DisCatSharp.Interactivity/EventHandling/Components/ComponentPaginator.cs index 50915b8b1..1f8798141 100644 --- a/DisCatSharp.Interactivity/EventHandling/Components/ComponentPaginator.cs +++ b/DisCatSharp.Interactivity/EventHandling/Components/ComponentPaginator.cs @@ -1,182 +1,182 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Generic; using System.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 DiscordFollowupMessageBuilder { 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.VoiceNext/Entities/AudioSender.cs b/DisCatSharp.VoiceNext/Entities/AudioSender.cs index 5de144ba6..9a817e17e 100644 --- a/DisCatSharp.VoiceNext/Entities/AudioSender.cs +++ b/DisCatSharp.VoiceNext/Entities/AudioSender.cs @@ -1,167 +1,167 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using DisCatSharp.Entities; using DisCatSharp.VoiceNext.Codec; namespace DisCatSharp.VoiceNext.Entities; /// /// The audio sender. /// internal class AudioSender : IDisposable { // starting the counter a full wrap ahead handles an edge case where the VERY first packets // we see are right around the wraparound line. private ulong _sequenceBase = 1 << 16; - + private SequenceWrapState _currentSequenceWrapState = SequenceWrapState.AssumeNextHighSequenceIsOutOfOrder; private enum SequenceWrapState { Normal, AssumeNextLowSequenceIsOverflow, AssumeNextHighSequenceIsOutOfOrder, } /// /// Gets the s s r c. /// public uint Ssrc { get; } - + /// /// Gets the id. /// public ulong Id => this.User?.Id ?? 0; - + /// /// Gets the decoder. /// public OpusDecoder Decoder { get; } - + /// /// Gets or sets the user. /// public DiscordUser User { get; set; } = null; - + /// /// Gets or sets the last sequence. /// public ulong? LastTrueSequence { get; set; } = null; - + /// /// Initializes a new instance of the class. /// /// The ssrc. /// The decoder. public AudioSender(uint ssrc, OpusDecoder decoder) { this.Ssrc = ssrc; this.Decoder = decoder; } /// /// Disposes . /// public void Dispose() => this.Decoder?.Dispose(); /// /// Accepts the 16-bit sequence number from the next RTP header in the associated stream and /// uses heuristics to (attempt to) convert it into a 64-bit counter that takes into account /// overflow wrapping around to zero. /// /// This method only works properly if it is called for every sequence number that we /// see in the stream. /// /// /// The 16-bit sequence number from the next RTP header. /// /// /// Our best-effort guess of the value that would /// have been, if the server had given us a 64-bit integer instead of a 16-bit one. /// public ulong GetTrueSequenceAfterWrapping(ushort originalSequence) { // section off a smallish zone at either end of the 16-bit integer range. whenever the // sequence numbers creep into the higher zone, we start keeping an eye out for when // sequence numbers suddenly start showing up in the lower zone. we expect this to mean // that the sequence numbers overflowed and wrapped around. there's a bit of a balance // when determining an appropriate size for the buffer zone: if it's too small, then a // brief (but recoverable) network interruption could cause us to miss the lead-up to // the overflow. on the other hand, if it's too large, then such a network interruption // could cause us to misinterpret a normal sequence for one that's out-of-order. // // at 20 milliseconds per packet, 3,000 packets means that the buffer zone is one minute // on either side. in other words, as long as we're getting packets delivered within a // minute or so of when they should be, the 64-bit sequence numbers coming out of this // method will be perfectly consistent with reality. const ushort OVERFLOW_BUFFER_ZONE = 3_000; const ushort LOW_THRESHOLD = OVERFLOW_BUFFER_ZONE; const ushort HIGH_THRESHOLD = ushort.MaxValue - OVERFLOW_BUFFER_ZONE; ulong wrappingAdjustment = 0; switch (this._currentSequenceWrapState) { case SequenceWrapState.Normal when originalSequence > HIGH_THRESHOLD: // we were going about our business up to this point. the sequence numbers have // gotten a bit high, so let's start looking out for any sequence numbers that // are suddenly WAY lower than where they are right now. this._currentSequenceWrapState = SequenceWrapState.AssumeNextLowSequenceIsOverflow; break; case SequenceWrapState.AssumeNextLowSequenceIsOverflow when originalSequence < LOW_THRESHOLD: // we had seen some sequence numbers that got a bit high, and now we see this // sequence number that's WAY lower than before. this is a classic sign that // the sequence numbers have wrapped around. in order to present a consistently // increasing "true" sequence number, add another 65,536 and keep counting. if // we see another high sequence number in the near future, assume that it's a // packet coming in out of order. this._sequenceBase += 1 << 16; this._currentSequenceWrapState = SequenceWrapState.AssumeNextHighSequenceIsOutOfOrder; break; case SequenceWrapState.AssumeNextHighSequenceIsOutOfOrder when originalSequence > HIGH_THRESHOLD: // we're seeing some high sequence numbers EITHER at the beginning of the stream // OR very close to the time when we saw some very low sequence numbers. in the // latter case, it happened because the packets came in out of order, right when // the sequence numbers wrapped around. in the former case, we MIGHT be in the // same kind of situation (we can't tell yet), so we err on the side of caution // and burn a full cycle before we start counting so that we can handle both // cases with the exact same adjustment. wrappingAdjustment = 1 << 16; break; case SequenceWrapState.AssumeNextHighSequenceIsOutOfOrder when originalSequence > LOW_THRESHOLD: // EITHER we're at the very beginning of the stream OR very close to the time // when we saw some very low sequence numbers. either way, we're out of the // zones where we should consider very low sequence numbers to come AFTER very // high ones, so we can go back to normal now. this._currentSequenceWrapState = SequenceWrapState.Normal; break; } return this._sequenceBase + originalSequence - wrappingAdjustment; } } diff --git a/DisCatSharp/Clients/DiscordClient.cs b/DisCatSharp/Clients/DiscordClient.cs index 95e4850b4..c77266d64 100644 --- a/DisCatSharp/Clients/DiscordClient.cs +++ b/DisCatSharp/Clients/DiscordClient.cs @@ -1,1296 +1,1296 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Globalization; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using DisCatSharp.Common.Utilities; using DisCatSharp.Entities; using DisCatSharp.Enums; using DisCatSharp.EventArgs; using DisCatSharp.Exceptions; using DisCatSharp.Net; using DisCatSharp.Net.Abstractions; using DisCatSharp.Net.Models; using DisCatSharp.Net.Serialization; using Microsoft.Extensions.Logging; using Newtonsoft.Json.Linq; namespace DisCatSharp; /// /// A Discord API wrapper. /// public sealed partial class DiscordClient : BaseDiscordClient { #region Internal Fields/Properties internal bool IsShard = false; /// /// Gets the message cache. /// internal RingBuffer MessageCache { get; } private List _extensions = new(); private StatusUpdate _status; /// /// Gets the connection lock. /// private readonly ManualResetEventSlim _connectionLock = new(true); #endregion #region Public Fields/Properties /// /// Gets the gateway protocol version. /// public int GatewayVersion { get; internal set; } /// /// Gets the gateway session information for this client. /// public GatewayInfo GatewayInfo { get; internal set; } /// /// Gets the gateway URL. /// public Uri GatewayUri { get; internal set; } /// /// Gets the total number of shards the bot is connected to. /// public int ShardCount => this.GatewayInfo != null ? this.GatewayInfo.ShardCount : this.Configuration.ShardCount; /// /// Gets the currently connected shard ID. /// public int ShardId => this.Configuration.ShardId; /// /// Gets the intents configured for this client. /// public DiscordIntents Intents => this.Configuration.Intents; /// /// Gets a dictionary of guilds that this client is in. The dictionary's key is the guild ID. Note that the /// guild objects in this dictionary will not be filled in if the specific guilds aren't available (the /// or events haven't been fired yet) /// public override IReadOnlyDictionary Guilds { get; } internal ConcurrentDictionary GuildsInternal = new(); /// /// Gets the websocket latency for this client. /// public int Ping => Volatile.Read(ref this._ping); private int _ping; /// /// Gets the collection of presences held by this client. /// public IReadOnlyDictionary Presences => this._presencesLazy.Value; internal Dictionary PresencesInternal = new(); private Lazy> _presencesLazy; /// /// Gets the collection of presences held by this client. /// public IReadOnlyDictionary EmbeddedActivities => this._embeddedActivitiesLazy.Value; internal Dictionary EmbeddedActivitiesInternal = new(); private Lazy> _embeddedActivitiesLazy; #endregion #region Constructor/Internal Setup /// /// Initializes a new instance of . /// /// Specifies configuration parameters. public DiscordClient(DiscordConfiguration config) : base(config) { if (this.Configuration.MessageCacheSize > 0) { var intents = this.Configuration.Intents; this.MessageCache = intents.HasIntent(DiscordIntents.GuildMessages) || intents.HasIntent(DiscordIntents.DirectMessages) ? new RingBuffer(this.Configuration.MessageCacheSize) : null; } this.InternalSetup(); this.Guilds = new ReadOnlyConcurrentDictionary(this.GuildsInternal); } /// /// Internal setup of the Client. /// internal void InternalSetup() { this._clientErrored = new AsyncEvent("CLIENT_ERRORED", EventExecutionLimit, this.Goof); this._socketErrored = new AsyncEvent("SOCKET_ERRORED", EventExecutionLimit, this.Goof); this._socketOpened = new AsyncEvent("SOCKET_OPENED", EventExecutionLimit, this.EventErrorHandler); this._socketClosed = new AsyncEvent("SOCKET_CLOSED", EventExecutionLimit, this.EventErrorHandler); this._ready = new AsyncEvent("READY", EventExecutionLimit, this.EventErrorHandler); this._resumed = new AsyncEvent("RESUMED", EventExecutionLimit, this.EventErrorHandler); this._channelCreated = new AsyncEvent("CHANNEL_CREATED", EventExecutionLimit, this.EventErrorHandler); this._channelUpdated = new AsyncEvent("CHANNEL_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._channelDeleted = new AsyncEvent("CHANNEL_DELETED", EventExecutionLimit, this.EventErrorHandler); this._dmChannelDeleted = new AsyncEvent("DM_CHANNEL_DELETED", EventExecutionLimit, this.EventErrorHandler); this._channelPinsUpdated = new AsyncEvent("CHANNEL_PINS_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._guildCreated = new AsyncEvent("GUILD_CREATED", EventExecutionLimit, this.EventErrorHandler); this._guildAvailable = new AsyncEvent("GUILD_AVAILABLE", EventExecutionLimit, this.EventErrorHandler); this._guildUpdated = new AsyncEvent("GUILD_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._guildDeleted = new AsyncEvent("GUILD_DELETED", EventExecutionLimit, this.EventErrorHandler); this._guildUnavailable = new AsyncEvent("GUILD_UNAVAILABLE", EventExecutionLimit, this.EventErrorHandler); this._guildDownloadCompletedEv = new AsyncEvent("GUILD_DOWNLOAD_COMPLETED", EventExecutionLimit, this.EventErrorHandler); this._inviteCreated = new AsyncEvent("INVITE_CREATED", EventExecutionLimit, this.EventErrorHandler); this._inviteDeleted = new AsyncEvent("INVITE_DELETED", EventExecutionLimit, this.EventErrorHandler); this._messageCreated = new AsyncEvent("MESSAGE_CREATED", EventExecutionLimit, this.EventErrorHandler); this._presenceUpdated = new AsyncEvent("PRESENCE_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._guildBanAdded = new AsyncEvent("GUILD_BAN_ADD", EventExecutionLimit, this.EventErrorHandler); this._guildBanRemoved = new AsyncEvent("GUILD_BAN_REMOVED", EventExecutionLimit, this.EventErrorHandler); this._guildEmojisUpdated = new AsyncEvent("GUILD_EMOJI_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._guildStickersUpdated = new AsyncEvent("GUILD_STICKER_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._guildIntegrationsUpdated = new AsyncEvent("GUILD_INTEGRATIONS_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._guildMemberAdded = new AsyncEvent("GUILD_MEMBER_ADD", EventExecutionLimit, this.EventErrorHandler); this._guildMemberRemoved = new AsyncEvent("GUILD_MEMBER_REMOVED", EventExecutionLimit, this.EventErrorHandler); this._guildMemberUpdated = new AsyncEvent("GUILD_MEMBER_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._guildRoleCreated = new AsyncEvent("GUILD_ROLE_CREATED", EventExecutionLimit, this.EventErrorHandler); this._guildRoleUpdated = new AsyncEvent("GUILD_ROLE_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._guildRoleDeleted = new AsyncEvent("GUILD_ROLE_DELETED", EventExecutionLimit, this.EventErrorHandler); this._messageAcknowledged = new AsyncEvent("MESSAGE_ACKNOWLEDGED", EventExecutionLimit, this.EventErrorHandler); this._messageUpdated = new AsyncEvent("MESSAGE_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._messageDeleted = new AsyncEvent("MESSAGE_DELETED", EventExecutionLimit, this.EventErrorHandler); this._messagesBulkDeleted = new AsyncEvent("MESSAGE_BULK_DELETED", EventExecutionLimit, this.EventErrorHandler); this._interactionCreated = new AsyncEvent("INTERACTION_CREATED", EventExecutionLimit, this.EventErrorHandler); this._componentInteractionCreated = new AsyncEvent("COMPONENT_INTERACTED", EventExecutionLimit, this.EventErrorHandler); this._contextMenuInteractionCreated = new AsyncEvent("CONTEXT_MENU_INTERACTED", EventExecutionLimit, this.EventErrorHandler); this._typingStarted = new AsyncEvent("TYPING_STARTED", EventExecutionLimit, this.EventErrorHandler); this._userSettingsUpdated = new AsyncEvent("USER_SETTINGS_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._userUpdated = new AsyncEvent("USER_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._voiceStateUpdated = new AsyncEvent("VOICE_STATE_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._voiceServerUpdated = new AsyncEvent("VOICE_SERVER_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._guildMembersChunked = new AsyncEvent("GUILD_MEMBERS_CHUNKED", EventExecutionLimit, this.EventErrorHandler); this._unknownEvent = new AsyncEvent("UNKNOWN_EVENT", EventExecutionLimit, this.EventErrorHandler); this._messageReactionAdded = new AsyncEvent("MESSAGE_REACTION_ADDED", EventExecutionLimit, this.EventErrorHandler); this._messageReactionRemoved = new AsyncEvent("MESSAGE_REACTION_REMOVED", EventExecutionLimit, this.EventErrorHandler); this._messageReactionsCleared = new AsyncEvent("MESSAGE_REACTIONS_CLEARED", EventExecutionLimit, this.EventErrorHandler); this._messageReactionRemovedEmoji = new AsyncEvent("MESSAGE_REACTION_REMOVED_EMOJI", EventExecutionLimit, this.EventErrorHandler); this._webhooksUpdated = new AsyncEvent("WEBHOOKS_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._heartbeated = new AsyncEvent("HEARTBEATED", EventExecutionLimit, this.EventErrorHandler); this._applicationCommandCreated = new AsyncEvent("APPLICATION_COMMAND_CREATED", EventExecutionLimit, this.EventErrorHandler); this._applicationCommandUpdated = new AsyncEvent("APPLICATION_COMMAND_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._applicationCommandDeleted = new AsyncEvent("APPLICATION_COMMAND_DELETED", EventExecutionLimit, this.EventErrorHandler); this._guildApplicationCommandCountUpdated = new AsyncEvent("GUILD_APPLICATION_COMMAND_COUNTS_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._applicationCommandPermissionsUpdated = new AsyncEvent("APPLICATION_COMMAND_PERMISSIONS_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._guildIntegrationCreated = new AsyncEvent("INTEGRATION_CREATED", EventExecutionLimit, this.EventErrorHandler); this._guildIntegrationUpdated = new AsyncEvent("INTEGRATION_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._guildIntegrationDeleted = new AsyncEvent("INTEGRATION_DELETED", EventExecutionLimit, this.EventErrorHandler); this._stageInstanceCreated = new AsyncEvent("STAGE_INSTANCE_CREATED", EventExecutionLimit, this.EventErrorHandler); this._stageInstanceUpdated = new AsyncEvent("STAGE_INSTANCE_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._stageInstanceDeleted = new AsyncEvent("STAGE_INSTANCE_DELETED", EventExecutionLimit, this.EventErrorHandler); this._threadCreated = new AsyncEvent("THREAD_CREATED", EventExecutionLimit, this.EventErrorHandler); this._threadUpdated = new AsyncEvent("THREAD_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._threadDeleted = new AsyncEvent("THREAD_DELETED", EventExecutionLimit, this.EventErrorHandler); this._threadListSynced = new AsyncEvent("THREAD_LIST_SYNCED", EventExecutionLimit, this.EventErrorHandler); this._threadMemberUpdated = new AsyncEvent("THREAD_MEMBER_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._threadMembersUpdated = new AsyncEvent("THREAD_MEMBERS_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._zombied = new AsyncEvent("ZOMBIED", EventExecutionLimit, this.EventErrorHandler); this._payloadReceived = new AsyncEvent("PAYLOAD_RECEIVED", EventExecutionLimit, this.EventErrorHandler); this._guildScheduledEventCreated = new AsyncEvent("GUILD_SCHEDULED_EVENT_CREATED", EventExecutionLimit, this.EventErrorHandler); this._guildScheduledEventUpdated = new AsyncEvent("GUILD_SCHEDULED_EVENT_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._guildScheduledEventDeleted = new AsyncEvent("GUILD_SCHEDULED_EVENT_DELETED", EventExecutionLimit, this.EventErrorHandler); this._guildScheduledEventUserAdded = new AsyncEvent("GUILD_SCHEDULED_EVENT_USER_ADDED", EventExecutionLimit, this.EventErrorHandler); this._guildScheduledEventUserRemoved = new AsyncEvent("GUILD_SCHEDULED_EVENT_USER_REMOVED", EventExecutionLimit, this.EventErrorHandler); this._embeddedActivityUpdated = new AsyncEvent("EMBEDDED_ACTIVITY_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._guildMemberTimeoutAdded = new AsyncEvent("GUILD_MEMBER_TIMEOUT_ADDED", EventExecutionLimit, this.EventErrorHandler); this._guildMemberTimeoutChanged = new AsyncEvent("GUILD_MEMBER_TIMEOUT_UPDATED", EventExecutionLimit, this.EventErrorHandler); this._guildMemberTimeoutRemoved = new AsyncEvent("GUILD_MEMBER_TIMEOUT_REMOVED", EventExecutionLimit, this.EventErrorHandler); this._rateLimitHit = new AsyncEvent("RATELIMIT_HIT", EventExecutionLimit, this.EventErrorHandler); this.GuildsInternal.Clear(); this._presencesLazy = new Lazy>(() => new ReadOnlyDictionary(this.PresencesInternal)); this._embeddedActivitiesLazy = new Lazy>(() => new ReadOnlyDictionary(this.EmbeddedActivitiesInternal)); } #endregion #region Client Extension Methods /// /// Registers an extension with this client. /// /// Extension to register. public void AddExtension(BaseExtension ext) { ext.Setup(this); this._extensions.Add(ext); } /// /// Retrieves a previously registered extension from this client. /// /// The type of extension to retrieve. /// The requested extension. public T GetExtension() where T : BaseExtension => this._extensions.FirstOrDefault(x => x.GetType() == typeof(T)) as T; #endregion #region Public Connection Methods /// /// Connects to the gateway. /// /// The activity to set. Defaults to null. /// The optional status to set. Defaults to null. /// Since when is the client performing the specified activity. Defaults to null. /// Thrown when an invalid token was provided. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task ConnectAsync(DiscordActivity activity = null, UserStatus? status = null, DateTimeOffset? idlesince = null) { // Check if connection lock is already set, and set it if it isn't if (!this._connectionLock.Wait(0)) throw new InvalidOperationException("This client is already connected."); this._connectionLock.Set(); var w = 7500; var i = 5; var s = false; Exception cex = null; if (activity == null && status == null && idlesince == null) this._status = null; else { var sinceUnix = idlesince != null ? (long?)Utilities.GetUnixTime(idlesince.Value) : null; this._status = new StatusUpdate() { Activity = new TransportActivity(activity), Status = status ?? UserStatus.Online, IdleSince = sinceUnix, IsAfk = idlesince != null, ActivityInternal = activity }; } if (!this.IsShard) { if (this.Configuration.TokenType != TokenType.Bot) this.Logger.LogWarning(LoggerEvents.Misc, "You are logging in with a token that is not a bot token. This is not officially supported by Discord, and can result in your account being terminated if you aren't careful."); this.Logger.LogInformation(LoggerEvents.Startup, "Lib {0}, version {1}", this.BotLibrary, this.VersionString); } while (i-- > 0 || this.Configuration.ReconnectIndefinitely) { try { await this.InternalConnectAsync().ConfigureAwait(false); s = true; break; } catch (UnauthorizedException e) { FailConnection(this._connectionLock); throw new Exception("Authentication failed. Check your token and try again.", e); } catch (PlatformNotSupportedException) { FailConnection(this._connectionLock); throw; } catch (NotImplementedException) { FailConnection(this._connectionLock); throw; } catch (Exception ex) { FailConnection(null); cex = ex; if (i <= 0 && !this.Configuration.ReconnectIndefinitely) break; this.Logger.LogError(LoggerEvents.ConnectionFailure, ex, "Connection attempt failed, retrying in {0}s", w / 1000); await Task.Delay(w).ConfigureAwait(false); if (i > 0) w *= 2; } } if (!s && cex != null) { this._connectionLock.Set(); throw new Exception("Could not connect to Discord.", cex); } // non-closure, hence args static void FailConnection(ManualResetEventSlim cl) => // unlock this (if applicable) so we can let others attempt to connect cl?.Set(); } /// /// Reconnects to the gateway. /// /// Whether to start a new session. public Task ReconnectAsync(bool startNewSession = false) => this.InternalReconnectAsync(startNewSession, code: startNewSession ? 1000 : 4002); /// /// Disconnects from the gateway. /// public async Task DisconnectAsync() { this.Configuration.AutoReconnect = false; if (this.WebSocketClient != null) await this.WebSocketClient.DisconnectAsync().ConfigureAwait(false); } #endregion #region Public REST Methods /// /// Gets a user. /// /// Id of the user /// Whether to fetch the user again. Defaults to true. /// The requested user. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task GetUserAsync(ulong userId, bool fetch = true) { if (!fetch) { return this.TryGetCachedUserInternal(userId, out var usr) ? usr : new DiscordUser { Id = userId, Discord = this }; } else { var usr = await this.ApiClient.GetUserAsync(userId).ConfigureAwait(false); usr = this.UserCache.AddOrUpdate(userId, usr, (id, old) => { old.Username = usr.Username; old.Discriminator = usr.Discriminator; old.AvatarHash = usr.AvatarHash; old.BannerHash = usr.BannerHash; old.BannerColorInternal = usr.BannerColorInternal; return old; }); return usr; } } /// /// Gets a channel. /// /// The id of the channel to get. /// The requested channel. /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task GetChannelAsync(ulong id) => this.InternalGetCachedChannel(id) ?? await this.ApiClient.GetChannelAsync(id).ConfigureAwait(false); /// /// Gets a thread. /// /// The id of the thread to get. /// The requested thread. /// Thrown when the thread does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task GetThreadAsync(ulong id) => this.InternalGetCachedThread(id) ?? await this.ApiClient.GetThreadAsync(id).ConfigureAwait(false); /// /// Sends a normal message. /// /// The channel to send to. /// The message content to send. /// The message that was sent. /// Thrown when the client does not have the permission. /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task SendMessageAsync(DiscordChannel channel, string content) => this.ApiClient.CreateMessageAsync(channel.Id, content, embeds: null, sticker: null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false); /// /// Sends a message with an embed. /// /// The channel to send to. /// The embed to attach to the message. /// The message that was sent. /// Thrown when the client does not have the permission. /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task SendMessageAsync(DiscordChannel channel, DiscordEmbed embed) => this.ApiClient.CreateMessageAsync(channel.Id, null, embed != null ? new[] { embed } : null, sticker: null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false); /// /// Sends a message with content and an embed. /// /// Channel to send to. /// The message content to send. /// The embed to attach to the message. /// The message that was sent. /// Thrown when the client does not have the permission. /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task SendMessageAsync(DiscordChannel channel, string content, DiscordEmbed embed) => this.ApiClient.CreateMessageAsync(channel.Id, content, embed != null ? new[] { embed } : null, sticker: null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false); /// /// Sends a message with the . /// /// The channel to send the message to. /// The message builder. /// The message that was sent. /// Thrown when the client does not have the permission if TTS is false and if TTS is true. /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task SendMessageAsync(DiscordChannel channel, DiscordMessageBuilder builder) => this.ApiClient.CreateMessageAsync(channel.Id, builder); /// /// Sends a message with an . /// /// The channel to send the message to. /// The message builder. /// The message that was sent. /// Thrown when the client does not have the permission if TTS is false and if TTS is true. /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task SendMessageAsync(DiscordChannel channel, Action action) { var builder = new DiscordMessageBuilder(); action(builder); return this.ApiClient.CreateMessageAsync(channel.Id, builder); } /// /// Creates a guild. This requires the bot to be in less than 10 guilds total. /// /// Name of the guild. /// Voice region of the guild. /// Stream containing the icon for the guild. /// Verification level for the guild. /// Default message notification settings for the guild. /// System channel flags fopr the guild. /// The created guild. /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task CreateGuildAsync(string name, string region = null, Optional icon = default, VerificationLevel? verificationLevel = null, DefaultMessageNotifications? defaultMessageNotifications = null, SystemChannelFlags? systemChannelFlags = null) { var iconb64 = ImageTool.Base64FromStream(icon); return this.ApiClient.CreateGuildAsync(name, region, iconb64, verificationLevel, defaultMessageNotifications, systemChannelFlags); } /// /// Creates a guild from a template. This requires the bot to be in less than 10 guilds total. /// /// The template code. /// Name of the guild. /// Stream containing the icon for the guild. /// The created guild. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task CreateGuildFromTemplateAsync(string code, string name, Optional icon = default) { var iconb64 = ImageTool.Base64FromStream(icon); return this.ApiClient.CreateGuildFromTemplateAsync(code, name, iconb64); } /// /// Executes a raw request. /// /// /// /// var request = await Client.ExecuteRawRequestAsync(RestRequestMethod.GET, $"{Endpoints.CHANNELS}/243184972190742178964/{Endpoints.INVITES}"); /// List<DiscordInvite> invites = DiscordJson.ToDiscordObject<List<DiscordInvite>>(request.Response); /// /// /// The method. /// The route. /// The route parameters. /// The json body. /// The addditional headers. /// The ratelimit wait override. /// Thrown when the ressource does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. /// A awaitable RestResponse - [Obsolete("This is no longer needed. Use DiscordClient.RestClient instead.", false)] + [Obsolete("This is no longer needed. Use DiscordClient.RestClient instead.", false)] public async Task ExecuteRawRequestAsync(RestRequestMethod method, string route, object routeParams, string jsonBody = null, Dictionary additionalHeaders = null, double? ratelimitWaitOverride = null) { var bucket = this.ApiClient.Rest.GetBucket(method, route, routeParams, out var path); var url = Utilities.GetApiUriFor(path, this.Configuration); var res = await this.ApiClient.DoRequestAsync(this, bucket, url, method, route, additionalHeaders, DiscordJson.SerializeObject(jsonBody), ratelimitWaitOverride); return res; } /// /// Gets a guild. /// Setting to true will make a REST request. /// /// The guild ID to search for. /// Whether to include approximate presence and member counts in the returned guild. /// The requested Guild. /// Thrown when the guild does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task GetGuildAsync(ulong id, bool? withCounts = null) { if (this.GuildsInternal.TryGetValue(id, out var guild) && (!withCounts.HasValue || !withCounts.Value)) return guild; guild = await this.ApiClient.GetGuildAsync(id, withCounts).ConfigureAwait(false); var channels = await this.ApiClient.GetGuildChannelsAsync(guild.Id).ConfigureAwait(false); foreach (var channel in channels) guild.ChannelsInternal[channel.Id] = channel; return guild; } /// /// Gets a guild preview. /// /// The guild ID. /// /// Thrown when the guild does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task GetGuildPreviewAsync(ulong id) => this.ApiClient.GetGuildPreviewAsync(id); /// /// Gets an invite. /// /// The invite code. /// Whether to include presence and total member counts in the returned invite. /// Whether to include the expiration date in the returned invite. /// The scheduled event id. /// The requested Invite. /// Thrown when the invite does not exists. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task GetInviteByCodeAsync(string code, bool? withCounts = null, bool? withExpiration = null, ulong? scheduledEventId = null) => this.ApiClient.GetInviteAsync(code, withCounts, withExpiration, scheduledEventId); /// /// Gets a list of user connections. /// /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task> GetConnectionsAsync() => this.ApiClient.GetUserConnectionsAsync(); /// /// Gets a sticker. /// /// The requested sticker. /// The id of the sticker. /// Thrown when the sticker does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task GetStickerAsync(ulong id) => this.ApiClient.GetStickerAsync(id); /// /// Gets all nitro sticker packs. /// /// List of sticker packs. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task> GetStickerPacksAsync() => this.ApiClient.GetStickerPacksAsync(); /// /// Gets the In-App OAuth Url. /// /// Defaults to . /// Redirect Uri. /// Defaults to . /// The OAuth Url public Uri GetInAppOAuth(Permissions permissions = Permissions.None, OAuthScopes scopes = OAuthScopes.BOT_DEFAULT, string redir = null) { permissions &= PermissionMethods.FullPerms; // hey look, it's not all annoying and blue :P return new Uri(new QueryUriBuilder($"{DiscordDomain.GetDomain(CoreDomain.Discord).Url}{Endpoints.OAUTH2}{Endpoints.AUTHORIZE}") .AddParameter("client_id", this.CurrentApplication.Id.ToString(CultureInfo.InvariantCulture)) .AddParameter("scope", OAuth.ResolveScopes(scopes)) .AddParameter("permissions", ((long)permissions).ToString(CultureInfo.InvariantCulture)) .AddParameter("state", "") .AddParameter("redirect_uri", redir ?? "") .ToString()); } /// /// Gets a webhook. /// /// The target webhook id. /// The requested webhook. /// Thrown when the webhook does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task GetWebhookAsync(ulong id) => this.ApiClient.GetWebhookAsync(id); /// /// Gets a webhook. /// /// The target webhook id. /// The target webhook token. /// The requested webhook. /// Thrown when the webhook does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task GetWebhookWithTokenAsync(ulong id, string token) => this.ApiClient.GetWebhookWithTokenAsync(id, token); /// /// Updates current user's activity and status. /// /// Activity to set. /// Status of the user. /// Since when is the client performing the specified activity. /// public Task UpdateStatusAsync(DiscordActivity activity = null, UserStatus? userStatus = null, DateTimeOffset? idleSince = null) => this.InternalUpdateStatusAsync(activity, userStatus, idleSince); /// /// Edits current user. /// /// New username. /// New avatar. /// The modified user. /// Thrown when the user does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task UpdateCurrentUserAsync(string username = null, Optional avatar = default) { var av64 = ImageTool.Base64FromStream(avatar); var usr = await this.ApiClient.ModifyCurrentUserAsync(username, av64).ConfigureAwait(false); this.CurrentUser.Username = usr.Username; this.CurrentUser.Discriminator = usr.Discriminator; this.CurrentUser.AvatarHash = usr.AvatarHash; return this.CurrentUser; } /// /// Gets a guild template by the code. /// /// The code of the template. /// The guild template for the code. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task GetTemplateAsync(string code) => this.ApiClient.GetTemplateAsync(code); /// /// Gets all the global application commands for this application. /// /// Whether to get the full localization dict. /// A list of global application commands. public Task> GetGlobalApplicationCommandsAsync(bool withLocalizations = false) => this.ApiClient.GetGlobalApplicationCommandsAsync(this.CurrentApplication.Id, withLocalizations); /// /// Overwrites the existing global application commands. New commands are automatically created and missing commands are automatically deleted. /// /// The list of commands to overwrite with. /// The list of global commands. public Task> BulkOverwriteGlobalApplicationCommandsAsync(IEnumerable commands) => this.ApiClient.BulkOverwriteGlobalApplicationCommandsAsync(this.CurrentApplication.Id, commands); /// /// Creates or overwrites a global application command. /// /// The command to create. /// The created command. public Task CreateGlobalApplicationCommandAsync(DiscordApplicationCommand command) => this.ApiClient.CreateGlobalApplicationCommandAsync(this.CurrentApplication.Id, command); /// /// Gets a global application command by its id. /// /// The id of the command to get. /// The command with the id. public Task GetGlobalApplicationCommandAsync(ulong commandId) => this.ApiClient.GetGlobalApplicationCommandAsync(this.CurrentApplication.Id, commandId); /// /// Edits a global application command. /// /// The id of the command to edit. /// Action to perform. /// The edited command. public async Task EditGlobalApplicationCommandAsync(ulong commandId, Action action) { var mdl = new ApplicationCommandEditModel(); action(mdl); var applicationId = this.CurrentApplication?.Id ?? (await this.GetCurrentApplicationAsync().ConfigureAwait(false)).Id; return await this.ApiClient.EditGlobalApplicationCommandAsync(applicationId, commandId, mdl.Name, mdl.Description, mdl.Options, mdl.NameLocalizations, mdl.DescriptionLocalizations, mdl.DefaultMemberPermissions, mdl.DmPermission).ConfigureAwait(false); } /// /// Deletes a global application command. /// /// The id of the command to delete. public Task DeleteGlobalApplicationCommandAsync(ulong commandId) => this.ApiClient.DeleteGlobalApplicationCommandAsync(this.CurrentApplication.Id, commandId); /// /// Gets all the application commands for a guild. /// /// The id of the guild to get application commands for. /// Whether to get the full localization dict. /// A list of application commands in the guild. public Task> GetGuildApplicationCommandsAsync(ulong guildId, bool withLocalizations = false) => this.ApiClient.GetGuildApplicationCommandsAsync(this.CurrentApplication.Id, guildId, withLocalizations); /// /// Overwrites the existing application commands in a guild. New commands are automatically created and missing commands are automatically deleted. /// /// The id of the guild. /// The list of commands to overwrite with. /// The list of guild commands. public Task> BulkOverwriteGuildApplicationCommandsAsync(ulong guildId, IEnumerable commands) => this.ApiClient.BulkOverwriteGuildApplicationCommandsAsync(this.CurrentApplication.Id, guildId, commands); /// /// Creates or overwrites a guild application command. /// /// The id of the guild to create the application command in. /// The command to create. /// The created command. public Task CreateGuildApplicationCommandAsync(ulong guildId, DiscordApplicationCommand command) => this.ApiClient.CreateGuildApplicationCommandAsync(this.CurrentApplication.Id, guildId, command); /// /// Gets a application command in a guild by its id. /// /// The id of the guild the application command is in. /// The id of the command to get. /// The command with the id. public Task GetGuildApplicationCommandAsync(ulong guildId, ulong commandId) => this.ApiClient.GetGuildApplicationCommandAsync(this.CurrentApplication.Id, guildId, commandId); /// /// Edits a application command in a guild. /// /// The id of the guild the application command is in. /// The id of the command to edit. /// Action to perform. /// The edited command. public async Task EditGuildApplicationCommandAsync(ulong guildId, ulong commandId, Action action) { var mdl = new ApplicationCommandEditModel(); action(mdl); var applicationId = this.CurrentApplication?.Id ?? (await this.GetCurrentApplicationAsync().ConfigureAwait(false)).Id; return await this.ApiClient.EditGuildApplicationCommandAsync(applicationId, guildId, commandId, mdl.Name, mdl.Description, mdl.Options, mdl.NameLocalizations, mdl.DescriptionLocalizations, mdl.DefaultMemberPermissions, mdl.DmPermission).ConfigureAwait(false); } /// /// Deletes a application command in a guild. /// /// The id of the guild to delete the application command in. /// The id of the command. public Task DeleteGuildApplicationCommandAsync(ulong guildId, ulong commandId) => this.ApiClient.DeleteGuildApplicationCommandAsync(this.CurrentApplication.Id, guildId, commandId); /// /// Gets all command permissions for a guild. /// /// The target guild. public Task> GetGuildApplicationCommandPermissionsAsync(ulong guildId) => this.ApiClient.GetGuildApplicationCommandPermissionsAsync(this.CurrentApplication.Id, guildId); /// /// Gets the permissions for a guild command. /// /// The target guild. /// The target command id. public Task GetApplicationCommandPermissionAsync(ulong guildId, ulong commandId) => this.ApiClient.GetGuildApplicationCommandPermissionAsync(this.CurrentApplication.Id, guildId, commandId); #endregion #region Internal Caching Methods /// /// Gets the internal chached threads. /// /// The target thread id. /// The requested thread. internal DiscordThreadChannel InternalGetCachedThread(ulong threadId) { foreach (var guild in this.Guilds.Values) if (guild.Threads.TryGetValue(threadId, out var foundThread)) return foundThread; return null; } /// /// Gets the internal cached scheduled event. /// /// The target scheduled event id. /// The requested scheduled event. internal DiscordScheduledEvent InternalGetCachedScheduledEvent(ulong scheduledEventId) { foreach (var guild in this.Guilds.Values) if (guild.ScheduledEvents.TryGetValue(scheduledEventId, out var foundScheduledEvent)) return foundScheduledEvent; return null; } /// /// Gets the internal cached channel. /// /// The target channel id. /// The requested channel. internal DiscordChannel InternalGetCachedChannel(ulong channelId) { foreach (var guild in this.Guilds.Values) if (guild.Channels.TryGetValue(channelId, out var foundChannel)) return foundChannel; return null; } /// /// Gets the internal chached guild. /// /// The target guild id. /// The requested guild. internal DiscordGuild InternalGetCachedGuild(ulong? guildId) { if (this.GuildsInternal != null && guildId.HasValue) { if (this.GuildsInternal.TryGetValue(guildId.Value, out var guild)) return guild; } return null; } /// /// Updates a message. /// /// The message to update. /// The author to update. /// The guild to update. /// The member to update. private void UpdateMessage(DiscordMessage message, TransportUser author, DiscordGuild guild, TransportMember member) { if (author != null) { var usr = new DiscordUser(author) { Discord = this }; if (member != null) member.User = author; message.Author = this.UpdateUser(usr, guild?.Id, guild, member); } var channel = this.InternalGetCachedChannel(message.ChannelId); if (channel != null) return; channel = !message.GuildId.HasValue ? new DiscordDmChannel { Id = message.ChannelId, Discord = this, Type = ChannelType.Private } : new DiscordChannel { Id = message.ChannelId, Discord = this }; message.Channel = channel; } /// /// Updates a scheduled event. /// /// The scheduled event to update. /// The guild to update. /// The updated scheduled event. private DiscordScheduledEvent UpdateScheduledEvent(DiscordScheduledEvent scheduledEvent, DiscordGuild guild) { if (scheduledEvent != null) { _ = guild.ScheduledEventsInternal.AddOrUpdate(scheduledEvent.Id, scheduledEvent, (id, old) => { old.Discord = this; old.Description = scheduledEvent.Description; old.ChannelId = scheduledEvent.ChannelId; old.EntityId = scheduledEvent.EntityId; old.EntityType = scheduledEvent.EntityType; old.EntityMetadata = scheduledEvent.EntityMetadata; old.PrivacyLevel = scheduledEvent.PrivacyLevel; old.Name = scheduledEvent.Name; old.Status = scheduledEvent.Status; old.UserCount = scheduledEvent.UserCount; old.ScheduledStartTimeRaw = scheduledEvent.ScheduledStartTimeRaw; old.ScheduledEndTimeRaw = scheduledEvent.ScheduledEndTimeRaw; return old; }); } return scheduledEvent; } /// /// Updates a user. /// /// The user to update. /// The guild id to update. /// The guild to update. /// The member to update. /// The updated user. private DiscordUser UpdateUser(DiscordUser usr, ulong? guildId, DiscordGuild guild, TransportMember mbr) { if (mbr != null) { if (mbr.User != null) { usr = new DiscordUser(mbr.User) { Discord = this }; _ = this.UserCache.AddOrUpdate(usr.Id, usr, (id, old) => { old.Username = usr.Username; old.Discriminator = usr.Discriminator; old.AvatarHash = usr.AvatarHash; return old; }); usr = new DiscordMember(mbr) { Discord = this, GuildId = guildId.Value }; } var intents = this.Configuration.Intents; DiscordMember member = default; if (!intents.HasAllPrivilegedIntents() || guild.IsLarge) // we have the necessary privileged intents, no need to worry about caching here unless guild is large. { if (guild?.MembersInternal.TryGetValue(usr.Id, out member) == false) { if (intents.HasIntent(DiscordIntents.GuildMembers) || this.Configuration.AlwaysCacheMembers) // member can be updated by events, so cache it { guild.MembersInternal.TryAdd(usr.Id, (DiscordMember)usr); } } else if (intents.HasIntent(DiscordIntents.GuildPresences) || this.Configuration.AlwaysCacheMembers) // we can attempt to update it if it's already in cache. { if (!intents.HasIntent(DiscordIntents.GuildMembers)) // no need to update if we already have the member events { _ = guild.MembersInternal.TryUpdate(usr.Id, (DiscordMember)usr, member); } } } } else if (usr.Username != null) // check if not a skeleton user { _ = this.UserCache.AddOrUpdate(usr.Id, usr, (id, old) => { old.Username = usr.Username; old.Discriminator = usr.Discriminator; old.AvatarHash = usr.AvatarHash; return old; }); } return usr; } /// /// Updates the cached scheduled events in a guild. /// /// The guild. /// The raw events. private void UpdateCachedScheduledEvent(DiscordGuild guild, JArray rawEvents) { if (this._disposed) return; if (rawEvents != null) { guild.ScheduledEventsInternal.Clear(); foreach (var xj in rawEvents) { var xtm = xj.ToDiscordObject(); xtm.Discord = this; guild.ScheduledEventsInternal[xtm.Id] = xtm; } } } /// /// Updates the cached guild. /// /// The new guild. /// The raw members. private void UpdateCachedGuild(DiscordGuild newGuild, JArray rawMembers) { if (this._disposed) return; if (!this.GuildsInternal.ContainsKey(newGuild.Id)) this.GuildsInternal[newGuild.Id] = newGuild; var guild = this.GuildsInternal[newGuild.Id]; if (newGuild.ChannelsInternal != null && !newGuild.ChannelsInternal.IsEmpty) { foreach (var channel in newGuild.ChannelsInternal.Values) { if (guild.ChannelsInternal.TryGetValue(channel.Id, out _)) continue; foreach (var overwrite in channel.PermissionOverwritesInternal) { overwrite.Discord = this; overwrite.ChannelId = channel.Id; } guild.ChannelsInternal[channel.Id] = channel; } } if (newGuild.ThreadsInternal != null && !newGuild.ThreadsInternal.IsEmpty) { foreach (var thread in newGuild.ThreadsInternal.Values) { if (guild.ThreadsInternal.TryGetValue(thread.Id, out _)) continue; guild.ThreadsInternal[thread.Id] = thread; } } if (newGuild.ScheduledEventsInternal != null && !newGuild.ScheduledEventsInternal.IsEmpty) { foreach (var @event in newGuild.ScheduledEventsInternal.Values) { if (guild.ScheduledEventsInternal.TryGetValue(@event.Id, out _)) continue; guild.ScheduledEventsInternal[@event.Id] = @event; } } foreach (var newEmoji in newGuild.EmojisInternal.Values) _ = guild.EmojisInternal.GetOrAdd(newEmoji.Id, _ => newEmoji); foreach (var newSticker in newGuild.StickersInternal.Values) _ = guild.StickersInternal.GetOrAdd(newSticker.Id, _ => newSticker); foreach (var newStageInstance in newGuild.StageInstancesInternal.Values) _ = guild.StageInstancesInternal.GetOrAdd(newStageInstance.Id, _ => newStageInstance); if (rawMembers != null) { guild.MembersInternal.Clear(); foreach (var xj in rawMembers) { var xtm = xj.ToDiscordObject(); var xu = new DiscordUser(xtm.User) { Discord = this }; _ = this.UserCache.AddOrUpdate(xtm.User.Id, xu, (id, old) => { old.Username = xu.Username; old.Discriminator = xu.Discriminator; old.AvatarHash = xu.AvatarHash; old.PremiumType = xu.PremiumType; return old; }); guild.MembersInternal[xtm.User.Id] = new DiscordMember(xtm) { Discord = this, GuildId = guild.Id }; } } foreach (var role in newGuild.RolesInternal.Values) { if (guild.RolesInternal.TryGetValue(role.Id, out _)) continue; role.GuildId = guild.Id; guild.RolesInternal[role.Id] = role; } guild.Name = newGuild.Name; guild.AfkChannelId = newGuild.AfkChannelId; guild.AfkTimeout = newGuild.AfkTimeout; guild.DefaultMessageNotifications = newGuild.DefaultMessageNotifications; guild.RawFeatures = newGuild.RawFeatures; guild.IconHash = newGuild.IconHash; guild.MfaLevel = newGuild.MfaLevel; guild.OwnerId = newGuild.OwnerId; guild.VoiceRegionId = newGuild.VoiceRegionId; guild.SplashHash = newGuild.SplashHash; guild.VerificationLevel = newGuild.VerificationLevel; guild.WidgetEnabled = newGuild.WidgetEnabled; guild.WidgetChannelId = newGuild.WidgetChannelId; guild.ExplicitContentFilter = newGuild.ExplicitContentFilter; guild.PremiumTier = newGuild.PremiumTier; guild.PremiumSubscriptionCount = newGuild.PremiumSubscriptionCount; guild.PremiumProgressBarEnabled = newGuild.PremiumProgressBarEnabled; guild.BannerHash = newGuild.BannerHash; guild.Description = newGuild.Description; guild.VanityUrlCode = newGuild.VanityUrlCode; guild.SystemChannelId = newGuild.SystemChannelId; guild.SystemChannelFlags = newGuild.SystemChannelFlags; guild.DiscoverySplashHash = newGuild.DiscoverySplashHash; guild.MaxMembers = newGuild.MaxMembers; guild.MaxPresences = newGuild.MaxPresences; guild.ApproximateMemberCount = newGuild.ApproximateMemberCount; guild.ApproximatePresenceCount = newGuild.ApproximatePresenceCount; guild.MaxVideoChannelUsers = newGuild.MaxVideoChannelUsers; guild.PreferredLocale = newGuild.PreferredLocale; guild.RulesChannelId = newGuild.RulesChannelId; guild.PublicUpdatesChannelId = newGuild.PublicUpdatesChannelId; guild.ApplicationId = newGuild.ApplicationId; // fields not sent for update: // - guild.Channels // - voice states // - guild.JoinedAt = new_guild.JoinedAt; // - guild.Large = new_guild.Large; // - guild.MemberCount = Math.Max(new_guild.MemberCount, guild._members.Count); // - guild.Unavailable = new_guild.Unavailable; } /// /// Populates the message reactions and cache. /// /// The message. /// The author. /// The member. private void PopulateMessageReactionsAndCache(DiscordMessage message, TransportUser author, TransportMember member) { var guild = message.Channel?.Guild ?? this.InternalGetCachedGuild(message.GuildId); this.UpdateMessage(message, author, guild, member); if (message.ReactionsInternal == null) message.ReactionsInternal = new List(); foreach (var xr in message.ReactionsInternal) xr.Emoji.Discord = this; if (this.Configuration.MessageCacheSize > 0 && message.Channel != null) this.MessageCache?.Add(message); } #endregion #region Disposal ~DiscordClient() { this.Dispose(); } /// /// Whether the client is disposed. /// private bool _disposed; /// /// Disposes the client. /// public override void Dispose() { if (this._disposed) return; this._disposed = true; GC.SuppressFinalize(this); this.DisconnectAsync().ConfigureAwait(false).GetAwaiter().GetResult(); this.ApiClient.Rest.Dispose(); this.CurrentUser = null; var extensions = this._extensions; // prevent _extensions being modified during dispose this._extensions = null; foreach (var extension in extensions) if (extension is IDisposable disposable) disposable.Dispose(); try { this._cancelTokenSource?.Cancel(); this._cancelTokenSource?.Dispose(); } catch { } this.GuildsInternal = null; this._heartbeatTask = null; } #endregion } diff --git a/DisCatSharp/Entities/Embed/DiscordEmbedField.cs b/DisCatSharp/Entities/Embed/DiscordEmbedField.cs index 57f4d4957..ed2b90448 100644 --- a/DisCatSharp/Entities/Embed/DiscordEmbedField.cs +++ b/DisCatSharp/Entities/Embed/DiscordEmbedField.cs @@ -1,100 +1,104 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using Newtonsoft.Json; namespace DisCatSharp.Entities; /// /// Represents a field inside a discord embed. /// public sealed class DiscordEmbedField { private string _name; /// /// The name of the field. /// Must be non-null, non-empty and <= 256 characters. /// [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] - public string Name { get => this._name; + public string Name + { + get => this._name; set { if (string.IsNullOrWhiteSpace(value)) { if (value == null) throw new ArgumentNullException(nameof(value)); throw new ArgumentException("Name cannot be empty or whitespace.", nameof(value)); } if (value.Length > 256) throw new ArgumentException("Embed field name length cannot exceed 256 characters."); this._name = value; } } private string _value; /// /// The value of the field. /// Must be non-null, non-empty and <= 1024 characters. /// [JsonProperty("value", NullValueHandling = NullValueHandling.Ignore)] - public string Value { get => this._value; + public string Value + { + get => this._value; set { if (string.IsNullOrWhiteSpace(value)) { if (value == null) throw new ArgumentNullException(nameof(value)); throw new ArgumentException("Value cannot be empty or whitespace.", nameof(value)); } if (value.Length > 1024) throw new ArgumentException("Embed field value length cannot exceed 1024 characters."); this._value = value; } } /// /// Whether or not this field should display inline. /// [JsonProperty("inline", NullValueHandling = NullValueHandling.Ignore)] public bool Inline { get; set; } /// /// Initializes a new instance of the class. /// /// /// /// public DiscordEmbedField(string name, string value, bool inline = false) { this.Name = name; this.Value = value; this.Inline = inline; } } diff --git a/DisCatSharp/Entities/Emoji/DiscordEmoji.cs b/DisCatSharp/Entities/Emoji/DiscordEmoji.cs index efed0680c..b98000d2d 100644 --- a/DisCatSharp/Entities/Emoji/DiscordEmoji.cs +++ b/DisCatSharp/Entities/Emoji/DiscordEmoji.cs @@ -1,375 +1,375 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Globalization; using System.Linq; using DisCatSharp.Enums; using DisCatSharp.Net; using Newtonsoft.Json; namespace DisCatSharp.Entities; /// /// Represents a Discord emoji. /// public partial class DiscordEmoji : SnowflakeObject, IEquatable { /// /// Gets the name of this emoji. /// [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] public string Name { get; internal set; } /// /// Gets IDs the roles this emoji is enabled for. /// [JsonIgnore] public IReadOnlyList Roles => this._rolesLazy.Value; [JsonProperty("roles", NullValueHandling = NullValueHandling.Ignore)] public List RolesInternal; private readonly Lazy> _rolesLazy; /// /// Gets whether this emoji requires colons to use. /// [JsonProperty("require_colons")] public bool RequiresColons { get; internal set; } /// /// Gets whether this emoji is managed by an integration. /// [JsonProperty("managed")] public bool IsManaged { get; internal set; } /// /// Gets whether this emoji is animated. /// [JsonProperty("animated")] public bool IsAnimated { get; internal set; } /// /// Gets whether the emoji is available for use. /// An emoji may not be available due to loss of server boost. /// [JsonProperty("available", NullValueHandling = NullValueHandling.Ignore)] public bool IsAvailable { get; internal set; } /// /// Gets the image URL of this emoji. /// [JsonIgnore] public string Url => this.Id == 0 ? throw new InvalidOperationException("Cannot get URL of unicode emojis.") : this.IsAnimated ? $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.EMOJIS}/{this.Id.ToString(CultureInfo.InvariantCulture)}.gif" : $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.EMOJIS}/{this.Id.ToString(CultureInfo.InvariantCulture)}.png"; /// /// Initializes a new instance of the class. /// internal DiscordEmoji() { this._rolesLazy = new Lazy>(() => new ReadOnlyCollection(this.RolesInternal)); } /// /// Gets emoji's name in non-Unicode format (eg. :thinking: instead of the Unicode representation of the emoji). /// public string GetDiscordName() { s_discordNameLookup.TryGetValue(this.Name, out var name); - return name ?? $":{ this.Name }:"; + return name ?? $":{this.Name}:"; } /// /// Returns a string representation of this emoji. /// /// String representation of this emoji. public override string ToString() => this.Id != 0 ? this.IsAnimated ? $"" : $"<:{this.Name}:{this.Id.ToString(CultureInfo.InvariantCulture)}>" : this.Name; /// /// Checks whether this is equal to another object. /// /// Object to compare to. /// Whether the object is equal to this . public override bool Equals(object obj) => this.Equals(obj as DiscordEmoji); /// /// Checks whether this is equal to another . /// /// to compare to. /// Whether the is equal to this . public bool Equals(DiscordEmoji e) => e is not null && (ReferenceEquals(this, e) || (this.Id == e.Id && this.Name == e.Name)); /// /// Gets the hash code for this . /// /// The hash code for this . public override int GetHashCode() { var hash = 13; hash = (hash * 7) + this.Id.GetHashCode(); hash = (hash * 7) + this.Name.GetHashCode(); return hash; } /// /// Gets the reactions string. /// internal string ToReactionString() => this.Id != 0 ? $"{this.Name}:{this.Id.ToString(CultureInfo.InvariantCulture)}" : this.Name; /// /// Gets whether the two objects are equal. /// /// First emoji to compare. /// Second emoji to compare. /// Whether the two emoji are equal. public static bool operator ==(DiscordEmoji e1, DiscordEmoji e2) { var o1 = e1 as object; var o2 = e2 as object; return (o1 != null || o2 == null) && (o1 == null || o2 != null) && ((o1 == null && o2 == null) || e1.Id == e2.Id); } /// /// Gets whether the two objects are not equal. /// /// First emoji to compare. /// Second emoji to compare. /// Whether the two emoji are not equal. public static bool operator !=(DiscordEmoji e1, DiscordEmoji e2) => !(e1 == e2); /// /// Implicitly converts this emoji to its string representation. /// /// Emoji to convert. public static implicit operator string(DiscordEmoji e1) => e1.ToString(); /// /// Checks whether specified unicode entity is a valid unicode emoji. /// /// Entity to check. /// Whether it's a valid emoji. public static bool IsValidUnicode(string unicodeEntity) => s_discordNameLookup.ContainsKey(unicodeEntity); /// /// Creates an emoji object from a unicode entity. /// /// to attach to the object. /// Unicode entity to create the object from. /// Create object. public static DiscordEmoji FromUnicode(BaseDiscordClient client, string unicodeEntity) => !IsValidUnicode(unicodeEntity) ? throw new ArgumentException("Specified unicode entity is not a valid unicode emoji.", nameof(unicodeEntity)) : new DiscordEmoji { Name = unicodeEntity, Discord = client }; /// /// Creates an emoji object from a unicode entity. /// /// Unicode entity to create the object from. /// Create object. public static DiscordEmoji FromUnicode(string unicodeEntity) => FromUnicode(null, unicodeEntity); /// /// Attempts to create an emoji object from a unicode entity. /// /// to attach to the object. /// Unicode entity to create the object from. /// Resulting object. /// Whether the operation was successful. public static bool TryFromUnicode(BaseDiscordClient client, string unicodeEntity, out DiscordEmoji emoji) { // this is a round-trip operation because of FE0F inconsistencies. // through this, the inconsistency is normalized. emoji = null; if (!s_discordNameLookup.TryGetValue(unicodeEntity, out var discordName)) return false; if (!s_unicodeEmojis.TryGetValue(discordName, out unicodeEntity)) return false; emoji = new DiscordEmoji { Name = unicodeEntity, Discord = client }; return true; } /// /// Attempts to create an emoji object from a unicode entity. /// /// Unicode entity to create the object from. /// Resulting object. /// Whether the operation was successful. public static bool TryFromUnicode(string unicodeEntity, out DiscordEmoji emoji) => TryFromUnicode(null, unicodeEntity, out emoji); /// /// Creates an emoji object from a guild emote. /// /// to attach to the object. /// Id of the emote. /// Create object. public static DiscordEmoji FromGuildEmote(BaseDiscordClient client, ulong id) { if (client == null) throw new ArgumentNullException(nameof(client), "Client cannot be null."); foreach (var guild in client.Guilds.Values) { if (guild.Emojis.TryGetValue(id, out var found)) return found; } throw new KeyNotFoundException("Given emote was not found."); } /// /// Attempts to create an emoji object from a guild emote. /// /// to attach to the object. /// Id of the emote. /// Resulting object. /// Whether the operation was successful. public static bool TryFromGuildEmote(BaseDiscordClient client, ulong id, out DiscordEmoji emoji) { if (client == null) throw new ArgumentNullException(nameof(client), "Client cannot be null."); foreach (var guild in client.Guilds.Values) { if (guild.Emojis.TryGetValue(id, out emoji)) return true; } emoji = null; return false; } /// /// Creates an emoji obejct from emote name that includes colons (eg. :thinking:). This method also supports /// skin tone variations (eg. :ok_hand::skin-tone-2:), standard emoticons (eg. :D), as well as guild emoji /// (still specified by :name:). /// /// to attach to the object. /// Name of the emote to find, including colons (eg. :thinking:). /// Should guild emojis be included in the search. /// Create object. public static DiscordEmoji FromName(BaseDiscordClient client, string name, bool includeGuilds = true) { if (client == null) throw new ArgumentNullException(nameof(client), "Client cannot be null."); if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullException(nameof(name), "Name cannot be empty or null."); if (s_unicodeEmojis.TryGetValue(name, out var unicodeEntity)) return new DiscordEmoji { Discord = client, Name = unicodeEntity }; if (includeGuilds) { var allEmojis = client.Guilds.Values .SelectMany(xg => xg.Emojis.Values); // save cycles - don't order var ek = name.AsSpan().Slice(1, name.Length - 2); foreach (var emoji in allEmojis) if (emoji.Name.AsSpan().SequenceEqual(ek)) return emoji; } throw new ArgumentException("Invalid emoji name specified.", nameof(name)); } /// /// Attempts to create an emoji object from emote name that includes colons (eg. :thinking:). This method also /// supports skin tone variations (eg. :ok_hand::skin-tone-2:), standard emoticons (eg. :D), as well as guild /// emoji (still specified by :name:). /// /// to attach to the object. /// Name of the emote to find, including colons (eg. :thinking:). /// Resulting object. /// Whether the operation was successful. public static bool TryFromName(BaseDiscordClient client, string name, out DiscordEmoji emoji) => TryFromName(client, name, true, out emoji); /// /// Attempts to create an emoji object from emote name that includes colons (eg. :thinking:). This method also /// supports skin tone variations (eg. :ok_hand::skin-tone-2:), standard emoticons (eg. :D), as well as guild /// emoji (still specified by :name:). /// /// to attach to the object. /// Name of the emote to find, including colons (eg. :thinking:). /// Should guild emojis be included in the search. /// Resulting object. /// Whether the operation was successful. public static bool TryFromName(BaseDiscordClient client, string name, bool includeGuilds, out DiscordEmoji emoji) { if (client == null) throw new ArgumentNullException(nameof(client), "Client cannot be null."); if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullException(nameof(name), "Name cannot be empty or null."); if (s_unicodeEmojis.TryGetValue(name, out var unicodeEntity)) { emoji = new DiscordEmoji { Discord = client, Name = unicodeEntity }; return true; } if (includeGuilds) { var allEmojis = client.Guilds.Values .SelectMany(xg => xg.Emojis.Values); // save cycles - don't order var ek = name.AsSpan().Slice(1, name.Length - 2); foreach (var xemoji in allEmojis) if (xemoji.Name.AsSpan().SequenceEqual(ek)) { emoji = xemoji; return true; } } emoji = null; return false; } } diff --git a/DisCatSharp/Entities/Guild/DiscordGuild.AuditLog.cs b/DisCatSharp/Entities/Guild/DiscordGuild.AuditLog.cs index 6a8b98d62..2a94f5c08 100644 --- a/DisCatSharp/Entities/Guild/DiscordGuild.AuditLog.cs +++ b/DisCatSharp/Entities/Guild/DiscordGuild.AuditLog.cs @@ -1,1293 +1,1294 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Globalization; using System.Linq; using System.Threading.Tasks; using DisCatSharp.Enums; using DisCatSharp.Net; using DisCatSharp.Net.Abstractions; using Microsoft.Extensions.Logging; using Newtonsoft.Json.Linq; namespace DisCatSharp.Entities; -public partial class DiscordGuild { +public partial class DiscordGuild +{ // TODO: Rework audit logs! /// /// Gets audit log entries for this guild. /// /// Maximum number of entries to fetch. /// Filter by member responsible. /// Filter by action type. /// A collection of requested audit log entries. /// Thrown when the client does not have the permission. /// Thrown when Discord is unable to process the request. public async Task> GetAuditLogsAsync(int? limit = null, DiscordMember byMember = null, AuditLogActionType? actionType = null) { var alrs = new List(); int ac = 1, tc = 0, rmn = 100; var last = 0ul; while (ac > 0) { rmn = limit != null ? limit.Value - tc : 100; rmn = Math.Min(100, rmn); if (rmn <= 0) break; var alr = await this.Discord.ApiClient.GetAuditLogsAsync(this.Id, rmn, null, last == 0 ? null : (ulong?)last, byMember?.Id, (int?)actionType).ConfigureAwait(false); ac = alr.Entries.Count(); tc += ac; if (ac > 0) { last = alr.Entries.Last().Id; alrs.Add(alr); } } var amr = alrs.SelectMany(xa => xa.Users) .GroupBy(xu => xu.Id) .Select(xgu => xgu.First()); foreach (var xau in amr) { if (this.Discord.UserCache.ContainsKey(xau.Id)) continue; var xtu = new TransportUser { Id = xau.Id, Username = xau.Username, Discriminator = xau.Discriminator, AvatarHash = xau.AvatarHash }; var xu = new DiscordUser(xtu) { Discord = this.Discord }; xu = this.Discord.UserCache.AddOrUpdate(xu.Id, xu, (id, old) => { old.Username = xu.Username; old.Discriminator = xu.Discriminator; old.AvatarHash = xu.AvatarHash; return old; }); } var atgse = alrs.SelectMany(xa => xa.ScheduledEvents) .GroupBy(xse => xse.Id) .Select(xgse => xgse.First()); var ath = alrs.SelectMany(xa => xa.Threads) .GroupBy(xt => xt.Id) .Select(xgt => xgt.First()); var aig = alrs.SelectMany(xa => xa.Integrations) .GroupBy(xi => xi.Id) .Select(xgi => xgi.First()); var ahr = alrs.SelectMany(xa => xa.Webhooks) .GroupBy(xh => xh.Id) .Select(xgh => xgh.First()); var ams = amr.Select(xau => this.MembersInternal != null && this.MembersInternal.TryGetValue(xau.Id, out var member) ? member : new DiscordMember { Discord = this.Discord, Id = xau.Id, GuildId = this.Id }); var amd = ams.ToDictionary(xm => xm.Id, xm => xm); #pragma warning disable CS0219 Dictionary dtc = null; Dictionary di = null; Dictionary dse = null; #pragma warning restore Dictionary ahd = null; if (ahr.Any()) { var whr = await this.GetWebhooksAsync().ConfigureAwait(false); var whs = whr.ToDictionary(xh => xh.Id, xh => xh); var amh = ahr.Select(xah => whs.TryGetValue(xah.Id, out var webhook) ? webhook : new DiscordWebhook { Discord = this.Discord, Name = xah.Name, Id = xah.Id, AvatarHash = xah.AvatarHash, ChannelId = xah.ChannelId, GuildId = xah.GuildId, Token = xah.Token }); ahd = amh.ToDictionary(xh => xh.Id, xh => xh); } var acs = alrs.SelectMany(xa => xa.Entries).OrderByDescending(xa => xa.Id); var entries = new List(); foreach (var xac in acs) { DiscordAuditLogEntry entry = null; ulong t1, t2; int t3, t4; long t5, t6; bool p1, p2; switch (xac.ActionType) { case AuditLogActionType.Invalid: break; case AuditLogActionType.GuildUpdate: entry = new DiscordAuditLogGuildEntry { Target = this }; var entrygld = entry as DiscordAuditLogGuildEntry; foreach (var xc in xac.Changes) { PropertyChange GetChannelChange() { ulong.TryParse(xc.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t1); ulong.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t2); return new PropertyChange { Before = this.GetChannel(t1) ?? new DiscordChannel { Id = t1, Discord = this.Discord, GuildId = this.Id }, After = this.GetChannel(t2) ?? new DiscordChannel { Id = t1, Discord = this.Discord, GuildId = this.Id } }; } switch (xc.Key.ToLowerInvariant()) { case "name": entrygld.NameChange = new PropertyChange { Before = xc.OldValueString, After = xc.NewValueString }; break; case "owner_id": entrygld.OwnerChange = new PropertyChange { Before = this.MembersInternal != null && this.MembersInternal.TryGetValue(xc.OldValueUlong, out var oldMember) ? oldMember : await this.GetMemberAsync(xc.OldValueUlong).ConfigureAwait(false), After = this.MembersInternal != null && this.MembersInternal.TryGetValue(xc.NewValueUlong, out var newMember) ? newMember : await this.GetMemberAsync(xc.NewValueUlong).ConfigureAwait(false) }; break; case "icon_hash": entrygld.IconChange = new PropertyChange { Before = xc.OldValueString != null ? $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.ICONS}/{this.Id}/{xc.OldValueString}.webp" : null, After = xc.OldValueString != null ? $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.ICONS}/{this.Id}/{xc.NewValueString}.webp" : null }; break; case "verification_level": entrygld.VerificationLevelChange = new PropertyChange { Before = (VerificationLevel)(long)xc.OldValue, After = (VerificationLevel)(long)xc.NewValue }; break; case "afk_channel_id": entrygld.AfkChannelChange = GetChannelChange(); break; case "system_channel_flags": entrygld.SystemChannelFlagsChange = new PropertyChange() { Before = (SystemChannelFlags)(long)xc.OldValue, After = (SystemChannelFlags)(long)xc.NewValue }; break; case "widget_channel_id": entrygld.WidgetChannelChange = GetChannelChange(); break; case "rules_channel_id": entrygld.RulesChannelChange = GetChannelChange(); break; case "public_updates_channel_id": entrygld.PublicUpdatesChannelChange = GetChannelChange(); break; case "splash_hash": entrygld.SplashChange = new PropertyChange { Before = xc.OldValueString != null ? $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.SPLASHES}/{this.Id}/{xc.OldValueString}.webp?size=2048" : null, After = xc.NewValueString != null ? $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.SPLASHES}/{this.Id}/{xc.NewValueString}.webp?size=2048" : null }; break; case "default_message_notifications": entrygld.NotificationSettingsChange = new PropertyChange { Before = (DefaultMessageNotifications)(long)xc.OldValue, After = (DefaultMessageNotifications)(long)xc.NewValue }; break; case "system_channel_id": entrygld.SystemChannelChange = GetChannelChange(); break; case "explicit_content_filter": entrygld.ExplicitContentFilterChange = new PropertyChange { Before = (ExplicitContentFilter)(long)xc.OldValue, After = (ExplicitContentFilter)(long)xc.NewValue }; break; case "mfa_level": entrygld.MfaLevelChange = new PropertyChange { Before = (MfaLevel)(long)xc.OldValue, After = (MfaLevel)(long)xc.NewValue }; break; case "region": entrygld.RegionChange = new PropertyChange { Before = xc.OldValueString, After = xc.NewValueString }; break; case "vanity_url_code": entrygld.VanityUrlCodeChange = new PropertyChange { Before = xc.OldValueString, After = xc.NewValueString }; break; case "premium_progress_bar_enabled": entrygld.PremiumProgressBarChange = new PropertyChange { Before = (bool)xc.OldValue, After = (bool)xc.NewValue }; break; default: this.Discord.Logger.LogWarning(LoggerEvents.AuditLog, "Unknown key in guild update: {0} - this should be reported to library developers", xc.Key); break; } } break; case AuditLogActionType.ChannelCreate: case AuditLogActionType.ChannelDelete: case AuditLogActionType.ChannelUpdate: entry = new DiscordAuditLogChannelEntry { Target = this.GetChannel(xac.TargetId.Value) ?? new DiscordChannel { Id = xac.TargetId.Value, Discord = this.Discord, GuildId = this.Id } }; var entrychn = entry as DiscordAuditLogChannelEntry; foreach (var xc in xac.Changes) { switch (xc.Key.ToLowerInvariant()) { case "name": entrychn.NameChange = new PropertyChange { Before = xc.OldValueString, After = xc.NewValueString }; break; case "type": p1 = ulong.TryParse(xc.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t1); p2 = ulong.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t2); entrychn.TypeChange = new PropertyChange { Before = p1 ? (ChannelType?)t1 : null, After = p2 ? (ChannelType?)t2 : null }; break; case "permission_overwrites": var olds = xc.OldValues?.OfType() ?.Select(xjo => xjo.ToObject()) ?.Select(xo => { xo.Discord = this.Discord; return xo; }); var news = xc.NewValues?.OfType() ?.Select(xjo => xjo.ToObject()) ?.Select(xo => { xo.Discord = this.Discord; return xo; }); entrychn.OverwriteChange = new PropertyChange> { Before = olds != null ? new ReadOnlyCollection(new List(olds)) : null, After = news != null ? new ReadOnlyCollection(new List(news)) : null }; break; case "topic": entrychn.TopicChange = new PropertyChange { Before = xc.OldValueString, After = xc.NewValueString }; break; case "nsfw": entrychn.NsfwChange = new PropertyChange { Before = (bool?)xc.OldValue, After = (bool?)xc.NewValue }; break; case "rtc_region": entrychn.RtcRegionIdChange = new PropertyChange { Before = xc.OldValueString, After = xc.NewValueString }; break; case "bitrate": entrychn.BitrateChange = new PropertyChange { Before = (int?)(long?)xc.OldValue, After = (int?)(long?)xc.NewValue }; break; case "user_limit": entrychn.UserLimitChange = new PropertyChange { Before = (int?)(long?)xc.OldValue, After = (int?)(long?)xc.NewValue }; break; case "rate_limit_per_user": entrychn.PerUserRateLimitChange = new PropertyChange { Before = (int?)(long?)xc.OldValue, After = (int?)(long?)xc.NewValue }; break; case "default_auto_archive_duration": entrychn.DefaultAutoArchiveDurationChange = new PropertyChange { Before = (ThreadAutoArchiveDuration?)(long?)xc.OldValue, After = (ThreadAutoArchiveDuration?)(long?)xc.NewValue }; break; default: this.Discord.Logger.LogWarning(LoggerEvents.AuditLog, "Unknown key in channel update: {0} - this should be reported to library developers", xc.Key); break; } } break; case AuditLogActionType.OverwriteCreate: case AuditLogActionType.OverwriteDelete: case AuditLogActionType.OverwriteUpdate: entry = new DiscordAuditLogOverwriteEntry { Target = this.GetChannel(xac.TargetId.Value)?.PermissionOverwrites.FirstOrDefault(xo => xo.Id == xac.Options.Id), Channel = this.GetChannel(xac.TargetId.Value) }; var entryovr = entry as DiscordAuditLogOverwriteEntry; foreach (var xc in xac.Changes) { switch (xc.Key.ToLowerInvariant()) { case "deny": p1 = ulong.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t1); p2 = ulong.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t2); entryovr.DenyChange = new PropertyChange { Before = p1 ? (Permissions?)t1 : null, After = p2 ? (Permissions?)t2 : null }; break; case "allow": p1 = ulong.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t1); p2 = ulong.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t2); entryovr.AllowChange = new PropertyChange { Before = p1 ? (Permissions?)t1 : null, After = p2 ? (Permissions?)t2 : null }; break; case "type": entryovr.TypeChange = new PropertyChange { Before = xc.OldValue != null ? (OverwriteType)(long)xc.OldValue : null, After = xc.NewValue != null ? (OverwriteType)(long)xc.NewValue : null }; break; case "id": p1 = ulong.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t1); p2 = ulong.TryParse(xc.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t2); entryovr.TargetIdChange = new PropertyChange { Before = p1 ? (ulong?)t1 : null, After = p2 ? (ulong?)t2 : null }; break; default: this.Discord.Logger.LogWarning(LoggerEvents.AuditLog, "Unknown key in overwrite update: {0} - this should be reported to library developers", xc.Key); break; } } break; case AuditLogActionType.Kick: entry = new DiscordAuditLogKickEntry { Target = amd.TryGetValue(xac.TargetId.Value, out var kickMember) ? kickMember : new DiscordMember { Id = xac.TargetId.Value, Discord = this.Discord, GuildId = this.Id } }; break; case AuditLogActionType.Prune: entry = new DiscordAuditLogPruneEntry { Days = xac.Options.DeleteMemberDays, Toll = xac.Options.MembersRemoved }; break; case AuditLogActionType.Ban: case AuditLogActionType.Unban: entry = new DiscordAuditLogBanEntry { Target = amd.TryGetValue(xac.TargetId.Value, out var unbanMember) ? unbanMember : new DiscordMember { Id = xac.TargetId.Value, Discord = this.Discord, GuildId = this.Id } }; break; case AuditLogActionType.MemberUpdate: case AuditLogActionType.MemberRoleUpdate: entry = new DiscordAuditLogMemberUpdateEntry { Target = amd.TryGetValue(xac.TargetId.Value, out var roleUpdMember) ? roleUpdMember : new DiscordMember { Id = xac.TargetId.Value, Discord = this.Discord, GuildId = this.Id } }; var entrymbu = entry as DiscordAuditLogMemberUpdateEntry; foreach (var xc in xac.Changes) { switch (xc.Key.ToLowerInvariant()) { case "nick": entrymbu.NicknameChange = new PropertyChange { Before = xc.OldValueString, After = xc.NewValueString }; break; case "deaf": entrymbu.DeafenChange = new PropertyChange { Before = (bool?)xc.OldValue, After = (bool?)xc.NewValue }; break; case "mute": entrymbu.MuteChange = new PropertyChange { Before = (bool?)xc.OldValue, After = (bool?)xc.NewValue }; break; case "communication_disabled_until": entrymbu.CommunicationDisabledUntilChange = new PropertyChange { Before = (DateTime?)xc.OldValue, After = (DateTime?)xc.NewValue }; break; case "$add": entrymbu.AddedRoles = new ReadOnlyCollection(xc.NewValues.Select(xo => (ulong)xo["id"]).Select(this.GetRole).ToList()); break; case "$remove": entrymbu.RemovedRoles = new ReadOnlyCollection(xc.NewValues.Select(xo => (ulong)xo["id"]).Select(this.GetRole).ToList()); break; default: this.Discord.Logger.LogWarning(LoggerEvents.AuditLog, "Unknown key in member update: {0} - this should be reported to library developers", xc.Key); break; } } break; case AuditLogActionType.RoleCreate: case AuditLogActionType.RoleDelete: case AuditLogActionType.RoleUpdate: entry = new DiscordAuditLogRoleUpdateEntry { Target = this.GetRole(xac.TargetId.Value) ?? new DiscordRole { Id = xac.TargetId.Value, Discord = this.Discord } }; var entryrol = entry as DiscordAuditLogRoleUpdateEntry; foreach (var xc in xac.Changes) { switch (xc.Key.ToLowerInvariant()) { case "name": entryrol.NameChange = new PropertyChange { Before = xc.OldValueString, After = xc.NewValueString }; break; case "color": p1 = int.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t3); p2 = int.TryParse(xc.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t4); entryrol.ColorChange = new PropertyChange { Before = p1 ? (int?)t3 : null, After = p2 ? (int?)t4 : null }; break; case "permissions": entryrol.PermissionChange = new PropertyChange { Before = xc.OldValue != null ? (Permissions?)long.Parse((string)xc.OldValue) : null, After = xc.NewValue != null ? (Permissions?)long.Parse((string)xc.NewValue) : null }; break; case "position": entryrol.PositionChange = new PropertyChange { Before = xc.OldValue != null ? (int?)(long)xc.OldValue : null, After = xc.NewValue != null ? (int?)(long)xc.NewValue : null, }; break; case "mentionable": entryrol.MentionableChange = new PropertyChange { Before = xc.OldValue != null ? (bool?)xc.OldValue : null, After = xc.NewValue != null ? (bool?)xc.NewValue : null }; break; case "hoist": entryrol.HoistChange = new PropertyChange { Before = (bool?)xc.OldValue, After = (bool?)xc.NewValue }; break; case "icon_hash": entryrol.IconHashChange = new PropertyChange { Before = xc.OldValueString, After = xc.NewValueString }; break; default: this.Discord.Logger.LogWarning(LoggerEvents.AuditLog, "Unknown key in role update: {0} - this should be reported to library developers", xc.Key); break; } } break; case AuditLogActionType.InviteCreate: case AuditLogActionType.InviteDelete: case AuditLogActionType.InviteUpdate: entry = new DiscordAuditLogInviteEntry(); var inv = new DiscordInvite { Discord = this.Discord, Guild = new DiscordInviteGuild { Discord = this.Discord, Id = this.Id, Name = this.Name, SplashHash = this.SplashHash } }; var entryinv = entry as DiscordAuditLogInviteEntry; foreach (var xc in xac.Changes) { switch (xc.Key.ToLowerInvariant()) { case "max_age": p1 = int.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t3); p2 = int.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t4); entryinv.MaxAgeChange = new PropertyChange { Before = p1 ? (int?)t3 : null, After = p2 ? (int?)t4 : null }; break; case "code": inv.Code = xc.OldValueString ?? xc.NewValueString; entryinv.CodeChange = new PropertyChange { Before = xc.OldValueString, After = xc.NewValueString }; break; case "temporary": entryinv.TemporaryChange = new PropertyChange { Before = xc.OldValue != null ? (bool?)xc.OldValue : null, After = xc.NewValue != null ? (bool?)xc.NewValue : null }; break; case "inviter_id": p1 = ulong.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t1); p2 = ulong.TryParse(xc.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t2); entryinv.InviterChange = new PropertyChange { Before = amd.TryGetValue(t1, out var propBeforeMember) ? propBeforeMember : new DiscordMember { Id = t1, Discord = this.Discord, GuildId = this.Id }, After = amd.TryGetValue(t2, out var propAfterMember) ? propAfterMember : new DiscordMember { Id = t1, Discord = this.Discord, GuildId = this.Id }, }; break; case "channel_id": p1 = ulong.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t1); p2 = ulong.TryParse(xc.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t2); entryinv.ChannelChange = new PropertyChange { Before = p1 ? this.GetChannel(t1) ?? new DiscordChannel { Id = t1, Discord = this.Discord, GuildId = this.Id } : null, After = p2 ? this.GetChannel(t2) ?? new DiscordChannel { Id = t1, Discord = this.Discord, GuildId = this.Id } : null }; var ch = entryinv.ChannelChange.Before ?? entryinv.ChannelChange.After; var cht = ch?.Type; inv.Channel = new DiscordInviteChannel { Discord = this.Discord, Id = p1 ? t1 : t2, Name = ch?.Name, Type = cht != null ? cht.Value : ChannelType.Unknown }; break; case "uses": p1 = int.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t3); p2 = int.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t4); entryinv.UsesChange = new PropertyChange { Before = p1 ? (int?)t3 : null, After = p2 ? (int?)t4 : null }; break; case "max_uses": p1 = int.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t3); p2 = int.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t4); entryinv.MaxUsesChange = new PropertyChange { Before = p1 ? (int?)t3 : null, After = p2 ? (int?)t4 : null }; break; // TODO: Add changes for target application default: this.Discord.Logger.LogWarning(LoggerEvents.AuditLog, "Unknown key in invite update: {0} - this should be reported to library developers", xc.Key); break; } } entryinv.Target = inv; break; case AuditLogActionType.WebhookCreate: case AuditLogActionType.WebhookDelete: case AuditLogActionType.WebhookUpdate: entry = new DiscordAuditLogWebhookEntry { Target = ahd.TryGetValue(xac.TargetId.Value, out var webhook) ? webhook : new DiscordWebhook { Id = xac.TargetId.Value, Discord = this.Discord } }; var entrywhk = entry as DiscordAuditLogWebhookEntry; foreach (var xc in xac.Changes) { switch (xc.Key.ToLowerInvariant()) { case "application_id": // ??? p1 = ulong.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t1); p2 = ulong.TryParse(xc.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t2); entrywhk.IdChange = new PropertyChange { Before = p1 ? t1 : null, After = p2 ? t2 : null }; break; case "name": entrywhk.NameChange = new PropertyChange { Before = xc.OldValueString, After = xc.NewValueString }; break; case "channel_id": p1 = ulong.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t1); p2 = ulong.TryParse(xc.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t2); entrywhk.ChannelChange = new PropertyChange { Before = p1 ? this.GetChannel(t1) ?? new DiscordChannel { Id = t1, Discord = this.Discord, GuildId = this.Id } : null, After = p2 ? this.GetChannel(t2) ?? new DiscordChannel { Id = t1, Discord = this.Discord, GuildId = this.Id } : null }; break; case "type": // ??? p1 = int.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t3); p2 = int.TryParse(xc.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t4); entrywhk.TypeChange = new PropertyChange { Before = p1 ? (int?)t3 : null, After = p2 ? (int?)t4 : null }; break; case "avatar_hash": entrywhk.AvatarHashChange = new PropertyChange { Before = xc.OldValueString, After = xc.NewValueString }; break; default: this.Discord.Logger.LogWarning(LoggerEvents.AuditLog, "Unknown key in webhook update: {0} - this should be reported to library developers", xc.Key); break; } } break; case AuditLogActionType.EmojiCreate: case AuditLogActionType.EmojiDelete: case AuditLogActionType.EmojiUpdate: entry = new DiscordAuditLogEmojiEntry { Target = this.EmojisInternal.TryGetValue(xac.TargetId.Value, out var target) ? target : new DiscordEmoji { Id = xac.TargetId.Value, Discord = this.Discord } }; var entryemo = entry as DiscordAuditLogEmojiEntry; foreach (var xc in xac.Changes) { switch (xc.Key.ToLowerInvariant()) { case "name": entryemo.NameChange = new PropertyChange { Before = xc.OldValueString, After = xc.NewValueString }; break; default: this.Discord.Logger.LogWarning(LoggerEvents.AuditLog, "Unknown key in emote update: {0} - this should be reported to library developers", xc.Key); break; } } break; case AuditLogActionType.StageInstanceCreate: case AuditLogActionType.StageInstanceDelete: case AuditLogActionType.StageInstanceUpdate: entry = new DiscordAuditLogStageEntry { Target = this.StageInstancesInternal.TryGetValue(xac.TargetId.Value, out var stage) ? stage : new DiscordStageInstance { Id = xac.TargetId.Value, Discord = this.Discord } }; var entrysta = entry as DiscordAuditLogStageEntry; foreach (var xc in xac.Changes) { switch (xc.Key.ToLowerInvariant()) { case "topic": entrysta.TopicChange = new PropertyChange { Before = xc.OldValueString, After = xc.NewValueString }; break; case "privacy_level": entrysta.PrivacyLevelChange = new PropertyChange { Before = long.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t5) ? (StagePrivacyLevel?)t5 : null, After = long.TryParse(xc.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t6) ? (StagePrivacyLevel?)t6 : null, }; break; default: this.Discord.Logger.LogWarning(LoggerEvents.AuditLog, "Unknown key in stage instance update: {0} - this should be reported to library developers", xc.Key); break; } } break; case AuditLogActionType.StickerCreate: case AuditLogActionType.StickerDelete: case AuditLogActionType.StickerUpdate: entry = new DiscordAuditLogStickerEntry { Target = this.StickersInternal.TryGetValue(xac.TargetId.Value, out var sticker) ? sticker : new DiscordSticker { Id = xac.TargetId.Value, Discord = this.Discord } }; var entrysti = entry as DiscordAuditLogStickerEntry; foreach (var xc in xac.Changes) { switch (xc.Key.ToLowerInvariant()) { case "name": entrysti.NameChange = new PropertyChange { Before = xc.OldValueString, After = xc.NewValueString }; break; case "description": entrysti.DescriptionChange = new PropertyChange { Before = xc.OldValueString, After = xc.NewValueString }; break; case "tags": entrysti.TagsChange = new PropertyChange { Before = xc.OldValueString, After = xc.NewValueString }; break; case "guild_id": entrysti.GuildIdChange = new PropertyChange { Before = ulong.TryParse(xc.OldValueString, out var ogid) ? ogid : null, After = ulong.TryParse(xc.NewValueString, out var ngid) ? ngid : null }; break; case "available": entrysti.AvailabilityChange = new PropertyChange { Before = (bool?)xc.OldValue, After = (bool?)xc.NewValue, }; break; case "asset": entrysti.AssetChange = new PropertyChange { Before = xc.OldValueString, After = xc.NewValueString }; break; case "id": entrysti.IdChange = new PropertyChange { Before = ulong.TryParse(xc.OldValueString, out var oid) ? oid : null, After = ulong.TryParse(xc.NewValueString, out var nid) ? nid : null }; break; case "type": p1 = long.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t5); p2 = long.TryParse(xc.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t6); entrysti.TypeChange = new PropertyChange { Before = p1 ? (StickerType?)t5 : null, After = p2 ? (StickerType?)t6 : null }; break; case "format_type": p1 = long.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t5); p2 = long.TryParse(xc.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t6); entrysti.FormatChange = new PropertyChange { Before = p1 ? (StickerFormat?)t5 : null, After = p2 ? (StickerFormat?)t6 : null }; break; default: this.Discord.Logger.LogWarning(LoggerEvents.AuditLog, "Unknown key in sticker update: {0} - this should be reported to library developers", xc.Key); break; } } break; case AuditLogActionType.MessageDelete: case AuditLogActionType.MessageBulkDelete: { entry = new DiscordAuditLogMessageEntry(); var entrymsg = entry as DiscordAuditLogMessageEntry; if (xac.Options != null) { entrymsg.Channel = this.GetChannel(xac.Options.ChannelId) ?? new DiscordChannel { Id = xac.Options.ChannelId, Discord = this.Discord, GuildId = this.Id }; entrymsg.MessageCount = xac.Options.Count; } if (entrymsg.Channel != null) { entrymsg.Target = this.Discord is DiscordClient dc && dc.MessageCache != null && dc.MessageCache.TryGet(xm => xm.Id == xac.TargetId.Value && xm.ChannelId == entrymsg.Channel.Id, out var msg) ? msg : new DiscordMessage { Discord = this.Discord, Id = xac.TargetId.Value }; } break; } case AuditLogActionType.MessagePin: case AuditLogActionType.MessageUnpin: { entry = new DiscordAuditLogMessagePinEntry(); var entrypin = entry as DiscordAuditLogMessagePinEntry; if (this.Discord is not DiscordClient dc) { break; } if (xac.Options != null) { DiscordMessage message = default; dc.MessageCache?.TryGet(x => x.Id == xac.Options.MessageId && x.ChannelId == xac.Options.ChannelId, out message); entrypin.Channel = this.GetChannel(xac.Options.ChannelId) ?? new DiscordChannel { Id = xac.Options.ChannelId, Discord = this.Discord, GuildId = this.Id }; entrypin.Message = message ?? new DiscordMessage { Id = xac.Options.MessageId, Discord = this.Discord }; } if (xac.TargetId.HasValue) { dc.UserCache.TryGetValue(xac.TargetId.Value, out var user); entrypin.Target = user ?? new DiscordUser { Id = user.Id, Discord = this.Discord }; } break; } case AuditLogActionType.BotAdd: { entry = new DiscordAuditLogBotAddEntry(); if (!(this.Discord is DiscordClient dc && xac.TargetId.HasValue)) { break; } dc.UserCache.TryGetValue(xac.TargetId.Value, out var bot); (entry as DiscordAuditLogBotAddEntry).TargetBot = bot ?? new DiscordUser { Id = xac.TargetId.Value, Discord = this.Discord }; break; } case AuditLogActionType.MemberMove: entry = new DiscordAuditLogMemberMoveEntry(); if (xac.Options == null) { break; } var moveentry = entry as DiscordAuditLogMemberMoveEntry; moveentry.UserCount = xac.Options.Count; moveentry.Channel = this.GetChannel(xac.Options.ChannelId) ?? new DiscordChannel { Id = xac.Options.ChannelId, Discord = this.Discord, GuildId = this.Id }; break; case AuditLogActionType.MemberDisconnect: entry = new DiscordAuditLogMemberDisconnectEntry { UserCount = xac.Options?.Count ?? 0 }; break; case AuditLogActionType.IntegrationCreate: case AuditLogActionType.IntegrationDelete: case AuditLogActionType.IntegrationUpdate: entry = new DiscordAuditLogIntegrationEntry(); var integentry = entry as DiscordAuditLogIntegrationEntry; foreach (var xc in xac.Changes) { switch (xc.Key.ToLowerInvariant()) { case "type": integentry.Type = new PropertyChange() { Before = xc.OldValueString, After = xc.NewValueString }; break; case "enable_emoticons": integentry.EnableEmoticons = new PropertyChange { Before = (bool?)xc.OldValue, After = (bool?)xc.NewValue }; break; case "expire_behavior": integentry.ExpireBehavior = new PropertyChange { Before = (int?)xc.OldValue, After = (int?)xc.NewValue }; break; case "expire_grace_period": integentry.ExpireBehavior = new PropertyChange { Before = (int?)xc.OldValue, After = (int?)xc.NewValue }; break; default: this.Discord.Logger.LogWarning(LoggerEvents.AuditLog, "Unknown key in integration update: {0} - this should be reported to library developers", xc.Key); break; } } break; case AuditLogActionType.ThreadCreate: case AuditLogActionType.ThreadDelete: case AuditLogActionType.ThreadUpdate: entry = new DiscordAuditLogThreadEntry { Target = this.ThreadsInternal.TryGetValue(xac.TargetId.Value, out var thread) ? thread : new DiscordThreadChannel { Id = xac.TargetId.Value, Discord = this.Discord } }; var entrythr = entry as DiscordAuditLogThreadEntry; foreach (var xc in xac.Changes) { switch (xc.Key.ToLowerInvariant()) { case "name": entrythr.NameChange = new PropertyChange { Before = xc.OldValueString, After = xc.NewValueString }; break; case "type": p1 = ulong.TryParse(xc.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t1); p2 = ulong.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t2); entrythr.TypeChange = new PropertyChange { Before = p1 ? (ChannelType?)t1 : null, After = p2 ? (ChannelType?)t2 : null }; break; case "archived": entrythr.ArchivedChange = new PropertyChange { Before = xc.OldValue != null ? (bool?)xc.OldValue : null, After = xc.NewValue != null ? (bool?)xc.NewValue : null }; break; case "locked": entrythr.LockedChange = new PropertyChange { Before = xc.OldValue != null ? (bool?)xc.OldValue : null, After = xc.NewValue != null ? (bool?)xc.NewValue : null }; break; case "invitable": entrythr.InvitableChange = new PropertyChange { Before = xc.OldValue != null ? (bool?)xc.OldValue : null, After = xc.NewValue != null ? (bool?)xc.NewValue : null }; break; case "auto_archive_duration": p1 = long.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t5); p2 = long.TryParse(xc.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t6); entrythr.AutoArchiveDurationChange = new PropertyChange { Before = p1 ? (ThreadAutoArchiveDuration?)t5 : null, After = p2 ? (ThreadAutoArchiveDuration?)t6 : null }; break; case "rate_limit_per_user": entrythr.PerUserRateLimitChange = new PropertyChange { Before = (int?)(long?)xc.OldValue, After = (int?)(long?)xc.NewValue }; break; default: this.Discord.Logger.LogWarning(LoggerEvents.AuditLog, "Unknown key in thread update: {0} - this should be reported to library developers", xc.Key); break; } } break; case AuditLogActionType.GuildScheduledEventCreate: case AuditLogActionType.GuildScheduledEventDelete: case AuditLogActionType.GuildScheduledEventUpdate: entry = new DiscordAuditLogGuildScheduledEventEntry { Target = this.ScheduledEventsInternal.TryGetValue(xac.TargetId.Value, out var scheduledEvent) ? scheduledEvent : new DiscordScheduledEvent { Id = xac.TargetId.Value, Discord = this.Discord } }; var entryse = entry as DiscordAuditLogGuildScheduledEventEntry; foreach (var xc in xac.Changes) { switch (xc.Key.ToLowerInvariant()) { case "channel_id": entryse.ChannelIdChange = new PropertyChange { Before = ulong.TryParse(xc.OldValueString, out var ogid) ? ogid : null, After = ulong.TryParse(xc.NewValueString, out var ngid) ? ngid : null }; break; case "name": entryse.NameChange = new PropertyChange { Before = xc.OldValueString, After = xc.NewValueString }; break; case "description": entryse.DescriptionChange = new PropertyChange { Before = xc.OldValueString, After = xc.NewValueString }; break; case "location": entryse.LocationChange = new PropertyChange { Before = xc.OldValueString, After = xc.NewValueString }; break; case "privacy_level": p1 = long.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t5); p2 = long.TryParse(xc.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t6); entryse.PrivacyLevelChange = new PropertyChange { Before = p1 ? (ScheduledEventPrivacyLevel?)t5 : null, After = p2 ? (ScheduledEventPrivacyLevel?)t6 : null }; break; case "entity_type": p1 = long.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t5); p2 = long.TryParse(xc.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t6); entryse.EntityTypeChange = new PropertyChange { Before = p1 ? (ScheduledEventEntityType?)t5 : null, After = p2 ? (ScheduledEventEntityType?)t6 : null }; break; case "status": p1 = long.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t5); p2 = long.TryParse(xc.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t6); entryse.StatusChange = new PropertyChange { Before = p1 ? (ScheduledEventStatus?)t5 : null, After = p2 ? (ScheduledEventStatus?)t6 : null }; break; default: this.Discord.Logger.LogWarning(LoggerEvents.AuditLog, "Unknown key in scheduled event update: {0} - this should be reported to library developers", xc.Key); break; } } break; default: this.Discord.Logger.LogWarning(LoggerEvents.AuditLog, "Unknown audit log action type: {0} - this should be reported to library developers", (int)xac.ActionType); break; } if (entry == null) continue; entry.ActionCategory = xac.ActionType switch { AuditLogActionType.ChannelCreate or AuditLogActionType.EmojiCreate or AuditLogActionType.InviteCreate or AuditLogActionType.OverwriteCreate or AuditLogActionType.RoleCreate or AuditLogActionType.WebhookCreate or AuditLogActionType.IntegrationCreate or AuditLogActionType.StickerCreate or AuditLogActionType.StageInstanceCreate or AuditLogActionType.ThreadCreate or AuditLogActionType.GuildScheduledEventCreate => AuditLogActionCategory.Create, AuditLogActionType.ChannelDelete or AuditLogActionType.EmojiDelete or AuditLogActionType.InviteDelete or AuditLogActionType.MessageDelete or AuditLogActionType.MessageBulkDelete or AuditLogActionType.OverwriteDelete or AuditLogActionType.RoleDelete or AuditLogActionType.WebhookDelete or AuditLogActionType.IntegrationDelete or AuditLogActionType.StickerDelete or AuditLogActionType.StageInstanceDelete or AuditLogActionType.ThreadDelete or AuditLogActionType.GuildScheduledEventDelete => AuditLogActionCategory.Delete, AuditLogActionType.ChannelUpdate or AuditLogActionType.EmojiUpdate or AuditLogActionType.InviteUpdate or AuditLogActionType.MemberRoleUpdate or AuditLogActionType.MemberUpdate or AuditLogActionType.OverwriteUpdate or AuditLogActionType.RoleUpdate or AuditLogActionType.WebhookUpdate or AuditLogActionType.IntegrationUpdate or AuditLogActionType.StickerUpdate or AuditLogActionType.StageInstanceUpdate or AuditLogActionType.ThreadUpdate or AuditLogActionType.GuildScheduledEventUpdate => AuditLogActionCategory.Update, _ => AuditLogActionCategory.Other, }; entry.Discord = this.Discord; entry.ActionType = xac.ActionType; entry.Id = xac.Id; entry.Reason = xac.Reason; entry.UserResponsible = amd[xac.UserId]; entries.Add(entry); } return new ReadOnlyCollection(entries); } } diff --git a/DisCatSharp/Entities/Guild/ScheduledEvent/DiscordScheduledEvent.cs b/DisCatSharp/Entities/Guild/ScheduledEvent/DiscordScheduledEvent.cs index ac14114d3..11486017b 100644 --- a/DisCatSharp/Entities/Guild/ScheduledEvent/DiscordScheduledEvent.cs +++ b/DisCatSharp/Entities/Guild/ScheduledEvent/DiscordScheduledEvent.cs @@ -1,327 +1,327 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Generic; using System.Globalization; using System.Threading.Tasks; using DisCatSharp.Enums; using DisCatSharp.Net; using DisCatSharp.Net.Models; using Newtonsoft.Json; namespace DisCatSharp.Entities; /// /// Represents an scheduled event. /// public class DiscordScheduledEvent : SnowflakeObject, IEquatable { /// /// Gets the guild id of the associated scheduled event. /// [JsonProperty("guild_id", NullValueHandling = NullValueHandling.Ignore)] public ulong GuildId { get; internal set; } /// /// Gets the guild to which this scheduled event belongs. /// [JsonIgnore] public DiscordGuild Guild => this.Discord.Guilds.TryGetValue(this.GuildId, out var guild) ? guild : null; /// /// Gets the associated channel. /// [JsonIgnore] public Task Channel => this.ChannelId.HasValue ? this.Discord.ApiClient.GetChannelAsync(this.ChannelId.Value) : null; /// /// Gets id of the associated channel id. /// [JsonProperty("channel_id", NullValueHandling = NullValueHandling.Ignore)] public ulong? ChannelId { get; internal set; } /// /// Gets the ID of the user that created the scheduled event. /// [JsonProperty("creator_id")] public ulong CreatorId { get; internal set; } /// /// Gets the user that created the scheduled event. /// [JsonProperty("creator")] public DiscordUser Creator { get; internal set; } /// /// Gets the member that created the scheduled event. /// [JsonIgnore] public DiscordMember CreatorMember => this.Guild.MembersInternal.TryGetValue(this.CreatorId, out var owner) ? owner : this.Discord.ApiClient.GetGuildMemberAsync(this.GuildId, this.CreatorId).Result; /// /// Gets the name of the scheduled event. /// [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] public string Name { get; internal set; } /// /// Gets the description of the scheduled event. /// [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] public string Description { get; internal set; } - /// - /// Gets this event's cover hash, when applicable. - /// - [JsonProperty("image", NullValueHandling = NullValueHandling.Include)] - public string CoverImageHash { get; internal set; } + /// + /// Gets this event's cover hash, when applicable. + /// + [JsonProperty("image", NullValueHandling = NullValueHandling.Include)] + public string CoverImageHash { get; internal set; } - /// - /// Gets this event's cover in url form. - /// - [JsonIgnore] - public string CoverImageUrl - => !string.IsNullOrWhiteSpace(this.CoverImageHash) ? $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Uri}{Endpoints.GUILD_EVENTS}/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.CoverImageHash}.png" : null; + /// + /// Gets this event's cover in url form. + /// + [JsonIgnore] + public string CoverImageUrl + => !string.IsNullOrWhiteSpace(this.CoverImageHash) ? $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Uri}{Endpoints.GUILD_EVENTS}/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.CoverImageHash}.png" : null; /// /// Gets the scheduled start time of the scheduled event. /// [JsonIgnore] public DateTimeOffset? ScheduledStartTime => !string.IsNullOrWhiteSpace(this.ScheduledStartTimeRaw) && DateTimeOffset.TryParse(this.ScheduledStartTimeRaw, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dto) ? dto : null; /// /// Gets the scheduled start time of the scheduled event as raw string. /// [JsonProperty("scheduled_start_time", NullValueHandling = NullValueHandling.Ignore)] internal string ScheduledStartTimeRaw { get; set; } /// /// Gets the scheduled end time of the scheduled event. /// [JsonIgnore] public DateTimeOffset? ScheduledEndTime => !string.IsNullOrWhiteSpace(this.ScheduledEndTimeRaw) && DateTimeOffset.TryParse(this.ScheduledEndTimeRaw, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dto) ? dto : null; /// /// Gets the scheduled end time of the scheduled event as raw string. /// [JsonProperty("scheduled_end_time", NullValueHandling = NullValueHandling.Ignore)] internal string ScheduledEndTimeRaw { get; set; } /// /// Gets the privacy level of the scheduled event. /// [JsonProperty("privacy_level", NullValueHandling = NullValueHandling.Ignore)] internal ScheduledEventPrivacyLevel PrivacyLevel { get; set; } /// /// Gets the status of the scheduled event. /// [JsonProperty("status", NullValueHandling = NullValueHandling.Ignore)] public ScheduledEventStatus Status { get; internal set; } /// /// Gets the entity type. /// [JsonProperty("entity_type", NullValueHandling = NullValueHandling.Ignore)] public ScheduledEventEntityType EntityType { get; internal set; } /// /// Gets id of the entity. /// [JsonProperty("entity_id", NullValueHandling = NullValueHandling.Ignore)] public ulong? EntityId { get; internal set; } /// /// Gets metadata of the entity. /// [JsonProperty("entity_metadata", NullValueHandling = NullValueHandling.Ignore)] public DiscordScheduledEventEntityMetadata EntityMetadata { get; internal set; } /* This isn't used. * See https://github.com/discord/discord-api-docs/pull/3586#issuecomment-969066061. * Was originally for paid stages. /// /// Gets the sku ids of the scheduled event. /// [JsonProperty("sku_ids", NullValueHandling = NullValueHandling.Ignore)] public IReadOnlyList SkuIds { get; internal set; }*/ /// /// Gets the total number of users subscribed to the scheduled event. /// [JsonProperty("user_count", NullValueHandling = NullValueHandling.Ignore)] public int UserCount { get; internal set; } /// /// Initializes a new instance of the class. /// internal DiscordScheduledEvent() { } #region Methods /// /// Modifies the current scheduled event. /// /// Action to perform on this thread /// Thrown when the client does not have the permission. /// Thrown when the event does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task ModifyAsync(Action action) { var mdl = new ScheduledEventEditModel(); action(mdl); Optional channelId = null; if (this.EntityType == ScheduledEventEntityType.External || mdl.EntityType != ScheduledEventEntityType.External) channelId = mdl.Channel .MapOrNull(c => c.Type != ChannelType.Voice && c.Type != ChannelType.Stage ? throw new ArgumentException("Channel needs to be a voice or stage channel.") : c.Id); var coverb64 = ImageTool.Base64FromStream(mdl.CoverImage); var scheduledEndTime = Optional.None; if (mdl.ScheduledEndTime.HasValue && mdl.EntityType.HasValue ? mdl.EntityType == ScheduledEventEntityType.External : this.EntityType == ScheduledEventEntityType.External) scheduledEndTime = mdl.ScheduledEndTime.Value; await this.Discord.ApiClient.ModifyGuildScheduledEventAsync(this.GuildId, this.Id, channelId, this.EntityType == ScheduledEventEntityType.External ? new DiscordScheduledEventEntityMetadata(mdl.Location.Value) : null, mdl.Name, mdl.ScheduledStartTime, scheduledEndTime, mdl.Description, mdl.EntityType, mdl.Status, coverb64, mdl.AuditLogReason); } /// /// Starts the current scheduled event. /// /// Thrown when the client does not have the permission. /// Thrown when the event does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task StartAsync(string reason = null) => this.Status == ScheduledEventStatus.Scheduled ? await this.Discord.ApiClient.ModifyGuildScheduledEventStatusAsync(this.GuildId, this.Id, ScheduledEventStatus.Active, reason) : throw new InvalidOperationException("You can only start scheduled events"); /// /// Cancels the current scheduled event. /// /// The audit log reason. /// Thrown when the client does not have the permission. /// Thrown when the event does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task CancelAsync(string reason = null) => this.Status == ScheduledEventStatus.Scheduled ? await this.Discord.ApiClient.ModifyGuildScheduledEventStatusAsync(this.GuildId, this.Id, ScheduledEventStatus.Canceled, reason) : throw new InvalidOperationException("You can only cancel scheduled events"); /// /// Ends the current scheduled event. /// /// The audit log reason. /// Thrown when the client does not have the permission. /// Thrown when the event does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task EndAsync(string reason = null) => this.Status == ScheduledEventStatus.Active ? await this.Discord.ApiClient.ModifyGuildScheduledEventStatusAsync(this.GuildId, this.Id, ScheduledEventStatus.Completed, reason) : throw new InvalidOperationException("You can only stop active events"); /// /// Gets a list of users RSVP'd to the scheduled event. /// /// The limit how many users to receive from the event. Defaults to 100. Max 100. /// Get results of before the given snowflake. /// Get results of after the given snowflake. /// Whether to include guild member data. /// Thrown when the client does not have the correct permissions. /// Thrown when the event does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task> GetUsersAsync(int? limit = null, ulong? before = null, ulong? after = null, bool? withMember = null) => await this.Discord.ApiClient.GetGuildScheduledEventRspvUsersAsync(this.GuildId, this.Id, limit, before, after, withMember); /// /// Deletes a scheduled event. /// /// The audit log reason. /// Thrown when the client does not have the permission. /// Thrown when the event does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task DeleteAsync(string reason = null) => await this.Discord.ApiClient.DeleteGuildScheduledEventAsync(this.GuildId, this.Id, reason); #endregion /// /// Checks whether this is equal to another object. /// /// Object to compare to. /// Whether the object is equal to this . public override bool Equals(object obj) => this.Equals(obj as DiscordScheduledEvent); /// /// Checks whether this is equal to another . /// /// to compare to. /// Whether the is equal to this . public bool Equals(DiscordScheduledEvent e) => e is not null && (ReferenceEquals(this, e) || this.Id == e.Id); /// /// Gets the hash code for this . /// /// The hash code for this . public override int GetHashCode() => this.Id.GetHashCode(); /// /// Gets whether the two objects are equal. /// /// First event to compare. /// Second event to compare. /// Whether the two events are equal. public static bool operator ==(DiscordScheduledEvent e1, DiscordScheduledEvent e2) { var o1 = e1 as object; var o2 = e2 as object; return (o1 != null || o2 == null) && (o1 == null || o2 != null) && ((o1 == null && o2 == null) || e1.Id == e2.Id); } /// /// Gets whether the two objects are not equal. /// /// First event to compare. /// Second event to compare. /// Whether the two events are not equal. public static bool operator !=(DiscordScheduledEvent e1, DiscordScheduledEvent e2) => !(e1 == e2); } diff --git a/DisCatSharp/Entities/Guild/ScheduledEvent/DiscordScheduledEventUser.cs b/DisCatSharp/Entities/Guild/ScheduledEvent/DiscordScheduledEventUser.cs index 0efd60990..2bfb499cc 100644 --- a/DisCatSharp/Entities/Guild/ScheduledEvent/DiscordScheduledEventUser.cs +++ b/DisCatSharp/Entities/Guild/ScheduledEvent/DiscordScheduledEventUser.cs @@ -1,123 +1,123 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using Newtonsoft.Json; namespace DisCatSharp.Entities; /// /// The discord scheduled event user. /// public class DiscordScheduledEventUser : IEquatable { /// /// Gets the client instance this object is tied to. /// [JsonIgnore] internal BaseDiscordClient Discord { get; set; } /// /// Gets the user. /// [JsonProperty("user")] public DiscordUser User { get; internal set; } /// /// Gets the member. /// Only applicable when requested with `with_member`. /// [JsonProperty("member", NullValueHandling = NullValueHandling.Ignore)] internal DiscordMember Member { get; set; } /// /// Gets the scheduled event. /// [JsonIgnore] public DiscordScheduledEvent ScheduledEvent => this.Discord.Guilds.TryGetValue(this.GuildId, out var guild) && guild.ScheduledEvents.TryGetValue(this.EventId, out var scheduledEvent) ? scheduledEvent : null; /// /// Gets or sets the event id. /// [JsonProperty("guild_scheduled_event_id")] internal ulong EventId { get; set; } /// /// Gets or sets the guild id. /// [JsonIgnore] internal ulong GuildId { get; set; } /// /// Initializes a new instance of the class. /// internal DiscordScheduledEventUser() -{ } + { } /// /// Checks whether this is equal to another object. /// /// Object to compare to. /// Whether the object is equal to this . public override bool Equals(object obj) => this.Equals(obj as DiscordScheduledEventUser); /// /// Checks whether this is equal to another . /// /// to compare to. /// Whether the is equal to this . public bool Equals(DiscordScheduledEventUser e) => e is not null && (ReferenceEquals(this, e) || HashCode.Combine(this.User.Id, this.EventId) == HashCode.Combine(e.User.Id, e.EventId)); /// /// Gets the hash code for this . /// /// The hash code for this . public override int GetHashCode() => HashCode.Combine(this.User.Id, this.EventId); /// /// Gets whether the two objects are equal. /// /// First event to compare. /// Second event to compare. /// Whether the two events are equal. public static bool operator ==(DiscordScheduledEventUser e1, DiscordScheduledEventUser e2) { var o1 = e1 as object; var o2 = e2 as object; return (o1 != null || o2 == null) && (o1 == null || o2 != null) && ((o1 == null && o2 == null) || HashCode.Combine(e1.User.Id, e1.EventId) == HashCode.Combine(e2.User.Id, e2.EventId)); } /// /// Gets whether the two objects are not equal. /// /// First event to compare. /// Second event to compare. /// Whether the two events are not equal. public static bool operator !=(DiscordScheduledEventUser e1, DiscordScheduledEventUser e2) => !(e1 == e2); } diff --git a/DisCatSharp/Entities/Interaction/Components/Button/DiscordButtonComponent.cs b/DisCatSharp/Entities/Interaction/Components/Button/DiscordButtonComponent.cs index 4c3d74aea..3be8fb42d 100644 --- a/DisCatSharp/Entities/Interaction/Components/Button/DiscordButtonComponent.cs +++ b/DisCatSharp/Entities/Interaction/Components/Button/DiscordButtonComponent.cs @@ -1,127 +1,127 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using DisCatSharp.Enums; using Newtonsoft.Json; namespace DisCatSharp.Entities; /// /// Represents a button that can be pressed. Fires event when pressed. /// public sealed class DiscordButtonComponent : DiscordComponent { /// /// The style of the button. /// [JsonProperty("style", NullValueHandling = NullValueHandling.Ignore)] public ButtonStyle Style { get; internal set; } /// /// The text to apply to the button. If this is not specified becomes required. /// [JsonProperty("label", NullValueHandling = NullValueHandling.Ignore)] public string Label { get; internal set; } /// /// Whether this button can be pressed. /// [JsonProperty("disabled", NullValueHandling = NullValueHandling.Ignore)] public bool Disabled { get; internal set; } /// /// The emoji to add to the button. Can be used in conjunction with a label, or as standalone. Must be added if label is not specified. /// [JsonProperty("emoji", NullValueHandling = NullValueHandling.Ignore)] public DiscordComponentEmoji Emoji { get; internal set; } /// /// Enables this component if it was disabled before. /// /// The current component. public DiscordButtonComponent Enable() { this.Disabled = false; return this; } /// /// Disables this component. /// /// The current component. public DiscordButtonComponent Disable() { this.Disabled = true; return this; } /// /// Constructs a new . /// public DiscordButtonComponent() { this.Type = ComponentType.Button; } /// /// Constructs a new button based on another button. /// /// The button to copy. public DiscordButtonComponent(DiscordButtonComponent other) : this() { this.CustomId = other.CustomId; this.Style = other.Style; this.Label = other.Label; this.Disabled = other.Disabled; this.Emoji = other.Emoji; } /// /// Constructs a new button with the specified options. /// /// The style/color of the button. /// The Id to assign to the button. This is sent back when a user presses it. /// The text to display on the button, up to 80 characters. Can be left blank if is set. /// Whether this button should be initialized as being disabled. User sees a greyed out button that cannot be interacted with. /// The emoji to add to the button. This is required if is empty or null. /// Is thrown when neither the nor the is set. public DiscordButtonComponent(ButtonStyle style, string customId = null, string label = null, bool disabled = false, DiscordComponentEmoji emoji = null) { this.Style = style; this.CustomId = customId ?? Guid.NewGuid().ToString(); this.Disabled = disabled; if (emoji != null) - { + { this.Label = label; this.Emoji = emoji; } else - { + { this.Label = label ?? throw new ArgumentException("Label can only be null if emoji is set."); this.Emoji = null; } this.Type = ComponentType.Button; } } diff --git a/DisCatSharp/Entities/Message/DiscordMessage.cs b/DisCatSharp/Entities/Message/DiscordMessage.cs index 02fc1de27..1f4bc799f 100644 --- a/DisCatSharp/Entities/Message/DiscordMessage.cs +++ b/DisCatSharp/Entities/Message/DiscordMessage.cs @@ -1,876 +1,876 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Globalization; using System.Linq; using System.Threading.Tasks; using Newtonsoft.Json; namespace DisCatSharp.Entities; /// /// Represents a Discord text message. /// public class DiscordMessage : SnowflakeObject, IEquatable { /// /// Initializes a new instance of the class. /// internal DiscordMessage() { this._attachmentsLazy = new Lazy>(() => new ReadOnlyCollection(this.AttachmentsInternal)); this._embedsLazy = new Lazy>(() => new ReadOnlyCollection(this.EmbedsInternal)); this._mentionedChannelsLazy = new Lazy>(() => this.MentionedChannelsInternal != null ? new ReadOnlyCollection(this.MentionedChannelsInternal) : Array.Empty()); this._mentionedRolesLazy = new Lazy>(() => this.MentionedRolesInternal != null ? new ReadOnlyCollection(this.MentionedRolesInternal) : Array.Empty()); this.MentionedUsersLazy = new Lazy>(() => new ReadOnlyCollection(this.MentionedUsersInternal)); this._reactionsLazy = new Lazy>(() => new ReadOnlyCollection(this.ReactionsInternal)); this._stickersLazy = new Lazy>(() => new ReadOnlyCollection(this.StickersInternal)); this._jumpLink = new Lazy(() => { string gid = null; if (this.Channel != null) gid = this.Channel is DiscordDmChannel ? "@me" : this.Channel is DiscordThreadChannel ? this.INTERNAL_THREAD.GuildId.Value.ToString(CultureInfo.InvariantCulture) : this.Channel.GuildId.Value.ToString(CultureInfo.InvariantCulture); var cid = this.ChannelId.ToString(CultureInfo.InvariantCulture); var mid = this.Id.ToString(CultureInfo.InvariantCulture); return new Uri($"https://{(this.Discord.Configuration.UseCanary ? "canary.discord.com" : this.Discord.Configuration.UsePtb ? "ptb.discord.com" : "discord.com")}/channels/{gid}/{cid}/{mid}"); }); } /// /// Initializes a new instance of the class. /// /// The other message. internal DiscordMessage(DiscordMessage other) : this() { this.Discord = other.Discord; this.AttachmentsInternal = other.AttachmentsInternal; // the attachments cannot change, thus no need to copy and reallocate. this.EmbedsInternal = new List(other.EmbedsInternal); if (other.MentionedChannelsInternal != null) this.MentionedChannelsInternal = new List(other.MentionedChannelsInternal); if (other.MentionedRolesInternal != null) this.MentionedRolesInternal = new List(other.MentionedRolesInternal); if (other.MentionedRoleIds != null) this.MentionedRoleIds = new List(other.MentionedRoleIds); this.MentionedUsersInternal = new List(other.MentionedUsersInternal); this.ReactionsInternal = new List(other.ReactionsInternal); this.StickersInternal = new List(other.StickersInternal); this.Author = other.Author; this.ChannelId = other.ChannelId; this.Content = other.Content; this.EditedTimestampRaw = other.EditedTimestampRaw; this.Id = other.Id; this.IsTts = other.IsTts; this.MessageType = other.MessageType; this.Pinned = other.Pinned; this.TimestampRaw = other.TimestampRaw; this.WebhookId = other.WebhookId; } /// /// Gets the channel in which the message was sent. /// [JsonIgnore] public DiscordChannel Channel { get => (this.Discord as DiscordClient)?.InternalGetCachedChannel(this.ChannelId) ?? this._channel; internal set => this._channel = value; } private DiscordChannel _channel; /// /// Gets the thread in which the message was sent. /// [JsonIgnore] private DiscordThreadChannel INTERNAL_THREAD { get => (this.Discord as DiscordClient)?.InternalGetCachedThread(this.ChannelId) ?? this._thread; set => this._thread = value; } private DiscordThreadChannel _thread; /// /// Gets the ID of the channel in which the message was sent. /// [JsonProperty("channel_id", NullValueHandling = NullValueHandling.Ignore)] public ulong ChannelId { get; internal set; } /// /// Gets the components this message was sent with. /// [JsonProperty("components", NullValueHandling = NullValueHandling.Ignore)] public IReadOnlyCollection Components { get; internal set; } /// /// Gets the user or member that sent the message. /// [JsonProperty("author", NullValueHandling = NullValueHandling.Ignore)] public DiscordUser Author { get; internal set; } /// /// Gets the message's content. /// [JsonProperty("content", NullValueHandling = NullValueHandling.Ignore)] public string Content { get; internal set; } /// /// Gets the message's creation timestamp. /// [JsonIgnore] public DateTimeOffset Timestamp => !string.IsNullOrWhiteSpace(this.TimestampRaw) && DateTimeOffset.TryParse(this.TimestampRaw, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dto) ? dto : this.CreationTimestamp; /// /// Gets the message's creation timestamp as raw string. /// [JsonProperty("timestamp", NullValueHandling = NullValueHandling.Ignore)] internal string TimestampRaw { get; set; } /// /// Gets the message's edit timestamp. Will be null if the message was not edited. /// [JsonIgnore] public DateTimeOffset? EditedTimestamp => !string.IsNullOrWhiteSpace(this.EditedTimestampRaw) && DateTimeOffset.TryParse(this.EditedTimestampRaw, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dto) ? (DateTimeOffset?)dto : null; /// /// Gets the message's edit timestamp as raw string. Will be null if the message was not edited. /// [JsonProperty("edited_timestamp", NullValueHandling = NullValueHandling.Ignore)] internal string EditedTimestampRaw { get; set; } /// /// Gets whether this message was edited. /// [JsonIgnore] public bool IsEdited => !string.IsNullOrWhiteSpace(this.EditedTimestampRaw); /// /// Gets whether the message is a text-to-speech message. /// [JsonProperty("tts", NullValueHandling = NullValueHandling.Ignore)] public bool IsTts { get; internal set; } /// /// Gets whether the message mentions everyone. /// [JsonProperty("mention_everyone", NullValueHandling = NullValueHandling.Ignore)] public bool MentionEveryone { get; internal set; } /// /// Gets users or members mentioned by this message. /// [JsonIgnore] public IReadOnlyList MentionedUsers => this.MentionedUsersLazy.Value; [JsonProperty("mentions", NullValueHandling = NullValueHandling.Ignore)] internal List MentionedUsersInternal; [JsonIgnore] internal readonly Lazy> MentionedUsersLazy; // TODO: this will probably throw an exception in DMs since it tries to wrap around a null List... // this is probably low priority but need to find out a clean way to solve it... /// /// Gets roles mentioned by this message. /// [JsonIgnore] public IReadOnlyList MentionedRoles => this._mentionedRolesLazy.Value; [JsonIgnore] internal List MentionedRolesInternal; [JsonProperty("mention_roles")] internal List MentionedRoleIds; [JsonIgnore] private readonly Lazy> _mentionedRolesLazy; /// /// Gets channels mentioned by this message. /// [JsonIgnore] public IReadOnlyList MentionedChannels => this._mentionedChannelsLazy.Value; [JsonIgnore] internal List MentionedChannelsInternal; [JsonIgnore] private readonly Lazy> _mentionedChannelsLazy; /// /// Gets files attached to this message. /// [JsonIgnore] public IReadOnlyList Attachments => this._attachmentsLazy.Value; [JsonProperty("attachments", NullValueHandling = NullValueHandling.Ignore)] internal List AttachmentsInternal = new(); [JsonIgnore] private readonly Lazy> _attachmentsLazy; /// /// Gets embeds attached to this message. /// [JsonIgnore] public IReadOnlyList Embeds => this._embedsLazy.Value; [JsonProperty("embeds", NullValueHandling = NullValueHandling.Ignore)] internal List EmbedsInternal = new(); [JsonIgnore] private readonly Lazy> _embedsLazy; /// /// Gets reactions used on this message. /// [JsonIgnore] public IReadOnlyList Reactions => this._reactionsLazy.Value; [JsonProperty("reactions", NullValueHandling = NullValueHandling.Ignore)] internal List ReactionsInternal = new(); [JsonIgnore] private readonly Lazy> _reactionsLazy; - /// - /// Gets the nonce sent with the message, if the message was sent by the client. - /// - [JsonProperty("nonce", NullValueHandling = NullValueHandling.Ignore)] - public string Nonce { get; internal set; } + /// + /// Gets the nonce sent with the message, if the message was sent by the client. + /// + [JsonProperty("nonce", NullValueHandling = NullValueHandling.Ignore)] + public string Nonce { get; internal set; } /// /// Gets whether the message is pinned. /// [JsonProperty("pinned", NullValueHandling = NullValueHandling.Ignore)] public bool Pinned { get; internal set; } /// /// Gets the id of the webhook that generated this message. /// [JsonProperty("webhook_id", NullValueHandling = NullValueHandling.Ignore)] public ulong? WebhookId { get; internal set; } /// /// Gets the type of the message. /// [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] public MessageType? MessageType { get; internal set; } /// /// Gets the message activity in the Rich Presence embed. /// [JsonProperty("activity", NullValueHandling = NullValueHandling.Ignore)] public DiscordMessageActivity Activity { get; internal set; } /// /// Gets the message application in the Rich Presence embed. /// [JsonProperty("application", NullValueHandling = NullValueHandling.Ignore)] public DiscordMessageApplication Application { get; internal set; } /// /// Gets the message application id in the Rich Presence embed. /// [JsonProperty("application_id", NullValueHandling = NullValueHandling.Ignore)] public ulong ApplicationId { get; internal set; } /// /// Gets the internal reference. /// [JsonProperty("message_reference", NullValueHandling = NullValueHandling.Ignore)] internal InternalDiscordMessageReference? InternalReference { get; set; } /// /// Gets the original message reference from the crossposted message. /// [JsonIgnore] public DiscordMessageReference Reference => this.InternalReference.HasValue ? this?.InternalBuildMessageReference() : null; /// /// Gets the bitwise flags for this message. /// [JsonProperty("flags", NullValueHandling = NullValueHandling.Ignore)] public MessageFlags? Flags { get; internal set; } /// /// Gets whether the message originated from a webhook. /// [JsonIgnore] public bool WebhookMessage => this.WebhookId != null; /// /// Gets the jump link to this message. /// [JsonIgnore] public Uri JumpLink => this._jumpLink.Value; private readonly Lazy _jumpLink; /// /// Gets stickers for this message. /// [JsonIgnore] public IReadOnlyList Stickers => this._stickersLazy.Value; [JsonProperty("sticker_items", NullValueHandling = NullValueHandling.Ignore)] internal List StickersInternal = new(); [JsonIgnore] private readonly Lazy> _stickersLazy; /// /// Gets the guild id. /// [JsonProperty("guild_id", NullValueHandling = NullValueHandling.Ignore)] internal ulong? GuildId { get; set; } /// /// Gets the message object for the referenced message /// [JsonProperty("referenced_message", NullValueHandling = NullValueHandling.Ignore)] public DiscordMessage ReferencedMessage { get; internal set; } /// /// Gets whether the message is a response to an interaction. /// [JsonProperty("interaction", NullValueHandling = NullValueHandling.Ignore)] public DiscordMessageInteraction Interaction { get; internal set; } /// /// Gets the thread that was started from this message. /// [JsonProperty("thread", NullValueHandling = NullValueHandling.Ignore)] public DiscordThreadChannel Thread { get; internal set; } /// /// Build the message reference. /// internal DiscordMessageReference InternalBuildMessageReference() { var client = this.Discord as DiscordClient; var guildId = this.InternalReference.Value.GuildId; var channelId = this.InternalReference.Value.ChannelId; var messageId = this.InternalReference.Value.MessageId; var reference = new DiscordMessageReference(); if (guildId.HasValue) reference.Guild = client.GuildsInternal.TryGetValue(guildId.Value, out var g) ? g : new DiscordGuild { Id = guildId.Value, Discord = client }; var channel = client.InternalGetCachedChannel(channelId.Value); if (channel == null) { reference.Channel = new DiscordChannel { Id = channelId.Value, Discord = client }; if (guildId.HasValue) reference.Channel.GuildId = guildId.Value; } else reference.Channel = channel; if (client.MessageCache != null && client.MessageCache.TryGet(m => m.Id == messageId.Value && m.ChannelId == channelId, out var msg)) reference.Message = msg; else { reference.Message = new DiscordMessage { ChannelId = this.ChannelId, Discord = client }; if (messageId.HasValue) reference.Message.Id = messageId.Value; } return reference; } /// /// Gets the mentions. /// /// An array of IMentions. private IMention[] GetMentions() { var mentions = new List(); if (this.ReferencedMessage != null && this.MentionedUsersInternal.Contains(this.ReferencedMessage.Author)) mentions.Add(new RepliedUserMention()); if (this.MentionedUsersInternal.Any()) mentions.AddRange(this.MentionedUsersInternal.Select(m => (IMention)new UserMention(m))); if (this.MentionedRoleIds.Any()) mentions.AddRange(this.MentionedRoleIds.Select(r => (IMention)new RoleMention(r))); return mentions.ToArray(); } /// /// Populates the mentions. /// internal void PopulateMentions() { var guild = this.Channel?.Guild; this.MentionedUsersInternal ??= new List(); this.MentionedRolesInternal ??= new List(); this.MentionedChannelsInternal ??= new List(); var mentionedUsers = new HashSet(new DiscordUserComparer()); if (guild != null) { foreach (var usr in this.MentionedUsersInternal) { usr.Discord = this.Discord; this.Discord.UserCache.AddOrUpdate(usr.Id, usr, (id, old) => { old.Username = usr.Username; old.Discriminator = usr.Discriminator; old.AvatarHash = usr.AvatarHash; return old; }); mentionedUsers.Add(guild.MembersInternal.TryGetValue(usr.Id, out var member) ? member : usr); } } if (!string.IsNullOrWhiteSpace(this.Content)) { //mentionedUsers.UnionWith(Utilities.GetUserMentions(this).Select(this.Discord.GetCachedOrEmptyUserInternal)); if (guild != null) { //this._mentionedRoles = this._mentionedRoles.Union(Utilities.GetRoleMentions(this).Select(xid => guild.GetRole(xid))).ToList(); this.MentionedRolesInternal = this.MentionedRolesInternal.Union(this.MentionedRoleIds.Select(xid => guild.GetRole(xid))).ToList(); this.MentionedChannelsInternal = this.MentionedChannelsInternal.Union(Utilities.GetChannelMentions(this).Select(xid => guild.GetChannel(xid))).ToList(); } } this.MentionedUsersInternal = mentionedUsers.ToList(); } /// /// Edits the message. /// /// New content. /// Thrown when the client tried to modify a message not sent by them. /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task ModifyAsync(Optional content) => this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, content, default, this.GetMentions(), default, default, Array.Empty(), default); /// /// Edits the message. /// /// New embed. /// Thrown when the client tried to modify a message not sent by them. /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task ModifyAsync(Optional embed = default) => this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, default, embed.Map(v => new[] { v }).ValueOr(Array.Empty()), this.GetMentions(), default, default, Array.Empty(), default); /// /// Edits the message. /// /// New content. /// New embed. /// Thrown when the client tried to modify a message not sent by them. /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task ModifyAsync(Optional content, Optional embed = default) => this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, content, embed.Map(v => new[] { v }).ValueOr(Array.Empty()), this.GetMentions(), default, default, Array.Empty(), default); /// /// Edits the message. /// /// New content. /// New embeds. /// Thrown when the client tried to modify a message not sent by them. /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task ModifyAsync(Optional content, Optional> embeds = default) => this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, content, embeds, this.GetMentions(), default, default, Array.Empty(), default); /// /// Edits the message. /// /// The builder of the message to edit. /// Thrown when the client tried to modify a message not sent by them. /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task ModifyAsync(DiscordMessageBuilder builder) { builder.Validate(true); return await this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, builder.Content, Optional.Some(builder.Embeds.AsEnumerable()), builder.Mentions, builder.Components, builder.Suppressed, builder.Files, builder.Attachments.Count > 0 ? Optional.Some(builder.Attachments.AsEnumerable()) : builder.KeepAttachmentsInternal.HasValue ? builder.KeepAttachmentsInternal.Value ? Optional.Some(this.Attachments.AsEnumerable()) : Array.Empty() : null); } /// /// Edits the message embed suppression. /// /// Suppress embeds. /// Thrown when the client tried to modify a message not sent by them. /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task ModifySuppressionAsync(bool suppress = false) => this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, default, default, default, default, suppress, default, default); /// /// Clears all attachments from the message. /// /// public Task ClearAttachmentsAsync() => this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, default, default, this.GetMentions(), default, default, default, Array.Empty()); /// /// Edits the message. /// /// The builder of the message to edit. /// Thrown when the client tried to modify a message not sent by them. /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task ModifyAsync(Action action) { var builder = new DiscordMessageBuilder(); action(builder); builder.Validate(true); return await this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, builder.Content, Optional.Some(builder.Embeds.AsEnumerable()), builder.Mentions, builder.Components, builder.Suppressed, builder.Files, builder.Attachments.Count > 0 ? Optional.Some(builder.Attachments.AsEnumerable()) : builder.KeepAttachmentsInternal.HasValue ? builder.KeepAttachmentsInternal.Value ? Optional.Some(this.Attachments.AsEnumerable()) : Array.Empty() : null); } /// /// Deletes the message. /// /// Thrown when the client does not have the permission. /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task DeleteAsync(string reason = null) => this.Discord.ApiClient.DeleteMessageAsync(this.ChannelId, this.Id, reason); /// /// Creates a thread. /// Depending on the of the parent channel it's either a or a . /// /// The name of the thread. /// till it gets archived. Defaults to /// The per user ratelimit, aka slowdown. /// The reason. /// Thrown when the client does not have the or permission. /// Thrown when the channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. /// Thrown when the cannot be modified. public async Task CreateThreadAsync(string name, ThreadAutoArchiveDuration autoArchiveDuration = ThreadAutoArchiveDuration.OneHour, int? rateLimitPerUser = null, string reason = null) => Utilities.CheckThreadAutoArchiveDurationFeature(this.Channel.Guild, autoArchiveDuration) ? await this.Discord.ApiClient.CreateThreadAsync(this.ChannelId, this.Id, name, autoArchiveDuration, this.Channel.Type == ChannelType.News ? ChannelType.NewsThread : ChannelType.PublicThread, rateLimitPerUser, reason) : throw new NotSupportedException($"Cannot modify ThreadAutoArchiveDuration. Guild needs boost tier {(autoArchiveDuration == ThreadAutoArchiveDuration.ThreeDays ? "one" : "two")}."); /// /// Pins the message in its channel. /// /// Thrown when the client does not have the permission. /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task PinAsync() => this.Discord.ApiClient.PinMessageAsync(this.ChannelId, this.Id); /// /// Unpins the message in its channel. /// /// Thrown when the client does not have the permission. /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task UnpinAsync() => this.Discord.ApiClient.UnpinMessageAsync(this.ChannelId, this.Id); /// /// Responds to the message. This produces a reply. /// /// Message content to respond with. /// The sent message. /// Thrown when the client does not have the permission. /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task RespondAsync(string content) => this.Discord.ApiClient.CreateMessageAsync(this.ChannelId, content, null, sticker: null, replyMessageId: this.Id, mentionReply: false, failOnInvalidReply: false); /// /// Responds to the message. This produces a reply. /// /// Embed to attach to the message. /// The sent message. /// Thrown when the client does not have the permission. /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task RespondAsync(DiscordEmbed embed) => this.Discord.ApiClient.CreateMessageAsync(this.ChannelId, null, embed != null ? new[] { embed } : null, sticker: null, replyMessageId: this.Id, mentionReply: false, failOnInvalidReply: false); /// /// Responds to the message. This produces a reply. /// /// Message content to respond with. /// Embed to attach to the message. /// The sent message. /// Thrown when the client does not have the permission. /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task RespondAsync(string content, DiscordEmbed embed) => this.Discord.ApiClient.CreateMessageAsync(this.ChannelId, content, embed != null ? new[] { embed } : null, sticker: null, replyMessageId: this.Id, mentionReply: false, failOnInvalidReply: false); /// /// Responds to the message. This produces a reply. /// /// The Discord message builder. /// The sent message. /// Thrown when the client does not have the permission. /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task RespondAsync(DiscordMessageBuilder builder) => this.Discord.ApiClient.CreateMessageAsync(this.ChannelId, builder.WithReply(this.Id, mention: false, failOnInvalidReply: false)); /// /// Responds to the message. This produces a reply. /// /// The Discord message builder. /// The sent message. /// Thrown when the client does not have the permission. /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task RespondAsync(Action action) { var builder = new DiscordMessageBuilder(); action(builder); return this.Discord.ApiClient.CreateMessageAsync(this.ChannelId, builder.WithReply(this.Id, mention: false, failOnInvalidReply: false)); } /// /// Creates a reaction to this message. /// /// The emoji you want to react with, either an emoji or name:id /// Thrown when the client does not have the permission. /// Thrown when the emoji does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task CreateReactionAsync(DiscordEmoji emoji) => this.Discord.ApiClient.CreateReactionAsync(this.ChannelId, this.Id, emoji.ToReactionString()); /// /// Deletes your own reaction /// /// Emoji for the reaction you want to remove, either an emoji or name:id /// Thrown when the emoji does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task DeleteOwnReactionAsync(DiscordEmoji emoji) => this.Discord.ApiClient.DeleteOwnReactionAsync(this.ChannelId, this.Id, emoji.ToReactionString()); /// /// Deletes another user's reaction. /// /// Emoji for the reaction you want to remove, either an emoji or name:id. /// Member you want to remove the reaction for /// Reason for audit logs. /// Thrown when the client does not have the permission. /// Thrown when the emoji does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task DeleteReactionAsync(DiscordEmoji emoji, DiscordUser user, string reason = null) => this.Discord.ApiClient.DeleteUserReactionAsync(this.ChannelId, this.Id, user.Id, emoji.ToReactionString(), reason); /// /// Gets users that reacted with this emoji. /// /// Emoji to react with. /// Limit of users to fetch. /// Fetch users after this user's id. /// Thrown when the emoji does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task> GetReactionsAsync(DiscordEmoji emoji, int limit = 25, ulong? after = null) => this.GetReactionsInternalAsync(emoji, limit, after); /// /// Deletes all reactions for this message. /// /// Reason for audit logs. /// Thrown when the client does not have the permission. /// Thrown when the emoji does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task DeleteAllReactionsAsync(string reason = null) => this.Discord.ApiClient.DeleteAllReactionsAsync(this.ChannelId, this.Id, reason); /// /// Deletes all reactions of a specific reaction for this message. /// /// The emoji to clear, either an emoji or name:id. /// Thrown when the client does not have the permission. /// Thrown when the emoji does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task DeleteReactionsEmojiAsync(DiscordEmoji emoji) => this.Discord.ApiClient.DeleteReactionsEmojiAsync(this.ChannelId, this.Id, emoji.ToReactionString()); /// /// Gets the reactions. /// /// The emoji to search for. /// The limit of results. /// Get the reasctions after snowflake. private async Task> GetReactionsInternalAsync(DiscordEmoji emoji, int limit = 25, ulong? after = null) { if (limit < 0) throw new ArgumentException("Cannot get a negative number of reactions' users."); if (limit == 0) return Array.Empty(); var users = new List(limit); var remaining = limit; var last = after; int lastCount; do { var fetchSize = remaining > 100 ? 100 : remaining; var fetch = await this.Discord.ApiClient.GetReactionsAsync(this.Channel.Id, this.Id, emoji.ToReactionString(), last, fetchSize).ConfigureAwait(false); lastCount = fetch.Count; remaining -= lastCount; users.AddRange(fetch); last = fetch.LastOrDefault()?.Id; } while (remaining > 0 && lastCount > 0); return new ReadOnlyCollection(users); } /// /// Returns a string representation of this message. /// /// String representation of this message. public override string ToString() => $"Message {this.Id}; Attachment count: {this.AttachmentsInternal.Count}; Embed count: {this.EmbedsInternal.Count}; Contents: {this.Content}"; /// /// Checks whether this is equal to another object. /// /// Object to compare to. /// Whether the object is equal to this . public override bool Equals(object obj) => this.Equals(obj as DiscordMessage); /// /// Checks whether this is equal to another . /// /// to compare to. /// Whether the is equal to this . public bool Equals(DiscordMessage e) => e is not null && (ReferenceEquals(this, e) || (this.Id == e.Id && this.ChannelId == e.ChannelId)); /// /// Gets the hash code for this . /// /// The hash code for this . public override int GetHashCode() { var hash = 13; hash = (hash * 7) + this.Id.GetHashCode(); hash = (hash * 7) + this.ChannelId.GetHashCode(); return hash; } /// /// Gets whether the two objects are equal. /// /// First message to compare. /// Second message to compare. /// Whether the two messages are equal. public static bool operator ==(DiscordMessage e1, DiscordMessage e2) { var o1 = e1 as object; var o2 = e2 as object; return (o1 != null || o2 == null) && (o1 == null || o2 != null) && ((o1 == null && o2 == null) || (e1.Id == e2.Id && e1.ChannelId == e2.ChannelId)); } /// /// Gets whether the two objects are not equal. /// /// First message to compare. /// Second message to compare. /// Whether the two messages are not equal. public static bool operator !=(DiscordMessage e1, DiscordMessage e2) => !(e1 == e2); } diff --git a/DisCatSharp/Entities/Thread/DiscordThreadChannel.cs b/DisCatSharp/Entities/Thread/DiscordThreadChannel.cs index 824b9b811..ebe8f237e 100644 --- a/DisCatSharp/Entities/Thread/DiscordThreadChannel.cs +++ b/DisCatSharp/Entities/Thread/DiscordThreadChannel.cs @@ -1,644 +1,644 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Globalization; using System.Linq; using System.Threading.Tasks; using DisCatSharp.Net.Models; using DisCatSharp.Net.Serialization; using Newtonsoft.Json; namespace DisCatSharp.Entities; /// /// Represents a discord thread channel. /// public class DiscordThreadChannel : DiscordChannel, IEquatable { /// /// Gets ID of the owner that started this thread. /// [JsonProperty("owner_id", NullValueHandling = NullValueHandling.Ignore)] public ulong OwnerId { get; internal set; } /// /// Gets the name of this thread. /// [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] public new string Name { get; internal set; } /// /// Gets the type of this thread. /// [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] public new ChannelType Type { get; internal set; } /// /// Gets whether this thread is private. /// [JsonIgnore] public new bool IsPrivate => this.Type == ChannelType.PrivateThread; /// /// Gets the ID of the last message sent in this thread. /// [JsonProperty("last_message_id", NullValueHandling = NullValueHandling.Ignore)] public new ulong? LastMessageId { get; internal set; } /// /// Gets the slowmode delay configured for this thread. /// All bots, as well as users with or permissions in the channel are exempt from slowmode. /// [JsonProperty("rate_limit_per_user", NullValueHandling = NullValueHandling.Ignore)] public new int? PerUserRateLimit { get; internal set; } /// /// Gets an approximate count of messages in a thread, stops counting at 50. /// [JsonProperty("message_count", NullValueHandling = NullValueHandling.Ignore)] public int? MessageCount { get; internal set; } /// /// Gets an approximate count of users in a thread, stops counting at 50. /// [JsonProperty("member_count", NullValueHandling = NullValueHandling.Ignore)] public int? MemberCount { get; internal set; } /// /// Represents the current member for this thread. This will have a value if the user has joined the thread. /// [JsonProperty("member", NullValueHandling = NullValueHandling.Ignore)] public DiscordThreadChannelMember CurrentMember { get; internal set; } /// /// Gets when the last pinned message was pinned in this thread. /// [JsonIgnore] public new DateTimeOffset? LastPinTimestamp => !string.IsNullOrWhiteSpace(this.LastPinTimestampRaw) && DateTimeOffset.TryParse(this.LastPinTimestampRaw, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dto) ? dto : null; /// /// Gets when the last pinned message was pinned in this thread as raw string. /// [JsonProperty("last_pin_timestamp", NullValueHandling = NullValueHandling.Ignore)] internal new string LastPinTimestampRaw { get; set; } /// /// Gets the threads metadata. /// [JsonProperty("thread_metadata", NullValueHandling = NullValueHandling.Ignore)] public DiscordThreadChannelMetadata ThreadMetadata { get; internal set; } /// /// Gets the thread members object. /// [JsonIgnore] public IReadOnlyDictionary ThreadMembers => new ReadOnlyConcurrentDictionary(this.ThreadMembersInternal); [JsonProperty("thread_member", NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] internal ConcurrentDictionary ThreadMembersInternal; /// /// Initializes a new instance of the class. /// internal DiscordThreadChannel() { } #region Methods /// /// Deletes a thread. /// /// Reason for audit logs. /// Thrown when the client does not have the permission. /// Thrown when the thread does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public new Task DeleteAsync(string reason = null) => this.Discord.ApiClient.DeleteThreadAsync(this.Id, reason); /// /// Modifies the current thread. /// /// Action to perform on this thread /// Thrown when the client does not have the permission. /// Thrown when the thread does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. /// Thrown when the cannot be modified. This happens, when the guild hasn't reached a certain boost . public Task ModifyAsync(Action action) { var mdl = new ThreadEditModel(); action(mdl); var canContinue = !mdl.AutoArchiveDuration.HasValue || !mdl.AutoArchiveDuration.Value.HasValue || Utilities.CheckThreadAutoArchiveDurationFeature(this.Guild, mdl.AutoArchiveDuration.Value.Value); if (mdl.Invitable.HasValue) { canContinue = this.Guild.Features.CanCreatePrivateThreads; } return canContinue ? this.Discord.ApiClient.ModifyThreadAsync(this.Id, mdl.Name, mdl.Locked, mdl.Archived, mdl.PerUserRateLimit, mdl.AutoArchiveDuration, mdl.Invitable, mdl.AuditLogReason) : throw new NotSupportedException($"Cannot modify ThreadAutoArchiveDuration. Guild needs boost tier {(mdl.AutoArchiveDuration.Value.Value == ThreadAutoArchiveDuration.ThreeDays ? "one" : "two")}."); } /// /// Archives a thread. /// /// Whether the thread should be locked. /// Reason for audit logs. /// Thrown when the client does not have the permission. /// Thrown when the thread does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task ArchiveAsync(bool locked = true, string reason = null) => this.Discord.ApiClient.ModifyThreadAsync(this.Id, null, locked, true, null, null, null, reason: reason); /// /// Unarchives a thread. /// /// Reason for audit logs. /// Thrown when the thread does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task UnarchiveAsync(string reason = null) => this.Discord.ApiClient.ModifyThreadAsync(this.Id, null, null, false, null, null, null, reason: reason); /// /// Gets the members of a thread. Needs the intent. /// /// Thrown when the thread does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task> GetMembersAsync() => await this.Discord.ApiClient.GetThreadMembersAsync(this.Id); /// /// Adds a member to this thread. /// /// The member id to be added. /// Thrown when the thread does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task AddMemberAsync(ulong memberId) => this.Discord.ApiClient.AddThreadMemberAsync(this.Id, memberId); /// /// Adds a member to this thread. /// /// The member to be added. /// Thrown when the thread does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task AddMemberAsync(DiscordMember member) => this.AddMemberAsync(member.Id); /// /// Gets a member in this thread. /// /// The member to be added. /// Thrown when the member is not part of the thread. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task GetMemberAsync(ulong memberId) => this.Discord.ApiClient.GetThreadMemberAsync(this.Id, memberId); /// /// Gets a member in this thread. /// /// The member to be added. /// Thrown when the member is not part of the thread. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task GetMemberAsync(DiscordMember member) => this.Discord.ApiClient.GetThreadMemberAsync(this.Id, member.Id); /// /// Removes a member from this thread. /// /// The member id to be removed. /// Thrown when the thread does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task RemoveMemberAsync(ulong memberId) => this.Discord.ApiClient.RemoveThreadMemberAsync(this.Id, memberId); /// /// Removes a member from this thread. Only applicable to private threads. /// /// The member to be removed. /// Thrown when the thread does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task RemoveMemberAsync(DiscordMember member) => this.RemoveMemberAsync(member.Id); /// /// Adds a role to this thread. Only applicable to private threads. /// /// The role id to be added. /// Thrown when the thread does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task AddRoleAsync(ulong roleId) { var role = this.Guild.GetRole(roleId); var members = await this.Guild.GetAllMembersAsync(); var roleMembers = members.Where(m => m.Roles.Contains(role)); foreach (var member in roleMembers) { await this.Discord.ApiClient.AddThreadMemberAsync(this.Id, member.Id); } } /// /// Adds a role to this thread. Only applicable to private threads. /// /// The role to be added. /// Thrown when the thread does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task AddRoleAsync(DiscordRole role) => this.AddRoleAsync(role.Id); /// /// Removes a role from this thread. Only applicable to private threads. /// /// The role id to be removed. /// Thrown when the thread does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task RemoveRoleAsync(ulong roleId) { var role = this.Guild.GetRole(roleId); var members = await this.Guild.GetAllMembersAsync(); var roleMembers = members.Where(m => m.Roles.Contains(role)); foreach (var member in roleMembers) { await this.Discord.ApiClient.RemoveThreadMemberAsync(this.Id, member.Id); } } /// /// Removes a role to from thread. Only applicable to private threads. /// /// The role to be removed. /// Thrown when the thread does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task RemoveRoleAsync(DiscordRole role) => this.RemoveRoleAsync(role.Id); /// /// Joins a thread. /// /// Thrown when the client has no access to this thread. /// Thrown when the thread does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task JoinAsync() => this.Discord.ApiClient.JoinThreadAsync(this.Id); /// /// Leaves a thread. /// /// Thrown when the client has no access to this thread. /// Thrown when the thread does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task LeaveAsync() => this.Discord.ApiClient.LeaveThreadAsync(this.Id); /// /// Sends a message to this thread. /// /// Content of the message to send. /// The sent message. /// Thrown when the client does not have the permission and if TTS is true or the thread is locked. /// Thrown when the thread does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public new Task SendMessageAsync(string content) => !this.IsWritable() ? throw new ArgumentException("Cannot send a text message to a non-thread channel.") : this.Discord.ApiClient.CreateMessageAsync(this.Id, content, null, sticker: null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false); /// /// Sends a message to this thread. /// /// Embed to attach to the message. /// The sent message. /// Thrown when the client does not have the permission and if TTS is true or the thread is locked. /// Thrown when the thread does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public new Task SendMessageAsync(DiscordEmbed embed) => !this.IsWritable() ? throw new ArgumentException("Cannot send a text message to a non-thread channel.") : this.Discord.ApiClient.CreateMessageAsync(this.Id, null, new[] { embed }, sticker: null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false); /// /// Sends a message to this thread. /// /// Content of the message to send. /// Embed to attach to the message. /// The sent message. /// Thrown when the client does not have the permission and if TTS is true or the thread is locked. /// Thrown when the thread does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public new Task SendMessageAsync(string content, DiscordEmbed embed) => !this.IsWritable() ? throw new ArgumentException("Cannot send a text message to a non-thread channel.") : this.Discord.ApiClient.CreateMessageAsync(this.Id, content, new[] { embed }, sticker: null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false); /// /// Sends a message to this thread. /// /// The builder with all the items to thread. /// The sent message. /// Thrown when the client does not have the permission and if TTS is true or the thread is locked. /// Thrown when the thread does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public new Task SendMessageAsync(DiscordMessageBuilder builder) => this.Discord.ApiClient.CreateMessageAsync(this.Id, builder); /// /// Sends a message to this channel. /// /// The builder with all the items to send. /// The sent message. /// Thrown when the client does not have the permission and if TTS is true or the thread is locked. /// Thrown when the thread does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public new Task SendMessageAsync(Action action) { var builder = new DiscordMessageBuilder(); action(builder); return !this.IsWritable() ? throw new ArgumentException("Cannot send a text message to a non-text channel.") : this.Discord.ApiClient.CreateMessageAsync(this.Id, builder); } /// /// Returns a specific message /// /// The id of the message /// Thrown when the client does not have the permission and if TTS is true or the thread is locked. /// Thrown when the thread does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public new async Task GetMessageAsync(ulong id) => this.Discord.Configuration.MessageCacheSize > 0 && this.Discord is DiscordClient dc && dc.MessageCache != null && dc.MessageCache.TryGet(xm => xm.Id == id && xm.ChannelId == this.Id, out var msg) ? msg : await this.Discord.ApiClient.GetMessageAsync(this.Id, id).ConfigureAwait(false); /// /// Returns a list of messages before a certain message. /// The amount of messages to fetch. /// Message to fetch before from. /// /// Thrown when the client does not have the or the permission. /// Thrown when the thread does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public new Task> GetMessagesBeforeAsync(ulong before, int limit = 100) => this.GetMessagesInternalAsync(limit, before, null, null); /// /// Returns a list of messages after a certain message. /// The amount of messages to fetch. /// Message to fetch after from. /// /// Thrown when the client does not have the or the permission. /// Thrown when the thread does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public new Task> GetMessagesAfterAsync(ulong after, int limit = 100) => this.GetMessagesInternalAsync(limit, null, after, null); /// /// Returns a list of messages around a certain message. /// The amount of messages to fetch. /// Message to fetch around from. /// /// Thrown when the client does not have the or the permission. /// Thrown when the thread does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public new Task> GetMessagesAroundAsync(ulong around, int limit = 100) => this.GetMessagesInternalAsync(limit, null, null, around); /// /// Returns a list of messages from the last message in the thread. /// The amount of messages to fetch. /// /// Thrown when the client does not have the or the permission. /// Thrown when the thread does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public new Task> GetMessagesAsync(int limit = 100) => this.GetMessagesInternalAsync(limit, null, null, null); /// /// Returns a list of messages /// /// How many messages should be returned. /// Get messages before snowflake. /// Get messages after snowflake. /// Get messages around snowflake. private async Task> GetMessagesInternalAsync(int limit = 100, ulong? before = null, ulong? after = null, ulong? around = null) { if (this.Type != ChannelType.PublicThread && this.Type != ChannelType.PrivateThread && this.Type != ChannelType.NewsThread) throw new ArgumentException("Cannot get the messages of a non-thread channel."); if (limit < 0) throw new ArgumentException("Cannot get a negative number of messages."); if (limit == 0) return Array.Empty(); //return this.Discord.ApiClient.GetChannelMessagesAsync(this.Id, limit, before, after, around); if (limit > 100 && around != null) throw new InvalidOperationException("Cannot get more than 100 messages around the specified ID."); var msgs = new List(limit); var remaining = limit; ulong? last = null; var isAfter = after != null; int lastCount; do { var fetchSize = remaining > 100 ? 100 : remaining; var fetch = await this.Discord.ApiClient.GetChannelMessagesAsync(this.Id, fetchSize, !isAfter ? last ?? before : null, isAfter ? last ?? after : null, around).ConfigureAwait(false); lastCount = fetch.Count; remaining -= lastCount; if (!isAfter) { msgs.AddRange(fetch); last = fetch.LastOrDefault()?.Id; } else { msgs.InsertRange(0, fetch); last = fetch.FirstOrDefault()?.Id; } } while (remaining > 0 && lastCount > 0); return new ReadOnlyCollection(msgs); } /// /// Deletes multiple messages if they are less than 14 days old. If they are older, none of the messages will be deleted and you will receive a error. /// /// A collection of messages to delete. /// Reason for audit logs. /// Thrown when the client does not have the permission. /// Thrown when the thread does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public new async Task DeleteMessagesAsync(IEnumerable messages, string reason = null) { // don't enumerate more than once var msgs = messages.Where(x => x.Channel.Id == this.Id).Select(x => x.Id).ToArray(); if (messages == null || !msgs.Any()) throw new ArgumentException("You need to specify at least one message to delete."); if (msgs.Length < 2) { await this.Discord.ApiClient.DeleteMessageAsync(this.Id, msgs.Single(), reason).ConfigureAwait(false); return; } for (var i = 0; i < msgs.Length; i += 100) await this.Discord.ApiClient.DeleteMessagesAsync(this.Id, msgs.Skip(i).Take(100), reason).ConfigureAwait(false); } /// /// Deletes a message /// /// The message to be deleted. /// Reason for audit logs. /// Thrown when the client does not have the permission. /// Thrown when the thread does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public new Task DeleteMessageAsync(DiscordMessage message, string reason = null) => this.Discord.ApiClient.DeleteMessageAsync(this.Id, message.Id, reason); /// /// Post a typing indicator /// /// Thrown when the thread does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public new Task TriggerTypingAsync() => this.Type != ChannelType.PublicThread && this.Type != ChannelType.PrivateThread && this.Type != ChannelType.NewsThread ? throw new ArgumentException("Cannot start typing in a non-text channel.") : this.Discord.ApiClient.TriggerTypingAsync(this.Id); /// /// Returns all pinned messages /// /// Thrown when the client does not have the permission or the client is missing . /// Thrown when the thread does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public new Task> GetPinnedMessagesAsync() => this.Type != ChannelType.PublicThread && this.Type != ChannelType.PrivateThread && this.Type != ChannelType.News ? throw new ArgumentException("A non-thread channel does not have pinned messages.") : this.Discord.ApiClient.GetPinnedMessagesAsync(this.Id); /// /// Returns a string representation of this thread. /// /// String representation of this thread. public override string ToString() => this.Type switch - { - ChannelType.NewsThread => $"News thread {this.Name} ({this.Id})", - ChannelType.PublicThread => $"Thread {this.Name} ({this.Id})", - ChannelType.PrivateThread => $"Private thread {this.Name} ({this.Id})", - _ => $"Thread {this.Name} ({this.Id})", - }; + { + ChannelType.NewsThread => $"News thread {this.Name} ({this.Id})", + ChannelType.PublicThread => $"Thread {this.Name} ({this.Id})", + ChannelType.PrivateThread => $"Private thread {this.Name} ({this.Id})", + _ => $"Thread {this.Name} ({this.Id})", + }; #endregion /// /// Checks whether this is equal to another object. /// /// Object to compare to. /// Whether the object is equal to this . public override bool Equals(object obj) => this.Equals(obj as DiscordThreadChannel); /// /// Checks whether this is equal to another . /// /// to compare to. /// Whether the is equal to this . public bool Equals(DiscordThreadChannel e) => e is not null && (ReferenceEquals(this, e) || this.Id == e.Id); /// /// Gets the hash code for this . /// /// The hash code for this . public override int GetHashCode() => this.Id.GetHashCode(); /// /// Gets whether the two objects are equal. /// /// First channel to compare. /// Second channel to compare. /// Whether the two channels are equal. public static bool operator ==(DiscordThreadChannel e1, DiscordThreadChannel e2) { var o1 = e1 as object; var o2 = e2 as object; return (o1 != null || o2 == null) && (o1 == null || o2 != null) && ((o1 == null && o2 == null) || e1.Id == e2.Id); } /// /// Gets whether the two objects are not equal. /// /// First channel to compare. /// Second channel to compare. /// Whether the two channels are not equal. public static bool operator !=(DiscordThreadChannel e1, DiscordThreadChannel e2) => !(e1 == e2); } diff --git a/DisCatSharp/Entities/User/DiscordUser.cs b/DisCatSharp/Entities/User/DiscordUser.cs index ed88b96f6..15c1d34f0 100644 --- a/DisCatSharp/Entities/User/DiscordUser.cs +++ b/DisCatSharp/Entities/User/DiscordUser.cs @@ -1,458 +1,458 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Generic; using System.Globalization; using System.Threading.Tasks; using DisCatSharp.Enums; using DisCatSharp.Exceptions; using DisCatSharp.Net; using DisCatSharp.Net.Abstractions; using Newtonsoft.Json; namespace DisCatSharp.Entities; /// /// Represents a Discord user. /// public class DiscordUser : SnowflakeObject, IEquatable { /// /// Initializes a new instance of the class. /// internal DiscordUser() { } /// /// Initializes a new instance of the class. /// /// The transport user. internal DiscordUser(TransportUser transport) { this.Id = transport.Id; this.Username = transport.Username; this.Discriminator = transport.Discriminator; this.AvatarHash = transport.AvatarHash; this.BannerHash = transport.BannerHash; this.BannerColorInternal = transport.BannerColor; this.IsBot = transport.IsBot; this.MfaEnabled = transport.MfaEnabled; this.Verified = transport.Verified; this.Email = transport.Email; this.PremiumType = transport.PremiumType; this.Locale = transport.Locale; this.Flags = transport.Flags; this.OAuthFlags = transport.OAuthFlags; this.Bio = transport.Bio; } /// /// Gets this user's username. /// [JsonProperty("username", NullValueHandling = NullValueHandling.Ignore)] public virtual string Username { get; internal set; } /// /// Gets this user's username with the discriminator. /// Example: Discord#0000 /// [JsonIgnore] public virtual string UsernameWithDiscriminator => $"{this.Username}#{this.Discriminator}"; /// /// Gets the user's 4-digit discriminator. /// [JsonProperty("discriminator", NullValueHandling = NullValueHandling.Ignore)] public virtual string Discriminator { get; internal set; } /// /// Gets the discriminator integer. /// [JsonIgnore] internal int DiscriminatorInt => int.Parse(this.Discriminator, NumberStyles.Integer, CultureInfo.InvariantCulture); /// /// Gets the user's banner color, if set. Mutually exclusive with . /// public virtual DiscordColor? BannerColor => !this.BannerColorInternal.HasValue ? null : new DiscordColor(this.BannerColorInternal.Value); /// /// Gets the user's banner color integer. /// [JsonProperty("accent_color")] internal int? BannerColorInternal; /// /// Gets the user's banner url /// [JsonIgnore] public string BannerUrl => string.IsNullOrWhiteSpace(this.BannerHash) ? null : $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.BANNERS}/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.BannerHash}.{(this.BannerHash.StartsWith("a_") ? "gif" : "png")}?size=4096"; /// /// Gets the user's profile banner hash. Mutually exclusive with . /// [JsonProperty("banner", NullValueHandling = NullValueHandling.Ignore)] public virtual string BannerHash { get; internal set; } /// /// Gets the users bio. /// This is not available to bots tho. /// [JsonProperty("bio", NullValueHandling = NullValueHandling.Ignore)] public virtual string Bio { get; internal set; } /// /// Gets the user's avatar hash. /// [JsonProperty("avatar", NullValueHandling = NullValueHandling.Ignore)] public virtual string AvatarHash { get; internal set; } /// /// Returns a uri to this users profile. /// [JsonIgnore] public Uri ProfileUri => new($"{DiscordDomain.GetDomain(CoreDomain.Discord).Url}{Endpoints.USERS}/{this.Id}"); /// /// Returns a string representing the direct URL to this users profile. /// /// The URL of this users profile. [JsonIgnore] public string ProfileUrl => this.ProfileUri.AbsoluteUri; /// /// Gets the user's avatar URL.s /// [JsonIgnore] public string AvatarUrl => string.IsNullOrWhiteSpace(this.AvatarHash) ? this.DefaultAvatarUrl : $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.AVATARS}/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.AvatarHash}.{(this.AvatarHash.StartsWith("a_") ? "gif" : "png")}?size=1024"; /// /// Gets the URL of default avatar for this user. /// [JsonIgnore] public string DefaultAvatarUrl => $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.EMBED}{Endpoints.AVATARS}/{(this.DiscriminatorInt % 5).ToString(CultureInfo.InvariantCulture)}.png?size=1024"; /// /// Gets whether the user is a bot. /// [JsonProperty("bot", NullValueHandling = NullValueHandling.Ignore)] public virtual bool IsBot { get; internal set; } /// /// Gets whether the user has multi-factor authentication enabled. /// [JsonProperty("mfa_enabled", NullValueHandling = NullValueHandling.Ignore)] public virtual bool? MfaEnabled { get; internal set; } /// /// Gets whether the user is an official Discord system user. /// [JsonProperty("system", NullValueHandling = NullValueHandling.Ignore)] public bool? IsSystem { get; internal set; } /// /// Gets whether the user is verified. /// This is only present in OAuth. /// [JsonProperty("verified", NullValueHandling = NullValueHandling.Ignore)] public virtual bool? Verified { get; internal set; } /// /// Gets the user's email address. /// This is only present in OAuth. /// [JsonProperty("email", NullValueHandling = NullValueHandling.Ignore)] public virtual string Email { get; internal set; } /// /// Gets the user's premium type. /// [JsonProperty("premium_type", NullValueHandling = NullValueHandling.Ignore)] public virtual PremiumType? PremiumType { get; internal set; } /// /// Gets the user's chosen language /// [JsonProperty("locale", NullValueHandling = NullValueHandling.Ignore)] public virtual string Locale { get; internal set; } /// /// Gets the user's flags for OAuth. /// [JsonProperty("flags", NullValueHandling = NullValueHandling.Ignore)] public virtual UserFlags? OAuthFlags { get; internal set; } /// /// Gets the user's flags. /// [JsonProperty("public_flags", NullValueHandling = NullValueHandling.Ignore)] public virtual UserFlags? Flags { get; internal set; } /// /// Gets the user's mention string. /// [JsonIgnore] public string Mention => Formatter.Mention(this, this is DiscordMember); /// /// Gets whether this user is the Client which created this object. /// [JsonIgnore] public bool IsCurrent => this.Id == this.Discord.CurrentUser.Id; #region Extension of DiscordUser /// /// Whether this member is a /// /// [JsonIgnore] public bool IsMod => this.Flags.HasValue && this.Flags.Value.HasFlag(UserFlags.CertifiedModerator); /// /// Whether this member is a /// /// [JsonIgnore] public bool IsPartner => this.Flags.HasValue && this.Flags.Value.HasFlag(UserFlags.Partner); /// /// Whether this member is a /// /// [JsonIgnore] public bool IsVerifiedBot => this.Flags.HasValue && this.Flags.Value.HasFlag(UserFlags.VerifiedBot); /// /// Whether this member is a /// /// [JsonIgnore] public bool IsBotDev => this.Flags.HasValue && this.Flags.Value.HasFlag(UserFlags.VerifiedDeveloper); /// /// Whether this member is a /// /// [JsonIgnore] public bool IsStaff => this.Flags.HasValue && this.Flags.Value.HasFlag(UserFlags.Staff); #endregion /// /// Fetches the user from the API. /// /// The user with fresh data from the API. public async Task GetFromApiAsync() => await this.Discord.ApiClient.GetUserAsync(this.Id); - + /// /// Whether this user is in a /// /// /// /// DiscordGuild guild = await Client.GetGuildAsync(806675511555915806); /// DiscordUser user = await Client.GetUserAsync(469957180968271873); /// Console.WriteLine($"{user.Username} {(user.IsInGuild(guild) ? "is a" : "is not a")} member of {guild.Name}"); /// /// results to J_M_Lutra is a member of Project Nyaw~. /// /// /// [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "")] public async Task IsInGuild(DiscordGuild guild) { try { var member = await guild.GetMemberAsync(this.Id); return member is not null; } catch (NotFoundException) { return false; } } /// /// Whether this user is not in a /// /// /// public async Task IsNotInGuild(DiscordGuild guild) => !await this.IsInGuild(guild); /// /// Returns the DiscordMember in the specified /// /// The to get this user on. /// The . /// Thrown when the user is not part of the guild. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task ConvertToMember(DiscordGuild guild) => await guild.GetMemberAsync(this.Id); /// /// Unbans this user from a guild. /// /// Guild to unban this user from. /// Reason for audit logs. /// Thrown when the client does not have the permission. /// Thrown when the user does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task UnbanAsync(DiscordGuild guild, string reason = null) => guild.UnbanMemberAsync(this, reason); /// /// Gets this user's presence. /// [JsonIgnore] public DiscordPresence Presence => this.Discord is DiscordClient dc && dc.Presences.TryGetValue(this.Id, out var presence) ? presence : null; /// /// Gets the user's avatar URL, in requested format and size. /// /// Format of the avatar to get. /// Maximum size of the avatar. Must be a power of two, minimum 16, maximum 2048. /// URL of the user's avatar. public string GetAvatarUrl(ImageFormat fmt, ushort size = 1024) { if (fmt == ImageFormat.Unknown) throw new ArgumentException("You must specify valid image format.", nameof(fmt)); if (size < 16 || size > 2048) throw new ArgumentOutOfRangeException(nameof(size)); var log = Math.Log(size, 2); if (log < 4 || log > 11 || log % 1 != 0) throw new ArgumentOutOfRangeException(nameof(size)); var sfmt = ""; sfmt = fmt switch { ImageFormat.Gif => "gif", ImageFormat.Jpeg => "jpg", ImageFormat.Png => "png", ImageFormat.WebP => "webp", ImageFormat.Auto => !string.IsNullOrWhiteSpace(this.AvatarHash) ? this.AvatarHash.StartsWith("a_") ? "gif" : "png" : "png", _ => throw new ArgumentOutOfRangeException(nameof(fmt)), }; var ssize = size.ToString(CultureInfo.InvariantCulture); if (!string.IsNullOrWhiteSpace(this.AvatarHash)) { var id = this.Id.ToString(CultureInfo.InvariantCulture); return $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.AVATARS}/{id}/{this.AvatarHash}.{sfmt}?size={ssize}"; } else { var type = (this.DiscriminatorInt % 5).ToString(CultureInfo.InvariantCulture); return $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.EMBED}{Endpoints.AVATARS}/{type}.{sfmt}?size={ssize}"; } } /// /// Returns a string representation of this user. /// /// String representation of this user. public override string ToString() => $"User {this.Id}; {this.Username}#{this.Discriminator}"; /// /// Checks whether this is equal to another object. /// /// Object to compare to. /// Whether the object is equal to this . public override bool Equals(object obj) => this.Equals(obj as DiscordUser); /// /// Checks whether this is equal to another . /// /// to compare to. /// Whether the is equal to this . public bool Equals(DiscordUser e) => e is not null && (ReferenceEquals(this, e) || this.Id == e.Id); /// /// Gets the hash code for this . /// /// The hash code for this . public override int GetHashCode() => this.Id.GetHashCode(); /// /// Gets whether the two objects are equal. /// /// First user to compare. /// Second user to compare. /// Whether the two users are equal. public static bool operator ==(DiscordUser e1, DiscordUser e2) { var o1 = e1 as object; var o2 = e2 as object; return (o1 != null || o2 == null) && (o1 == null || o2 != null) && ((o1 == null && o2 == null) || e1.Id == e2.Id); } /// /// Gets whether the two objects are not equal. /// /// First user to compare. /// Second user to compare. /// Whether the two users are not equal. public static bool operator !=(DiscordUser e1, DiscordUser e2) => !(e1 == e2); } /// /// Represents a user comparer. /// internal class DiscordUserComparer : IEqualityComparer { /// /// Whether the users are equal. /// /// The first user /// The second user. public bool Equals(DiscordUser x, DiscordUser y) => x.Equals(y); /// /// Gets the hash code. /// /// The user. public int GetHashCode(DiscordUser obj) => obj.Id.GetHashCode(); } diff --git a/DisCatSharp/Enums/Application/ApplicationCommandPermissionType.cs b/DisCatSharp/Enums/Application/ApplicationCommandPermissionType.cs index c1652cf2e..f393ddfcc 100644 --- a/DisCatSharp/Enums/Application/ApplicationCommandPermissionType.cs +++ b/DisCatSharp/Enums/Application/ApplicationCommandPermissionType.cs @@ -1,47 +1,47 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; namespace DisCatSharp; /// - /// Represents the type of the application command permission. - /// +/// Represents the type of the application command permission. +/// [Flags] public enum ApplicationCommandPermissionType { /// /// The permission is bound to a role. /// Role = 1, /// /// The permission is bound to a user. /// User = 2, /// /// The permission is bound to a channel. /// Channel = 3 } diff --git a/DisCatSharp/Enums/Interaction/ButtonStyle.cs b/DisCatSharp/Enums/Interaction/ButtonStyle.cs index 05f1a024d..4fb9b4be4 100644 --- a/DisCatSharp/Enums/Interaction/ButtonStyle.cs +++ b/DisCatSharp/Enums/Interaction/ButtonStyle.cs @@ -1,54 +1,54 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. namespace DisCatSharp.Enums; /// /// Represents a button's style/color. /// public enum ButtonStyle : int { /// /// Blurple button. /// Primary = 1, /// /// Grey button. /// Secondary = 2, /// /// Green button. /// Success = 3, - /// - /// Red button. - /// - Danger = 4, + /// + /// Red button. + /// + Danger = 4, - /// - /// Link Button. - /// - Link = 5 - } + /// + /// Link Button. + /// + Link = 5 +} diff --git a/DisCatSharp/Net/Rest/DiscordApiClient.cs b/DisCatSharp/Net/Rest/DiscordApiClient.cs index 68e7f450e..e44ee44c3 100644 --- a/DisCatSharp/Net/Rest/DiscordApiClient.cs +++ b/DisCatSharp/Net/Rest/DiscordApiClient.cs @@ -1,5272 +1,5271 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Globalization; using System.Linq; using System.Net; using System.Text; using System.Threading.Tasks; -using System.Xml.Linq; 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. /// Whether this query will be transmitted via POST. private static string BuildQueryString(IDictionary values, bool post = false) { if (values == null || values.Count == 0) return string.Empty; var valsCollection = values.Select(xkvp => $"{WebUtility.UrlEncode(xkvp.Key)}={WebUtility.UrlEncode(xkvp.Value)}"); var vals = string.Join("&", valsCollection); return !post ? $"?{vals}" : vals; } /// /// Prepares the message. /// /// The msg_raw. /// A DiscordMessage. private DiscordMessage PrepareMessage(JToken msgRaw) { var author = msgRaw["author"].ToObject(); var ret = msgRaw.ToDiscordObject(); ret.Discord = this.Discord; this.PopulateMessage(author, ret); var referencedMsg = msgRaw["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 message. 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, GuildId = guild.Id }; ret.Author = mbr; } else { ret.Author = usr; } } ret.PopulateMentions(); if (ret.ReactionsInternal == null) ret.ReactionsInternal = new List(); foreach (var xr in ret.ReactionsInternal) 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 guildId, 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 = guildId }, 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, GuildId = guildId }); } return mbrs; } /// /// Gets the guild ban async. /// /// The guild_id. /// The user_id. internal async Task GetGuildBanAsync(ulong guildId, ulong userId) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.BANS}/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {guild_id = guildId, user_id = userId}, 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 regionId, Optional iconb64, VerificationLevel? verificationLevel, DefaultMessageNotifications? defaultMessageNotifications, SystemChannelFlags? systemChannelFlags) { var pld = new RestGuildCreatePayload { Name = name, RegionId = regionId, DefaultMessageNotifications = defaultMessageNotifications, VerificationLevel = verificationLevel, IconBase64 = iconb64, SystemChannelFlags = systemChannelFlags }; 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 rawMembers = (JArray)json["members"]; var guild = json.ToDiscordObject(); if (this.Discord is DiscordClient dc) await dc.OnGuildCreateEventAsync(guild, rawMembers, null).ConfigureAwait(false); return guild; } /// /// Creates the guild from template async. /// /// The template_code. /// The name. /// The iconb64. internal async Task CreateGuildFromTemplateAsync(string templateCode, 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 = templateCode }, 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 rawMembers = (JArray)json["members"]; var guild = json.ToDiscordObject(); if (this.Discord is DiscordClient dc) await dc.OnGuildCreateEventAsync(guild, rawMembers, null).ConfigureAwait(false); return guild; } /// /// Deletes the guild async. /// /// The guild_id. internal async Task DeleteGuildAsync(ulong guildId) { var route = $"{Endpoints.GUILDS}/:guild_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new {guild_id = guildId }, 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.GuildsInternal[guildId]; 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 discoverySplashb64, 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 = discoverySplashb64, 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.RolesInternal.Values) r.GuildId = 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 = Optional.FromNullable(description), 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.RolesInternal.Values) r.GuildId = guild.Id; if (this.Discord is DiscordClient dc) await dc.OnGuildUpdateEventAsync(guild, rawMembers).ConfigureAwait(false); return guild; } /// /// Implements https://discord.com/developers/docs/resources/guild#get-guild-bans. /// internal async Task> GetGuildBansAsync(ulong guildId, int? limit, ulong? before, ulong? after) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.BANS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {guild_id = guildId }, out var path); var urlParams = new Dictionary(); if (limit != null) 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); 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 bansRaw = 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(bansRaw)); return bans; } /// /// Creates the guild ban async. /// /// The guild_id. /// The user_id. /// The delete_message_days. /// The reason. internal Task CreateGuildBanAsync(ulong guildId, ulong userId, int deleteMessageDays, string reason) { if (deleteMessageDays < 0 || deleteMessageDays > 7) throw new ArgumentException("Delete message days must be a number between 0 and 7.", nameof(deleteMessageDays)); var urlParams = new Dictionary { ["delete_message_days"] = deleteMessageDays.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 = guildId, user_id = userId }, 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. internal Task RemoveGuildBanAsync(ulong guildId, ulong userId, 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 = guildId, user_id = userId }, 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. internal Task LeaveGuildAsync(ulong guildId) { var route = $"{Endpoints.USERS}{Endpoints.ME}{Endpoints.GUILDS}/:guild_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new {guild_id = guildId }, 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. internal async Task AddGuildMemberAsync(ulong guildId, ulong userId, string accessToken, string nick, IEnumerable roles, bool muted, bool deafened) { var pld = new RestGuildMemberAddPayload { AccessToken = accessToken, 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 = guildId, user_id = userId }, 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, GuildId = guildId }; } /// /// Lists the guild members async. /// /// The guild_id. /// The limit. /// The after. internal async Task> ListGuildMembersAsync(ulong guildId, 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 = 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).ConfigureAwait(false); var membersRaw = JsonConvert.DeserializeObject>(res.Response); return new ReadOnlyCollection(membersRaw); } /// /// Adds the guild member role async. /// /// The guild_id. /// The user_id. /// The role_id. /// The reason. internal Task AddGuildMemberRoleAsync(ulong guildId, ulong userId, ulong roleId, 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 = guildId, user_id = userId, role_id = roleId }, 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. internal Task RemoveGuildMemberRoleAsync(ulong guildId, ulong userId, ulong roleId, 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 = guildId, user_id = userId, role_id = roleId }, 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. internal Task ModifyGuildChannelPositionAsync(ulong guildId, 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 = guildId }, 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. internal Task ModifyGuildChannelParentAsync(ulong guildId, 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 = guildId }, 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. internal Task DetachGuildChannelParentAsync(ulong guildId, 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 = guildId }, 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. internal Task ModifyGuildRolePositionAsync(ulong guildId, 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 = guildId }, 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. internal async Task GetAuditLogsAsync(ulong guildId, int limit, ulong? after, ulong? before, ulong? responsible, int? actionType) { 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 (actionType != null) urlParams["action_type"] = actionType?.ToString(CultureInfo.InvariantCulture); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.AUDIT_LOGS}"; 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).ConfigureAwait(false); var auditLogDataRaw = JsonConvert.DeserializeObject(res.Response); return auditLogDataRaw; } /// /// Gets the guild vanity url async. /// /// The guild_id. internal async Task GetGuildVanityUrlAsync(ulong guildId) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.VANITY_URL}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, 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.GET, route).ConfigureAwait(false); var invite = JsonConvert.DeserializeObject(res.Response); return invite; } /// /// Gets the guild widget async. /// /// The guild_id. internal async Task GetGuildWidgetAsync(ulong guildId) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.WIDGET_JSON}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, 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.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[guildId]; 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. internal async Task GetGuildWidgetSettingsAsync(ulong guildId) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.WIDGET}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, 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.GET, route).ConfigureAwait(false); var ret = JsonConvert.DeserializeObject(res.Response); ret.Guild = this.Discord.Guilds[guildId]; return ret; } /// /// Modifies the guild widget settings async. /// /// The guild_id. /// If true, is enabled. /// The channel id. /// The reason. internal async Task ModifyGuildWidgetSettingsAsync(ulong guildId, 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 = 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 ret = JsonConvert.DeserializeObject(res.Response); ret.Guild = this.Discord.Guilds[guildId]; return ret; } /// /// Gets the guild templates async. /// /// The guild_id. internal async Task> GetGuildTemplatesAsync(ulong guildId) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.TEMPLATES}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, 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.GET, route).ConfigureAwait(false); var templatesRaw = JsonConvert.DeserializeObject>(res.Response); return new ReadOnlyCollection(new List(templatesRaw)); } /// /// Creates the guild template async. /// /// The guild_id. /// The name. /// The description. internal async Task CreateGuildTemplateAsync(ulong guildId, 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 = guildId }, 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. internal async Task SyncGuildTemplateAsync(ulong guildId, string templateCode) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.TEMPLATES}/:template_code"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new {guild_id = guildId, template_code = templateCode }, 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 templateRaw = JsonConvert.DeserializeObject(res.Response); return templateRaw; } /// /// Modifies the guild template async. /// /// The guild_id. /// The template_code. /// The name. /// The description. internal async Task ModifyGuildTemplateAsync(ulong guildId, string templateCode, 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 = guildId, template_code = templateCode }, 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 templateRaw = JsonConvert.DeserializeObject(res.Response); return templateRaw; } /// /// Deletes the guild template async. /// /// The guild_id. /// The template_code. internal async Task DeleteGuildTemplateAsync(ulong guildId, string templateCode) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.TEMPLATES}/:template_code"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new {guild_id = guildId, template_code = templateCode }, 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 templateRaw = JsonConvert.DeserializeObject(res.Response); return templateRaw; } /// /// Gets the guild membership screening form async. /// /// The guild_id. internal async Task GetGuildMembershipScreeningFormAsync(ulong guildId) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBER_VERIFICATION}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, 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.GET, route).ConfigureAwait(false); var screeningRaw = JsonConvert.DeserializeObject(res.Response); return screeningRaw; } /// /// Modifies the guild membership screening form async. /// /// The guild_id. /// The enabled. /// The fields. /// The description. internal async Task ModifyGuildMembershipScreeningFormAsync(ulong guildId, 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 = guildId }, 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 screeningRaw = JsonConvert.DeserializeObject(res.Response); return screeningRaw; } /// /// Gets the guild welcome screen async. /// /// The guild_id. internal async Task GetGuildWelcomeScreenAsync(ulong guildId) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.WELCOME_SCREEN}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, 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.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. internal async Task ModifyGuildWelcomeScreenAsync(ulong guildId, 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 = guildId }, 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. internal async Task UpdateCurrentUserVoiceStateAsync(ulong guildId, 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 = guildId }, 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. internal async Task UpdateUserVoiceStateAsync(ulong guildId, ulong userId, 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 = guildId, user_id = userId }, 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 guildId, ulong? channelId, DiscordScheduledEventEntityMetadata metadata, string name, DateTimeOffset scheduledStartTime, DateTimeOffset? scheduledEndTime, string description, ScheduledEventEntityType type, Optional coverb64, string reason = null) { var pld = new RestGuildScheduledEventCreatePayload { ChannelId = channelId, EntityMetadata = metadata, Name = name, ScheduledStartTime = scheduledStartTime, ScheduledEndTime = scheduledEndTime, Description = description, EntityType = type, CoverBase64 = coverb64 }; 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 = guildId }, 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 scheduledEvent = JsonConvert.DeserializeObject(res.Response); var guild = this.Discord.Guilds[guildId]; scheduledEvent.Discord = this.Discord; if (scheduledEvent.Creator != null) scheduledEvent.Creator.Discord = this.Discord; if (this.Discord is DiscordClient dc) await dc.OnGuildScheduledEventCreateEventAsync(scheduledEvent, guild); return scheduledEvent; } /// /// Modifies a scheduled event. /// internal async Task ModifyGuildScheduledEventAsync(ulong guildId, ulong scheduledEventId, Optional channelId, Optional metadata, Optional name, Optional scheduledStartTime, Optional scheduledEndTime, Optional description, Optional type, Optional status, Optional coverb64, string reason = null) { var pld = new RestGuildScheduledEventModifyPayload { ChannelId = channelId, EntityMetadata = metadata, Name = name, ScheduledStartTime = scheduledStartTime, ScheduledEndTime = scheduledEndTime, Description = description, EntityType = type, Status = status, CoverBase64 = coverb64 }; 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 = guildId, scheduled_event_id = scheduledEventId }, 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 scheduledEvent = JsonConvert.DeserializeObject(res.Response); var guild = this.Discord.Guilds[guildId]; scheduledEvent.Discord = this.Discord; if (scheduledEvent.Creator != null) { scheduledEvent.Creator.Discord = this.Discord; this.Discord.UserCache.AddOrUpdate(scheduledEvent.Creator.Id, scheduledEvent.Creator, (id, old) => { old.Username = scheduledEvent.Creator.Username; old.Discriminator = scheduledEvent.Creator.Discriminator; old.AvatarHash = scheduledEvent.Creator.AvatarHash; old.Flags = scheduledEvent.Creator.Flags; return old; }); } if (this.Discord is DiscordClient dc) await dc.OnGuildScheduledEventUpdateEventAsync(scheduledEvent, guild); return scheduledEvent; } /// /// Modifies a scheduled event. /// internal async Task ModifyGuildScheduledEventStatusAsync(ulong guildId, ulong scheduledEventId, ScheduledEventStatus status, string reason = null) { var pld = new RestGuildScheduledEventModifyPayload { 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 = guildId, scheduled_event_id = scheduledEventId }, 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 scheduledEvent = JsonConvert.DeserializeObject(res.Response); var guild = this.Discord.Guilds[guildId]; scheduledEvent.Discord = this.Discord; if (scheduledEvent.Creator != null) { scheduledEvent.Creator.Discord = this.Discord; this.Discord.UserCache.AddOrUpdate(scheduledEvent.Creator.Id, scheduledEvent.Creator, (id, old) => { old.Username = scheduledEvent.Creator.Username; old.Discriminator = scheduledEvent.Creator.Discriminator; old.AvatarHash = scheduledEvent.Creator.AvatarHash; old.Flags = scheduledEvent.Creator.Flags; return old; }); } if (this.Discord is DiscordClient dc) await dc.OnGuildScheduledEventUpdateEventAsync(scheduledEvent, guild); return scheduledEvent; } /// /// Gets a scheduled event. /// /// The guild_id. /// The event id. /// Whether to include user count. internal async Task GetGuildScheduledEventAsync(ulong guildId, ulong scheduledEventId, bool? withUserCount) { var urlParams = new Dictionary(); if (withUserCount.HasValue) urlParams["with_user_count"] = withUserCount?.ToString(); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.SCHEDULED_EVENTS}/:scheduled_event_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {guild_id = guildId, scheduled_event_id = scheduledEventId }, 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 scheduledEvent = JsonConvert.DeserializeObject(res.Response); var guild = this.Discord.Guilds[guildId]; scheduledEvent.Discord = this.Discord; if (scheduledEvent.Creator != null) { scheduledEvent.Creator.Discord = this.Discord; this.Discord.UserCache.AddOrUpdate(scheduledEvent.Creator.Id, scheduledEvent.Creator, (id, old) => { old.Username = scheduledEvent.Creator.Username; old.Discriminator = scheduledEvent.Creator.Discriminator; old.AvatarHash = scheduledEvent.Creator.AvatarHash; old.Flags = scheduledEvent.Creator.Flags; return old; }); } return scheduledEvent; } /// /// 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 guildId, bool? withUserCount) { var urlParams = new Dictionary(); if (withUserCount.HasValue) urlParams["with_user_count"] = withUserCount?.ToString(); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.SCHEDULED_EVENTS}"; 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); var events = new Dictionary(); var eventsRaw = JsonConvert.DeserializeObject>(res.Response); var guild = this.Discord.Guilds[guildId]; foreach (var ev in eventsRaw) { 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 scheduled event. /// /// The guild_id. /// The scheduled event id. /// The reason. internal Task DeleteGuildScheduledEventAsync(ulong guildId, ulong scheduledEventId, 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 = guildId, scheduled_event_id = scheduledEventId }, 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 scheduled event. /// Optional with member objects. /// This endpoint is paginated. /// /// The guild_id. /// The scheduled event id. /// The limit how many users to receive from the event. /// Get results before the given id. /// Get results after the given id. /// Whether to include guild member data. attaches guild_member property to the user object. internal async Task> GetGuildScheduledEventRspvUsersAsync(ulong guildId, ulong scheduledEventId, int? limit, ulong? before, ulong? after, bool? withMember) { 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 (withMember != null) urlParams["with_member"] = withMember.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 = guildId, scheduled_event_id = scheduledEventId }, 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 rspvUsers = JsonConvert.DeserializeObject>(res.Response); Dictionary rspv = new(); foreach (var rspvUser in rspvUsers) { rspvUser.Discord = this.Discord; rspvUser.GuildId = guildId; rspvUser.User.Discord = this.Discord; rspvUser.User = this.Discord.UserCache.AddOrUpdate(rspvUser.User.Id, rspvUser.User, (id, old) => { old.Username = rspvUser.User.Username; old.Discriminator = rspvUser.User.Discriminator; old.AvatarHash = rspvUser.User.AvatarHash; old.BannerHash = rspvUser.User.BannerHash; old.BannerColorInternal = rspvUser.User.BannerColorInternal; return old; }); /*if (with_member.HasValue && with_member.Value && rspv_user.Member != null) { rspv_user.Member.Discord = this.Discord; }*/ rspv.Add(rspvUser.User.Id, rspvUser); } 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 default auto archive duration. /// The reason. internal async Task CreateGuildChannelAsync(ulong guildId, string name, ChannelType type, ulong? parent, Optional topic, int? bitrate, int? userLimit, IEnumerable overwrites, bool? nsfw, Optional perUserRateLimit, VideoQualityMode? qualityMode, ThreadAutoArchiveDuration? defaultAutoArchiveDuration, 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 = userLimit, PermissionOverwrites = restOverwrites, Nsfw = nsfw, PerUserRateLimit = perUserRateLimit, QualityMode = qualityMode, DefaultAutoArchiveDuration = defaultAutoArchiveDuration }; 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 = guildId }, 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.PermissionOverwritesInternal) { xo.Discord = this.Discord; xo.ChannelId = 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 channelId, string name, int? position, Optional topic, bool? nsfw, Optional parent, int? bitrate, int? userLimit, 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 = userLimit, 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 = channelId }, 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. internal async Task GetChannelAsync(ulong channelId) { var route = $"{Endpoints.CHANNELS}/:channel_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {channel_id = channelId }, 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.PermissionOverwritesInternal) { xo.Discord = this.Discord; xo.ChannelId = ret.Id; } return ret; } /// /// Deletes the channel async. /// /// The channel_id. /// The reason. internal Task DeleteChannelAsync(ulong channelId, 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 = channelId }, 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. internal async Task GetMessageAsync(ulong channelId, ulong messageId) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}/:message_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {channel_id = channelId, message_id = messageId }, 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. internal async Task CreateMessageAsync(ulong channelId, 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 = channelId }, 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. internal async Task CreateMessageAsync(ulong channelId, 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 = channelId }, 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 fileId = 0; List attachments = new(builder.Files.Count); foreach (var file in builder.Files) { DiscordAttachment att = new() { Id = fileId, Discord = this.Discord, Description = file.Description, FileName = file.FileName }; attachments.Add(att); fileId++; } 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 = channelId }, 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.FilesInternal.Where(x => x.ResetPositionTo.HasValue)) { file.Stream.Position = file.ResetPositionTo.Value; } return ret; } } /// /// Gets the guild channels async. /// /// The guild_id. internal async Task> GetGuildChannelsAsync(ulong guildId) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.CHANNELS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, 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.GET, route).ConfigureAwait(false); var channelsRaw = JsonConvert.DeserializeObject>(res.Response).Select(xc => { xc.Discord = this.Discord; return xc; }); foreach (var ret in channelsRaw) foreach (var xo in ret.PermissionOverwritesInternal) { xo.Discord = this.Discord; xo.ChannelId = ret.Id; } return new ReadOnlyCollection(new List(channelsRaw)); } /// /// 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 channelId, string topic, bool sendStartNotification, StagePrivacyLevel privacyLevel, string reason) { var pld = new RestStageInstanceCreatePayload { ChannelId = channelId, Topic = topic, PrivacyLevel = privacyLevel, SendStartNotification = sendStartNotification }; 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 channelId) { var route = $"{Endpoints.STAGE_INSTANCES}/:channel_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {channel_id = channelId }, 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 channelId, Optional topic, Optional privacyLevel, string reason) { var pld = new RestStageInstanceModifyPayload { Topic = topic, PrivacyLevel = privacyLevel }; var route = $"{Endpoints.STAGE_INSTANCES}/:channel_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new {channel_id = channelId }, 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 channelId, string reason) { var route = $"{Endpoints.STAGE_INSTANCES}/:channel_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new {channel_id = channelId }, 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. internal async Task> GetChannelMessagesAsync(ulong channelId, 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 = channelId }, 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 msgsRaw = JArray.Parse(res.Response); var msgs = new List(); foreach (var xj in msgsRaw) msgs.Add(this.PrepareMessage(xj)); return new ReadOnlyCollection(new List(msgs)); } /// /// Gets the channel message async. /// /// The channel_id. /// The message_id. internal async Task GetChannelMessageAsync(ulong channelId, ulong messageId) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}/:message_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {channel_id = channelId, message_id = messageId }, 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. internal async Task EditMessageAsync(ulong channelId, ulong messageId, Optional content, Optional> embeds, Optional> mentions, IReadOnlyList components, Optional suppressEmbed, 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.ValueOrDefault(), HasEmbed = embeds.HasValue && (embeds.Value?.Any() ?? false), Embeds = embeds.HasValue && (embeds.Value?.Any() ?? false) ? embeds.Value : null, Components = components, Flags = suppressEmbed.HasValue && (bool)suppressEmbed ? MessageFlags.SuppressedEmbeds : null, Mentions = mentions .Map(m => new DiscordMentions(m ?? Mentions.None, false, mentions.Value?.OfType().Any() ?? false)) .ValueOrDefault() }; if (files?.Count > 0) { ulong fileId = 0; List attachmentsNew = new(); foreach (var file in files) { DiscordAttachment att = new() { Id = fileId, Discord = this.Discord, Description = file.Description, FileName = file.FileName }; attachmentsNew.Add(att); fileId++; } if (attachments.HasValue && attachments.Value.Any()) attachmentsNew.AddRange(attachments.Value); pld.Attachments = attachmentsNew; 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 = channelId, message_id = messageId }, 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.ValueOrDefault(); var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}/:message_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new {channel_id = channelId, message_id = messageId }, 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. internal Task DeleteMessageAsync(ulong channelId, ulong messageId, 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 = channelId, message_id = messageId }, 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. internal Task DeleteMessagesAsync(ulong channelId, IEnumerable messageIds, string reason) { var pld = new RestChannelMessageBulkDeletePayload { Messages = messageIds }; 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 = channelId }, 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. internal async Task> GetChannelInvitesAsync(ulong channelId) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.INVITES}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {channel_id = channelId }, 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 invitesRaw = JsonConvert.DeserializeObject>(res.Response).Select(xi => { xi.Discord = this.Discord; return xi; }); return new ReadOnlyCollection(new List(invitesRaw)); } /// /// 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. internal async Task CreateChannelInviteAsync(ulong channelId, int maxAge, int maxUses, TargetType? targetType, TargetActivity? targetApplication, ulong? targetUser, bool temporary, bool unique, string reason) { var pld = new RestChannelInviteCreatePayload { MaxAge = maxAge, MaxUses = maxUses, TargetType = targetType, TargetApplication = targetApplication, TargetUserId = targetUser, 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 = channelId }, 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. internal Task DeleteChannelPermissionAsync(ulong channelId, ulong overwriteId, 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 = channelId, overwrite_id = overwriteId }, 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. internal Task EditChannelPermissionsAsync(ulong channelId, ulong overwriteId, Permissions allow, Permissions deny, string type, string reason) { var pld = new RestChannelPermissionEditPayload { Type = type, Allow = allow & PermissionMethods.FullPerms, Deny = deny & PermissionMethods.FullPerms }; 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 = channelId, overwrite_id = overwriteId }, 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. internal Task TriggerTypingAsync(ulong channelId) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.TYPING}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new {channel_id = channelId }, 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. internal async Task> GetPinnedMessagesAsync(ulong channelId) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.PINS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {channel_id = channelId }, 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 msgsRaw = JArray.Parse(res.Response); var msgs = new List(); foreach (var xj in msgsRaw) msgs.Add(this.PrepareMessage(xj)); return new ReadOnlyCollection(new List(msgs)); } /// /// Pins the message async. /// /// The channel_id. /// The message_id. internal Task PinMessageAsync(ulong channelId, ulong messageId) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.PINS}/:message_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new {channel_id = channelId, message_id = messageId }, 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. internal Task UnpinMessageAsync(ulong channelId, ulong messageId) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.PINS}/:message_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new {channel_id = channelId, message_id = messageId }, 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. internal Task AddGroupDmRecipientAsync(ulong channelId, ulong userId, string accessToken, string nickname) { var pld = new RestChannelGroupDmRecipientAddPayload { AccessToken = accessToken, 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 = channelId, user_id = userId }, 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. internal Task RemoveGroupDmRecipientAsync(ulong channelId, ulong userId) { 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 = channelId, user_id = userId }, 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. internal async Task CreateGroupDmAsync(IEnumerable accessTokens, IDictionary nicks) { var pld = new RestUserGroupDmCreatePayload { AccessTokens = accessTokens, 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. internal async Task CreateDmAsync(ulong recipientId) { var pld = new RestUserDmCreatePayload { Recipient = recipientId }; 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. internal async Task FollowChannelAsync(ulong channelId, ulong webhookChannelId) { var pld = new FollowedChannelAddPayload { WebhookChannelId = webhookChannelId }; var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.FOLLOWERS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new {channel_id = channelId }, 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. internal async Task CrosspostMessageAsync(ulong channelId, ulong messageId) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}/:message_id{Endpoints.CROSSPOST}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new {channel_id = channelId, message_id = messageId }, 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. /// internal Task GetCurrentUserAsync() => this.GetUserAsync("@me"); /// /// Gets the user async. /// /// The user_id. internal Task GetUserAsync(ulong userId) => this.GetUserAsync(userId.ToString(CultureInfo.InvariantCulture)); /// /// Gets the user async. /// /// The user_id. internal async Task GetUserAsync(string userId) { var route = $"{Endpoints.USERS}/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {user_id = userId }, 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 userRaw = JsonConvert.DeserializeObject(res.Response); var duser = new DiscordUser(userRaw) { Discord = this.Discord }; return duser; } /// /// Gets the guild member async. /// /// The guild_id. /// The user_id. internal async Task GetGuildMemberAsync(ulong guildId, ulong userId) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBERS}/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {guild_id = guildId, user_id = userId }, 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, GuildId = guildId }; } /// /// Removes the guild member async. /// /// The guild_id. /// The user_id. /// The reason. internal Task RemoveGuildMemberAsync(ulong guildId, ulong userId, 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 = guildId, user_id = userId }, 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. internal async Task ModifyCurrentUserAsync(string username, Optional base64Avatar) { var pld = new RestUserUpdateCurrentPayload { Username = username, AvatarBase64 = base64Avatar.ValueOrDefault(), AvatarSet = base64Avatar.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 userRaw = JsonConvert.DeserializeObject(res.Response); return userRaw; } /// /// Gets the current user guilds async. /// /// The limit. /// The before. /// The after. 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 guildsRaw = JsonConvert.DeserializeObject>(res.Response); var glds = guildsRaw.Select(xug => (this.Discord as DiscordClient)?.GuildsInternal[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. internal Task ModifyGuildMemberAsync(ulong guildId, ulong userId, Optional nick, Optional> roleIds, Optional mute, Optional deaf, Optional voiceChannelId, string reason) { var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var pld = new RestGuildMemberModifyPayload { Nickname = nick, RoleIds = roleIds, Deafen = deaf, Mute = mute, VoiceChannelId = voiceChannelId }; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.MEMBERS}/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new {guild_id = guildId, user_id = userId }, 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. internal Task ModifyTimeoutAsync(ulong guildId, ulong userId, 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 = guildId, user_id = userId }, 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. internal Task ModifyCurrentMemberNicknameAsync(ulong guildId, 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 = guildId }, 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. internal async Task> GetGuildRolesAsync(ulong guildId) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.ROLES}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, 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.GET, route).ConfigureAwait(false); var rolesRaw = JsonConvert.DeserializeObject>(res.Response).Select(xr => { xr.Discord = this.Discord; xr.GuildId = guildId; return xr; }); return new ReadOnlyCollection(new List(rolesRaw)); } /// /// Gets the guild async. /// /// The guild id. /// If true, with_counts. internal async Task GetGuildAsync(ulong guildId, bool? withCounts) { var urlParams = new Dictionary(); if (withCounts.HasValue) urlParams["with_counts"] = withCounts?.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.RolesInternal.Values) r.GuildId = guildRest.Id; if (this.Discord is DiscordClient dc) { await dc.OnGuildUpdateEventAsync(guildRest, rawMembers).ConfigureAwait(false); return dc.GuildsInternal[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 guildId, ulong roleId, 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.FullPerms, 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 = guildId, role_id = roleId }, 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.GuildId = guildId; return ret; } /// /// Deletes the role async. /// /// The guild_id. /// The role_id. /// The reason. internal Task DeleteRoleAsync(ulong guildId, ulong roleId, 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 = guildId, role_id = roleId }, 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. internal async Task CreateGuildRoleAsync(ulong guildId, string name, Permissions? permissions, int? color, bool? hoist, bool? mentionable, string reason) { var pld = new RestGuildRolePayload { Name = name, Permissions = permissions & PermissionMethods.FullPerms, 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 = guildId }, 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.GuildId = guildId; return ret; } #endregion #region Prune /// /// Gets the guild prune count async. /// /// The guild_id. /// The days. /// The include_roles. internal async Task GetGuildPruneCountAsync(ulong guildId, int days, IEnumerable includeRoles) { 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 (includeRoles != null) { var roleArray = includeRoles.ToArray(); var roleArrayCount = roleArray.Length; 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 = guildId }, 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. internal async Task BeginGuildPruneAsync(ulong guildId, int days, bool computePruneCount, IEnumerable includeRoles, 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"] = computePruneCount.ToString() }; var sb = new StringBuilder(); if (includeRoles != null) { var roleArray = includeRoles.ToArray(); var roleArrayCount = roleArray.Length; 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 = guildId }, 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. 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 templatesRaw = JsonConvert.DeserializeObject(res.Response); return templatesRaw; } /// /// Gets the guild integrations async. /// /// The guild_id. internal async Task> GetGuildIntegrationsAsync(ulong guildId) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.INTEGRATIONS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, 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.GET, route).ConfigureAwait(false); var integrationsRaw = JsonConvert.DeserializeObject>(res.Response).Select(xi => { xi.Discord = this.Discord; return xi; }); return new ReadOnlyCollection(new List(integrationsRaw)); } /// /// Gets the guild preview async. /// /// The guild_id. internal async Task GetGuildPreviewAsync(ulong guildId) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.PREVIEW}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, 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.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. internal async Task CreateGuildIntegrationAsync(ulong guildId, 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 = guildId }, 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. internal async Task ModifyGuildIntegrationAsync(ulong guildId, ulong integrationId, int expireBehaviour, int expireGracePeriod, bool enableEmoticons) { var pld = new RestGuildIntegrationModifyPayload { ExpireBehavior = expireBehaviour, ExpireGracePeriod = expireGracePeriod, EnableEmoticons = enableEmoticons }; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.INTEGRATIONS}/:integration_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new {guild_id = guildId, integration_id = integrationId }, 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. internal Task DeleteGuildIntegrationAsync(ulong guildId, DiscordIntegration integration) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.INTEGRATIONS}/:integration_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new {guild_id = guildId, 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. internal Task SyncGuildIntegrationAsync(ulong guildId, ulong integrationId) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.INTEGRATIONS}/:integration_id{Endpoints.SYNC}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new {guild_id = guildId, integration_id = integrationId }, 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. internal async Task> GetGuildVoiceRegionsAsync(ulong guildId) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.REGIONS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, 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.GET, route).ConfigureAwait(false); var regionsRaw = JsonConvert.DeserializeObject>(res.Response); return new ReadOnlyCollection(new List(regionsRaw)); } /// /// Gets the guild invites async. /// /// The guild_id. internal async Task> GetGuildInvitesAsync(ulong guildId) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.INVITES}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, 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.GET, route).ConfigureAwait(false); var invitesRaw = JsonConvert.DeserializeObject>(res.Response).Select(xi => { xi.Discord = this.Discord; return xi; }); return new ReadOnlyCollection(new List(invitesRaw)); } #endregion #region Invite /// /// Gets the invite async. /// /// The invite_code. /// If true, with_counts. /// If true, with_expiration. /// The scheduled event id to get. internal async Task GetInviteAsync(string inviteCode, bool? withCounts, bool? withExpiration, ulong? guildScheduledEventId) { var urlParams = new Dictionary(); if (withCounts.HasValue) urlParams["with_counts"] = withCounts?.ToString(); if (withExpiration.HasValue) urlParams["with_expiration"] = withExpiration?.ToString(); if (guildScheduledEventId.HasValue) urlParams["guild_scheduled_event_id"] = guildScheduledEventId?.ToString(); var route = $"{Endpoints.INVITES}/:invite_code"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {invite_code = inviteCode }, 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. internal async Task DeleteInviteAsync(string inviteCode, 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 = inviteCode }, 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. /// internal async Task> GetUserConnectionsAsync() { 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 connectionsRaw = JsonConvert.DeserializeObject>(res.Response).Select(xc => { xc.Discord = this.Discord; return xc; }); return new ReadOnlyCollection(new List(connectionsRaw)); } #endregion #region Voice /// /// Lists the voice regions async. /// 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. internal async Task CreateWebhookAsync(ulong channelId, string name, Optional base64Avatar, string reason) { var pld = new RestWebhookPayload { Name = name, AvatarBase64 = base64Avatar.ValueOrDefault(), AvatarSet = base64Avatar.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 = channelId }, 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. internal async Task> GetChannelWebhooksAsync(ulong channelId) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.WEBHOOKS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {channel_id = channelId }, 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 webhooksRaw = JsonConvert.DeserializeObject>(res.Response).Select(xw => { xw.Discord = this.Discord; xw.ApiClient = this; return xw; }); return new ReadOnlyCollection(new List(webhooksRaw)); } /// /// Gets the guild webhooks async. /// /// The guild_id. internal async Task> GetGuildWebhooksAsync(ulong guildId) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.WEBHOOKS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, 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.GET, route).ConfigureAwait(false); var webhooksRaw = JsonConvert.DeserializeObject>(res.Response).Select(xw => { xw.Discord = this.Discord; xw.ApiClient = this; return xw; }); return new ReadOnlyCollection(new List(webhooksRaw)); } /// /// Gets the webhook async. /// /// The webhook_id. internal async Task GetWebhookAsync(ulong webhookId) { var route = $"{Endpoints.WEBHOOKS}/:webhook_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {webhook_id = webhookId }, 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. internal async Task GetWebhookWithTokenAsync(ulong webhookId, string webhookToken) { var route = $"{Endpoints.WEBHOOKS}/:webhook_id/:webhook_token"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {webhook_id = webhookId, webhook_token = webhookToken }, 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 = webhookToken; ret.Id = webhookId; 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. internal async Task ModifyWebhookAsync(ulong webhookId, ulong channelId, string name, Optional base64Avatar, string reason) { var pld = new RestWebhookPayload { Name = name, AvatarBase64 = base64Avatar.ValueOrDefault(), AvatarSet = base64Avatar.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 = webhookId }, 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. internal async Task ModifyWebhookAsync(ulong webhookId, string name, string base64Avatar, string webhookToken, string reason) { var pld = new RestWebhookPayload { Name = name, AvatarBase64 = base64Avatar }; 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 = webhookId, webhook_token = webhookToken }, 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. internal Task DeleteWebhookAsync(ulong webhookId, 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 = webhookId }, 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. internal Task DeleteWebhookAsync(ulong webhookId, string webhookToken, 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 = webhookId, webhook_token = webhookToken }, 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. internal async Task ExecuteWebhookAsync(ulong webhookId, string webhookToken, DiscordWebhookBuilder builder, string threadId) { 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.ValueOrDefault(), AvatarUrl = builder.AvatarUrl.ValueOrDefault(), 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 fileId = 0; List attachments = new(); foreach (var file in builder.Files) { DiscordAttachment att = new() { Id = fileId, Discord = this.Discord, Description = file.Description, FileName = file.FileName, FileSize = null }; attachments.Add(att); fileId++; } 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 = webhookId, webhook_token = webhookToken }, out var path); var qub = Utilities.GetApiUriBuilderFor(path, this.Discord.Configuration).AddParameter("wait", "true"); if (threadId != null) qub.AddParameter("thread_id", threadId); 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. internal async Task ExecuteWebhookSlackAsync(ulong webhookId, string webhookToken, string jsonPayload, string threadId) { var route = $"{Endpoints.WEBHOOKS}/:webhook_id/:webhook_token{Endpoints.SLACK}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new {webhook_id = webhookId, webhook_token = webhookToken }, out var path); var qub = Utilities.GetApiUriBuilderFor(path, this.Discord.Configuration).AddParameter("wait", "true"); if (threadId != null) qub.AddParameter("thread_id", threadId); var url = qub.Build(); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: jsonPayload).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. internal async Task ExecuteWebhookGithubAsync(ulong webhookId, string webhookToken, string jsonPayload, string threadId) { var route = $"{Endpoints.WEBHOOKS}/:webhook_id/:webhook_token{Endpoints.GITHUB}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new {webhook_id = webhookId, webhook_token = webhookToken }, out var path); var qub = Utilities.GetApiUriBuilderFor(path, this.Discord.Configuration).AddParameter("wait", "true"); if (threadId != null) qub.AddParameter("thread_id", threadId); var url = qub.Build(); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.POST, route, payload: jsonPayload).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. internal async Task EditWebhookMessageAsync(ulong webhookId, string webhookToken, string messageId, DiscordWebhookBuilder builder, string threadId) { 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 fileId = 0; List attachments = new(); foreach (var file in builder.Files) { DiscordAttachment att = new() { Id = fileId, Discord = this.Discord, Description = file.Description, FileName = file.FileName, FileSize = null }; attachments.Add(att); fileId++; } 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 = webhookId, webhook_token = webhookToken, message_id = messageId }, out var path); var qub = Utilities.GetApiUriBuilderFor(path, this.Discord.Configuration); if (threadId != null) qub.AddParameter("thread_id", threadId); 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.AttachmentsInternal) 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 = webhookId, webhook_token = webhookToken, message_id = messageId }, out var path); var qub = Utilities.GetApiUriBuilderFor(path, this.Discord.Configuration); if (threadId != null) qub.AddParameter("thread_id", threadId); 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.AttachmentsInternal) 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. internal Task EditWebhookMessageAsync(ulong webhookId, string webhookToken, ulong messageId, DiscordWebhookBuilder builder, ulong threadId) => this.EditWebhookMessageAsync(webhookId, webhookToken, messageId.ToString(), builder, threadId.ToString()); /// /// Gets the webhook message async. /// /// The webhook_id. /// The webhook_token. /// The message_id. /// The thread_id. internal async Task GetWebhookMessageAsync(ulong webhookId, string webhookToken, string messageId, string threadId) { var route = $"{Endpoints.WEBHOOKS}/:webhook_id/:webhook_token{Endpoints.MESSAGES}/:message_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {webhook_id = webhookId, webhook_token = webhookToken, message_id = messageId }, out var path); var qub = Utilities.GetApiUriBuilderFor(path, this.Discord.Configuration); if (threadId != null) qub.AddParameter("thread_id", threadId); 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. internal Task GetWebhookMessageAsync(ulong webhookId, string webhookToken, ulong messageId) => this.GetWebhookMessageAsync(webhookId, webhookToken, messageId.ToString(), null); /// /// Gets the webhook message async. /// /// The webhook_id. /// The webhook_token. /// The message_id. /// The thread_id. internal Task GetWebhookMessageAsync(ulong webhookId, string webhookToken, ulong messageId, ulong threadId) => this.GetWebhookMessageAsync(webhookId, webhookToken, messageId.ToString(), threadId.ToString()); /// /// Deletes the webhook message async. /// /// The webhook_id. /// The webhook_token. /// The message_id. /// The thread_id. internal async Task DeleteWebhookMessageAsync(ulong webhookId, string webhookToken, string messageId, string threadId) { var route = $"{Endpoints.WEBHOOKS}/:webhook_id/:webhook_token{Endpoints.MESSAGES}/:message_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new {webhook_id = webhookId, webhook_token = webhookToken, message_id = messageId }, out var path); var qub = Utilities.GetApiUriBuilderFor(path, this.Discord.Configuration); if (threadId != null) qub.AddParameter("thread_id", threadId); 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. internal Task DeleteWebhookMessageAsync(ulong webhookId, string webhookToken, ulong messageId) => this.DeleteWebhookMessageAsync(webhookId, webhookToken, messageId.ToString(), null); /// /// Deletes the webhook message async. /// /// The webhook_id. /// The webhook_token. /// The message_id. /// The thread_id. internal Task DeleteWebhookMessageAsync(ulong webhookId, string webhookToken, ulong messageId, ulong threadId) => this.DeleteWebhookMessageAsync(webhookId, webhookToken, messageId.ToString(), threadId.ToString()); #endregion #region Reactions /// /// Creates the reaction async. /// /// The channel_id. /// The message_id. /// The emoji. internal Task CreateReactionAsync(ulong channelId, ulong messageId, 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 = channelId, message_id = messageId, 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. internal Task DeleteOwnReactionAsync(ulong channelId, ulong messageId, 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 = channelId, message_id = messageId, 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. internal Task DeleteUserReactionAsync(ulong channelId, ulong messageId, ulong userId, 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 = channelId, message_id = messageId, emoji, user_id = userId }, 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. internal async Task> GetReactionsAsync(ulong channelId, ulong messageId, string emoji, ulong? afterId = null, int limit = 25) { var urlParams = new Dictionary(); if (afterId.HasValue) urlParams["after"] = afterId.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 = channelId, message_id = messageId, 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 reactersRaw = JsonConvert.DeserializeObject>(res.Response); var reacters = new List(); foreach (var xr in reactersRaw) { 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. internal Task DeleteAllReactionsAsync(ulong channelId, ulong messageId, 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 = channelId, message_id = messageId }, 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. internal Task DeleteReactionsEmojiAsync(ulong channelId, ulong messageId, 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 = channelId, message_id = messageId, 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. /// /// The channel id to create the thread in. /// The optional message id to create the thread from. /// 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 CreateThreadAsync(ulong channelId, ulong? messageId, string name, ThreadAutoArchiveDuration autoArchiveDuration, ChannelType type, int? rateLimitPerUser, string reason) { var pld = new RestThreadChannelCreatePayload { Name = name, AutoArchiveDuration = autoArchiveDuration, PerUserRateLimit = rateLimitPerUser, Type = type }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.CHANNELS}/:channel_id"; if (messageId is not null) route += $"{Endpoints.MESSAGES}/:message_id"; route += Endpoints.THREADS; object param = messageId is null ? new {channel_id = channelId} : new {channel_id = channelId, message_id = messageId}; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, param, 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 threadChannel = JsonConvert.DeserializeObject(res.Response); threadChannel.Discord = this.Discord; return threadChannel; } /// /// Gets the thread. /// /// The thread id. internal async Task GetThreadAsync(ulong threadId) { var route = $"{Endpoints.CHANNELS}/:thread_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {thread_id = threadId }, 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 channelId) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.THREAD_MEMBERS}{Endpoints.ME}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new {channel_id = channelId }, 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 channelId) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.THREAD_MEMBERS}{Endpoints.ME}"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new {channel_id = channelId }, 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 channelId, ulong userId) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.THREAD_MEMBERS}/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new {channel_id = channelId, user_id = userId }, 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 channelId, ulong userId) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.THREAD_MEMBERS}/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {channel_id = channelId, user_id = userId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route); var threadMember = JsonConvert.DeserializeObject(res.Response); return threadMember; } /// /// Removes a thread member. /// /// The channel id to remove the member from. /// The user id to remove. internal async Task RemoveThreadMemberAsync(ulong channelId, ulong userId) { var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.THREAD_MEMBERS}/:user_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new {channel_id = channelId, user_id = userId }, 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 threadId) { var route = $"{Endpoints.CHANNELS}/:thread_id{Endpoints.THREAD_MEMBERS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {thread_id = threadId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route); var threadMembersRaw = JsonConvert.DeserializeObject>(res.Response); return new ReadOnlyCollection(threadMembersRaw); } /// /// Gets the active threads in a guild. /// /// The guild id. internal async Task GetActiveThreadsAsync(ulong guildId) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.THREADS}{Endpoints.THREAD_ACTIVE}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, 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.GET, route); var threadReturn = JsonConvert.DeserializeObject(res.Response); return threadReturn; } /// /// Gets the joined private archived threads in a channel. /// /// The channel id. /// Get threads before snowflake. /// Limit the results. internal async Task GetJoinedPrivateArchivedThreadsAsync(ulong channelId, 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 = channelId }, 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 threadReturn = JsonConvert.DeserializeObject(res.Response); return threadReturn; } /// /// Gets the public archived threads in a channel. /// /// The channel id. /// Get threads before snowflake. /// Limit the results. internal async Task GetPublicArchivedThreadsAsync(ulong channelId, 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 = channelId }, 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 threadReturn = JsonConvert.DeserializeObject(res.Response); return threadReturn; } /// /// Gets the private archived threads in a channel. /// /// The channel id. /// Get threads before snowflake. /// Limit the results. internal async Task GetPrivateArchivedThreadsAsync(ulong channelId, 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 = channelId }, 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 threadReturn = JsonConvert.DeserializeObject(res.Response); return threadReturn; } /// /// Modifies a thread. /// /// The thread to modify. /// The new name. /// The new locked state. /// The new archived state. /// The new per user rate limit. /// The new auto archive duration. /// The new user invitable state. /// The reason for the modification. internal Task ModifyThreadAsync(ulong threadId, string name, Optional locked, Optional archived, Optional perUserRateLimit, Optional autoArchiveDuration, 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 = threadId }, 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 threadId, 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 = threadId }, 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. internal async Task> GetGuildEmojisAsync(ulong guildId) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.EMOJIS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, 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.GET, route).ConfigureAwait(false); var emojisRaw = JsonConvert.DeserializeObject>(res.Response); this.Discord.Guilds.TryGetValue(guildId, 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. internal async Task GetGuildEmojiAsync(ulong guildId, ulong emojiId) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.EMOJIS}/:emoji_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {guild_id = guildId, emoji_id = emojiId }, 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(guildId, out var gld); var emojiRaw = JObject.Parse(res.Response); var emoji = emojiRaw.ToObject(); emoji.Guild = gld; var xtu = emojiRaw["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. internal async Task CreateGuildEmojiAsync(ulong guildId, 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 = guildId }, 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(guildId, out var gld); var emojiRaw = JObject.Parse(res.Response); var emoji = emojiRaw.ToObject(); emoji.Guild = gld; var xtu = emojiRaw["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. internal async Task ModifyGuildEmojiAsync(ulong guildId, ulong emojiId, 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 = guildId, emoji_id = emojiId }, 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(guildId, out var gld); var emojiRaw = JObject.Parse(res.Response); var emoji = emojiRaw.ToObject(); emoji.Guild = gld; var xtu = emojiRaw["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. internal Task DeleteGuildEmojiAsync(ulong guildId, ulong emojiId, 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 = guildId, emoji_id = emojiId }, 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 stickerId) { var route = $"{Endpoints.STICKERS}/:sticker_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {sticker_id = stickerId}, 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 guildId) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.STICKERS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, 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.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 guildId, ulong stickerId) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.STICKERS}/:sticker_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {guild_id = guildId, sticker_id = stickerId}, 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 guildId, 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 = guildId}, 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 guildId, ulong stickerId, 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 = guildId, sticker_id = stickerId}, 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 guildId, ulong stickerId, string reason) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.STICKERS}/:sticker_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new {guild_id = guildId, sticker_id = stickerId }, 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. /// /// The application id. /// Whether to get the full localization dict. internal async Task> GetGlobalApplicationCommandsAsync(ulong applicationId, bool withLocalizations = false) { var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.COMMANDS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {application_id = applicationId }, out var path); var querydict = new Dictionary { ["with_localizations"] = withLocalizations.ToString().ToLower() }; var url = Utilities.GetApiUriFor(path, BuildQueryString(querydict), 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(); } /// /// Bulk overwrites the global application commands. /// /// The application id. /// The commands. internal async Task> BulkOverwriteGlobalApplicationCommandsAsync(ulong applicationId, 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, NameLocalizations = command.NameLocalizations?.GetKeyValuePairs(), DescriptionLocalizations = command.DescriptionLocalizations?.GetKeyValuePairs(), DefaultMemberPermission = command.DefaultMemberPermissions, DmPermission = command.DmPermission }); } 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 = applicationId }, 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 a global application command. /// /// The applicationid. /// The command. internal async Task CreateGlobalApplicationCommandAsync(ulong applicationId, DiscordApplicationCommand command) { var pld = new RestApplicationCommandCreatePayload { Type = command.Type, Name = command.Name, Description = command.Description, Options = command.Options, NameLocalizations = command.NameLocalizations.GetKeyValuePairs(), DescriptionLocalizations = command.DescriptionLocalizations.GetKeyValuePairs(), DefaultMemberPermission = command.DefaultMemberPermissions, DmPermission = command.DmPermission }; var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.COMMANDS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new {application_id = applicationId }, 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 a global application command. /// /// The application id. /// The command id. internal async Task GetGlobalApplicationCommandAsync(ulong applicationId, ulong commandId) { var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.COMMANDS}/:command_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {application_id = applicationId, command_id = commandId }, 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 a global application command. /// /// The application id. /// The command id. /// The name. /// The description. /// The options. /// The localizations of the name. /// The localizations of the description. /// The default member permissions. /// The dm permission. internal async Task EditGlobalApplicationCommandAsync(ulong applicationId, ulong commandId, Optional name, Optional description, Optional> options, Optional nameLocalization, Optional descriptionLocalization, Optional defaultMemberPermission, Optional dmPermission) { var pld = new RestApplicationCommandEditPayload { Name = name, Description = description, Options = options, DefaultMemberPermission = defaultMemberPermission, DmPermission = dmPermission, NameLocalizations = nameLocalization.Map(l => l.GetKeyValuePairs()).ValueOrDefault(), DescriptionLocalizations = descriptionLocalization.Map(l => l.GetKeyValuePairs()).ValueOrDefault(), }; var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.COMMANDS}/:command_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new {application_id = applicationId, command_id = commandId }, 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 a global application command. /// /// The application_id. /// The command_id. internal async Task DeleteGlobalApplicationCommandAsync(ulong applicationId, ulong commandId) { var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.COMMANDS}/:command_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.DELETE, route, new {application_id = applicationId, command_id = commandId }, 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. /// /// The application id. /// The guild id. /// Whether to get the full localization dict. internal async Task> GetGuildApplicationCommandsAsync(ulong applicationId, ulong guildId, bool withLocalizations = false) { var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.GUILDS}/:guild_id{Endpoints.COMMANDS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {application_id = applicationId, guild_id = guildId }, out var path); var querydict = new Dictionary { ["with_localizations"] = withLocalizations.ToString().ToLower() }; var url = Utilities.GetApiUriFor(path, BuildQueryString(querydict), 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(); } /// /// Bulk overwrites the guild application commands. /// /// The application id. /// The guild id. /// The commands. internal async Task> BulkOverwriteGuildApplicationCommandsAsync(ulong applicationId, ulong guildId, 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, NameLocalizations = command.NameLocalizations?.GetKeyValuePairs(), DescriptionLocalizations = command.DescriptionLocalizations?.GetKeyValuePairs(), DefaultMemberPermission = command.DefaultMemberPermissions, DmPermission = command.DmPermission }); } 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 = applicationId, guild_id = guildId }, 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 a guild application command. /// /// The application id. /// The guild id. /// The command. internal async Task CreateGuildApplicationCommandAsync(ulong applicationId, ulong guildId, DiscordApplicationCommand command) { var pld = new RestApplicationCommandCreatePayload { Type = command.Type, Name = command.Name, Description = command.Description, Options = command.Options, NameLocalizations = command.NameLocalizations.GetKeyValuePairs(), DescriptionLocalizations = command.DescriptionLocalizations.GetKeyValuePairs(), DefaultMemberPermission = command.DefaultMemberPermissions, DmPermission = command.DmPermission }; var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.GUILDS}/:guild_id{Endpoints.COMMANDS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new {application_id = applicationId, guild_id = guildId }, 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 a guild application command. /// /// The application id. /// The guild id. /// The command id. internal async Task GetGuildApplicationCommandAsync(ulong applicationId, ulong guildId, ulong commandId) { 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 = applicationId, guild_id = guildId, command_id = commandId }, 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 a guild application command. /// /// The application id. /// The guild id. /// The command id. /// The name. /// The description. /// The options. /// The localizations of the name. /// The localizations of the description. /// The default member permissions. /// The dm permission. internal async Task EditGuildApplicationCommandAsync(ulong applicationId, ulong guildId, ulong commandId, Optional name, Optional description, Optional> options, Optional nameLocalization, Optional descriptionLocalization, Optional defaultMemberPermission, Optional dmPermission) { var pld = new RestApplicationCommandEditPayload { Name = name, Description = description, Options = options, DefaultMemberPermission = defaultMemberPermission, DmPermission = dmPermission, NameLocalizations = nameLocalization.Map(l => l.GetKeyValuePairs()).ValueOrDefault(), DescriptionLocalizations = descriptionLocalization.Map(l => l.GetKeyValuePairs()).ValueOrDefault(), }; 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 = applicationId, guild_id = guildId, command_id = commandId }, 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 a guild application command. /// /// The application id. /// The guild id. /// The command id. internal async Task DeleteGuildApplicationCommandAsync(ulong applicationId, ulong guildId, ulong commandId) { 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 = applicationId, guild_id = guildId, command_id = commandId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.DELETE, route); } #region Permissions 2.1 /// /// Gets the guild application command permissions. /// /// The target application id. /// The target guild id. internal async Task> GetGuildApplicationCommandPermissionsAsync(ulong applicationId, ulong guildId) { 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 = applicationId, guild_id = guildId }, 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 a guild application command permission. /// /// The target application id. /// The target guild id. /// The target command id. internal async Task GetGuildApplicationCommandPermissionAsync(ulong applicationId, ulong guildId, ulong commandId) { 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 = applicationId, guild_id = guildId, command_id = commandId }, 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; } #endregion /// /// Creates the interaction response. /// /// The interaction id. /// The interaction token. /// The type. /// The builder. internal async Task CreateInteractionResponseAsync(ulong interactionId, string interactionToken, 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 fileId = 0; List attachments = new(); foreach (var file in builder.Files) { DiscordAttachment att = new() { Id = fileId, Discord = this.Discord, Description = file.Description, FileName = file.FileName, FileSize = null }; attachments.Add(att); fileId++; } 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 = interactionId, interaction_token = interactionToken }, 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. /// /// The interaction id. /// The interaction token. /// The type. /// The builder. internal async Task CreateInteractionModalResponseAsync(ulong interactionId, string interactionToken, 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 = interactionId, interaction_token = interactionToken }, 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. /// /// The application id. /// The interaction token. internal Task GetOriginalInteractionResponseAsync(ulong applicationId, string interactionToken) => this.GetWebhookMessageAsync(applicationId, interactionToken, Endpoints.ORIGINAL, null); /// /// Edits the original interaction response. /// /// The application id. /// The interaction token. /// The builder. internal Task EditOriginalInteractionResponseAsync(ulong applicationId, string interactionToken, DiscordWebhookBuilder builder) => this.EditWebhookMessageAsync(applicationId, interactionToken, Endpoints.ORIGINAL, builder, null); /// /// Deletes the original interaction response. /// /// The application id. /// The interaction token. internal Task DeleteOriginalInteractionResponseAsync(ulong applicationId, string interactionToken) => this.DeleteWebhookMessageAsync(applicationId, interactionToken, Endpoints.ORIGINAL, null); /// /// Creates the followup message. /// /// The application id. /// The interaction token. /// The builder. internal async Task CreateFollowupMessageAsync(ulong applicationId, string interactionToken, 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 fileId = 0; List attachments = new(); foreach (var file in builder.Files) { DiscordAttachment att = new() { Id = fileId, Discord = this.Discord, Description = file.Description, FileName = file.FileName, FileSize = null }; attachments.Add(att); fileId++; } 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 = applicationId, interaction_token = interactionToken }, 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.AttachmentsInternal) { 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. /// /// The application id. /// The interaction token. /// The message id. internal Task GetFollowupMessageAsync(ulong applicationId, string interactionToken, ulong messageId) => this.GetWebhookMessageAsync(applicationId, interactionToken, messageId); /// /// Edits the followup message. /// /// The application id. /// The interaction token. /// The message id. /// The builder. internal Task EditFollowupMessageAsync(ulong applicationId, string interactionToken, ulong messageId, DiscordWebhookBuilder builder) => this.EditWebhookMessageAsync(applicationId, interactionToken, messageId.ToString(), builder, null); /// /// Deletes the followup message. /// /// The application id. /// The interaction token. /// The message id. internal Task DeleteFollowupMessageAsync(ulong applicationId, string interactionToken, ulong messageId) => this.DeleteWebhookMessageAsync(applicationId, interactionToken, messageId); #endregion #region Misc /// /// Gets the current application info async. /// internal Task GetCurrentApplicationInfoAsync() => this.GetApplicationInfoAsync("@me"); /// /// Gets the application info async. /// /// The application_id. internal Task GetApplicationInfoAsync(ulong applicationId) => this.GetApplicationInfoAsync(applicationId.ToString(CultureInfo.InvariantCulture)); /// /// Gets the application info async. /// /// The application_id. private async Task GetApplicationInfoAsync(string applicationId) { var route = $"{Endpoints.OAUTH2}{Endpoints.APPLICATIONS}/:application_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {application_id = applicationId }, 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. 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. /// 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.ResetAfterInternal); return info; } #endregion } diff --git a/DisCatSharp/Net/Rest/RestClient.cs b/DisCatSharp/Net/Rest/RestClient.cs index 7b9af5249..0602ccd35 100644 --- a/DisCatSharp/Net/Rest/RestClient.cs +++ b/DisCatSharp/Net/Rest/RestClient.cs @@ -1,872 +1,872 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Reflection; using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using DisCatSharp.Exceptions; using Microsoft.Extensions.Logging; namespace DisCatSharp.Net; /// /// Represents a client used to make REST requests. /// internal sealed class RestClient : IDisposable { /// /// Gets the route argument regex. /// private static Regex s_routeArgumentRegex { get; } = new(@":([a-z_]+)"); /// /// Gets the http client. /// internal HttpClient HttpClient { get; } /// /// Gets the discord client. /// private readonly BaseDiscordClient _discord; /// /// Gets a value indicating whether debug is enabled. /// internal bool Debug { get; set; } /// /// Gets the logger. /// private readonly ILogger _logger; /// /// Gets the routes to hashes. /// private readonly ConcurrentDictionary _routesToHashes; /// /// Gets the hashes to buckets. /// private readonly ConcurrentDictionary _hashesToBuckets; /// /// Gets the request queue. /// private readonly ConcurrentDictionary _requestQueue; /// /// Gets the global rate limit event. /// private readonly AsyncManualResetEvent _globalRateLimitEvent; /// /// Gets a value indicating whether use reset after. /// private readonly bool _useResetAfter; private CancellationTokenSource _bucketCleanerTokenSource; private readonly TimeSpan _bucketCleanupDelay = TimeSpan.FromSeconds(60); private volatile bool _cleanerRunning; private Task _cleanerTask; private volatile bool _disposed; /// /// Initializes a new instance of the class. /// /// The client. internal RestClient(BaseDiscordClient client) : this(client.Configuration.Proxy, client.Configuration.HttpTimeout, client.Configuration.UseRelativeRatelimit, client.Logger) { this._discord = client; this.HttpClient.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", Utilities.GetFormattedToken(client)); if (client.Configuration.Override != null) { this.HttpClient.DefaultRequestHeaders.TryAddWithoutValidation("x-super-properties", client.Configuration.Override); } } /// /// Initializes a new instance of the class. /// This is for meta-clients, such as the webhook client. /// /// The proxy. /// The timeout. /// Whether to use relative ratelimit. /// The logger. internal RestClient(IWebProxy proxy, TimeSpan timeout, bool useRelativeRatelimit, - ILogger logger) + ILogger logger) { this._logger = logger; var httphandler = new HttpClientHandler { UseCookies = false, AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip, UseProxy = proxy != null, Proxy = proxy }; this.HttpClient = new HttpClient(httphandler) { BaseAddress = new Uri(Utilities.GetApiBaseUri(this._discord?.Configuration)), Timeout = timeout }; this.HttpClient.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", Utilities.GetUserAgent()); if (this._discord != null && this._discord.Configuration != null && this._discord.Configuration.Override != null) { this.HttpClient.DefaultRequestHeaders.TryAddWithoutValidation("x-super-properties", this._discord.Configuration.Override); } this._routesToHashes = new ConcurrentDictionary(); this._hashesToBuckets = new ConcurrentDictionary(); this._requestQueue = new ConcurrentDictionary(); this._globalRateLimitEvent = new AsyncManualResetEvent(true); this._useResetAfter = useRelativeRatelimit; } /// /// Gets a ratelimit bucket. /// /// The method. /// The route. /// The route parameters. /// The url. /// A ratelimit bucket. public RateLimitBucket GetBucket(RestRequestMethod method, string route, object routeParams, out string url) { var rparamsProps = routeParams.GetType() .GetTypeInfo() .DeclaredProperties; var rparams = new Dictionary(); foreach (var xp in rparamsProps) { var val = xp.GetValue(routeParams); rparams[xp.Name] = val is string xs ? xs : val is DateTime dt ? dt.ToString("yyyy-MM-ddTHH:mm:sszzz", CultureInfo.InvariantCulture) : val is DateTimeOffset dto ? dto.ToString("yyyy-MM-ddTHH:mm:sszzz", CultureInfo.InvariantCulture) : val is IFormattable xf ? xf.ToString(null, CultureInfo.InvariantCulture) : val.ToString(); } var guildId = rparams.ContainsKey("guild_id") ? rparams["guild_id"] : ""; var channelId = rparams.ContainsKey("channel_id") ? rparams["channel_id"] : ""; var webhookId = rparams.ContainsKey("webhook_id") ? rparams["webhook_id"] : ""; // Create a generic route (minus major params) key // ex: POST:/channels/channel_id/messages var hashKey = RateLimitBucket.GenerateHashKey(method, route); // We check if the hash is present, using our generic route (without major params) // ex: in POST:/channels/channel_id/messages, out 80c17d2f203122d936070c88c8d10f33 // If it doesn't exist, we create an unlimited hash as our initial key in the form of the hash key + the unlimited constant // and assign this to the route to hash cache // ex: this.RoutesToHashes[POST:/channels/channel_id/messages] = POST:/channels/channel_id/messages:unlimited var hash = this._routesToHashes.GetOrAdd(hashKey, RateLimitBucket.GenerateUnlimitedHash(method, route)); // Next we use the hash to generate the key to obtain the bucket. // ex: 80c17d2f203122d936070c88c8d10f33:guild_id:506128773926879242:webhook_id // or if unlimited: POST:/channels/channel_id/messages:unlimited:guild_id:506128773926879242:webhook_id var bucketId = RateLimitBucket.GenerateBucketId(hash, guildId, channelId, webhookId); // If it's not in cache, create a new bucket and index it by its bucket id. var bucket = this._hashesToBuckets.GetOrAdd(bucketId, new RateLimitBucket(hash, guildId, channelId, webhookId)); bucket.LastAttemptAt = DateTimeOffset.UtcNow; // Cache the routes for each bucket so it can be used for GC later. if (!bucket.RouteHashes.Contains(bucketId)) bucket.RouteHashes.Add(bucketId); // Add the current route to the request queue, which indexes the amount // of requests occurring to the bucket id. _ = this._requestQueue.TryGetValue(bucketId, out var count); // Increment by one atomically due to concurrency this._requestQueue[bucketId] = Interlocked.Increment(ref count); // Start bucket cleaner if not already running. if (!this._cleanerRunning) { this._cleanerRunning = true; this._bucketCleanerTokenSource = new CancellationTokenSource(); this._cleanerTask = Task.Run(this.CleanupBucketsAsync, this._bucketCleanerTokenSource.Token); this._logger.LogDebug(LoggerEvents.RestCleaner, "Bucket cleaner task started."); } url = s_routeArgumentRegex.Replace(route, xm => rparams[xm.Groups[1].Value]); return bucket; } /// /// Executes the request. /// /// The request to be executed. public Task ExecuteRequestAsync(BaseRestRequest request) => request == null ? throw new ArgumentNullException(nameof(request)) : this.ExecuteRequestAsync(request, null, null); /// /// Executes the request. /// This is to allow proper rescheduling of the first request from a bucket. /// /// The request to be executed. /// The bucket. /// The ratelimit task completion source. private async Task ExecuteRequestAsync(BaseRestRequest request, RateLimitBucket bucket, TaskCompletionSource ratelimitTcs) { if (this._disposed) return; HttpResponseMessage res = default; try { await this._globalRateLimitEvent.WaitAsync().ConfigureAwait(false); if (bucket == null) bucket = request.RateLimitBucket; if (ratelimitTcs == null) ratelimitTcs = await this.WaitForInitialRateLimit(bucket).ConfigureAwait(false); if (ratelimitTcs == null) // ckeck rate limit only if we are not the probe request { var now = DateTimeOffset.UtcNow; await bucket.TryResetLimitAsync(now).ConfigureAwait(false); // Decrement the remaining number of requests as there can be other concurrent requests before this one finishes and has a chance to update the bucket if (Interlocked.Decrement(ref bucket.RemainingInternal) < 0) { this._logger.LogDebug(LoggerEvents.RatelimitDiag, "Request for {0} is blocked", bucket.ToString()); var delay = bucket.Reset - now; var resetDate = bucket.Reset; if (this._useResetAfter) { delay = bucket.ResetAfter.Value; resetDate = bucket.ResetAfterOffset; } if (delay < new TimeSpan(-TimeSpan.TicksPerMinute)) { this._logger.LogError(LoggerEvents.RatelimitDiag, "Failed to retrieve ratelimits - giving up and allowing next request for bucket"); bucket.RemainingInternal = 1; } if (delay < TimeSpan.Zero) delay = TimeSpan.FromMilliseconds(100); this._logger.LogWarning(LoggerEvents.RatelimitPreemptive, "Preemptive ratelimit triggered - waiting until {0:yyyy-MM-dd HH:mm:ss zzz} ({1:c}).", resetDate, delay); Task.Delay(delay) .ContinueWith(_ => this.ExecuteRequestAsync(request, null, null)) .LogTaskFault(this._logger, LogLevel.Error, LoggerEvents.RestError, "Error while executing request"); return; } this._logger.LogDebug(LoggerEvents.RatelimitDiag, "Request for {0} is allowed", bucket.ToString()); } else this._logger.LogDebug(LoggerEvents.RatelimitDiag, "Initial request for {0} is allowed", bucket.ToString()); var req = this.BuildRequest(request); if (this.Debug) this._logger.LogTrace(LoggerEvents.Misc, await req.Content.ReadAsStringAsync()); var response = new RestResponse(); try { if (this._disposed) return; res = await this.HttpClient.SendAsync(req, HttpCompletionOption.ResponseContentRead, CancellationToken.None).ConfigureAwait(false); var bts = await res.Content.ReadAsByteArrayAsync().ConfigureAwait(false); var txt = Utilities.UTF8.GetString(bts, 0, bts.Length); this._logger.LogTrace(LoggerEvents.RestRx, txt); response.Headers = res.Headers.ToDictionary(xh => xh.Key, xh => string.Join("\n", xh.Value), StringComparer.OrdinalIgnoreCase); response.Response = txt; response.ResponseCode = (int)res.StatusCode; } catch (HttpRequestException httpex) { this._logger.LogError(LoggerEvents.RestError, httpex, "Request to {0} triggered an HttpException", request.Url); request.SetFaulted(httpex); this.FailInitialRateLimitTest(request, ratelimitTcs); return; } this.UpdateBucket(request, response, ratelimitTcs); Exception ex = null; switch (response.ResponseCode) { case 400: case 405: ex = new BadRequestException(request, response); break; case 401: case 403: ex = new UnauthorizedException(request, response); break; case 404: ex = new NotFoundException(request, response); break; case 413: ex = new RequestSizeException(request, response); break; case 429: ex = new RateLimitException(request, response); // check the limit info and requeue this.Handle429(response, out var wait, out var global); if (wait != null) { if (global) { bucket.IsGlobal = true; this._logger.LogError(LoggerEvents.RatelimitHit, "Global ratelimit hit, cooling down"); try { this._globalRateLimitEvent.Reset(); await wait.ConfigureAwait(false); } finally { // we don't want to wait here until all the blocked requests have been run, additionally Set can never throw an exception that could be suppressed here _ = this._globalRateLimitEvent.SetAsync(); } this.ExecuteRequestAsync(request, bucket, ratelimitTcs) .LogTaskFault(this._logger, LogLevel.Error, LoggerEvents.RestError, "Error while retrying request"); } else { if (this._discord is DiscordClient) { await (this._discord as DiscordClient)._rateLimitHit.InvokeAsync(this._discord as DiscordClient, new EventArgs.RateLimitExceptionEventArgs(this._discord.ServiceProvider) { Exception = ex as RateLimitException, ApiEndpoint = request.Url.AbsoluteUri }); } this._logger.LogError(LoggerEvents.RatelimitHit, "Ratelimit hit, requeuing request to {0}", request.Url); await wait.ConfigureAwait(false); this.ExecuteRequestAsync(request, bucket, ratelimitTcs) .LogTaskFault(this._logger, LogLevel.Error, LoggerEvents.RestError, "Error while retrying request"); } return; } break; case 500: case 502: case 503: case 504: ex = new ServerErrorException(request, response); break; } if (ex != null) request.SetFaulted(ex); else request.SetCompleted(response); } catch (Exception ex) { this._logger.LogError(LoggerEvents.RestError, ex, "Request to {0} triggered an exception", request.Url); // if something went wrong and we couldn't get rate limits for the first request here, allow the next request to run if (bucket != null && ratelimitTcs != null && bucket.LimitTesting != 0) this.FailInitialRateLimitTest(request, ratelimitTcs); if (!request.TrySetFaulted(ex)) throw; } finally { res?.Dispose(); // Get and decrement active requests in this bucket by 1. _ = this._requestQueue.TryGetValue(bucket.BucketId, out var count); this._requestQueue[bucket.BucketId] = Interlocked.Decrement(ref count); // If it's 0 or less, we can remove the bucket from the active request queue, // along with any of its past routes. if (count <= 0) { foreach (var r in bucket.RouteHashes) { if (this._requestQueue.ContainsKey(r)) { _ = this._requestQueue.TryRemove(r, out _); } } } } } /// /// Fails the initial rate limit test. /// /// The request. /// The ratelimit task completion source. /// Whether to reset to initial values. private void FailInitialRateLimitTest(BaseRestRequest request, TaskCompletionSource ratelimitTcs, bool resetToInitial = false) { if (ratelimitTcs == null && !resetToInitial) return; var bucket = request.RateLimitBucket; bucket.LimitValid = false; bucket.LimitTestFinished = null; bucket.LimitTesting = 0; //Reset to initial values. if (resetToInitial) { this.UpdateHashCaches(request, bucket); bucket.Maximum = 0; bucket.RemainingInternal = 0; return; } // no need to wait on all the potentially waiting tasks _ = Task.Run(() => ratelimitTcs.TrySetResult(false)); } /// /// Waits for the initial rate limit. /// /// The bucket. private async Task> WaitForInitialRateLimit(RateLimitBucket bucket) { while (!bucket.LimitValid) { if (bucket.LimitTesting == 0) { if (Interlocked.CompareExchange(ref bucket.LimitTesting, 1, 0) == 0) { // if we got here when the first request was just finishing, we must not create the waiter task as it would signal ExecureRequestAsync to bypass rate limiting if (bucket.LimitValid) return null; // allow exactly one request to go through without having rate limits available var ratelimitsTcs = new TaskCompletionSource(); bucket.LimitTestFinished = ratelimitsTcs.Task; return ratelimitsTcs; } } // it can take a couple of cycles for the task to be allocated, so wait until it happens or we are no longer probing for the limits Task waitTask = null; while (bucket.LimitTesting != 0 && (waitTask = bucket.LimitTestFinished) == null) await Task.Yield(); if (waitTask != null) await waitTask.ConfigureAwait(false); // if the request failed and the response did not have rate limit headers we have allow the next request and wait again, thus this is a loop here } return null; } /// /// Builds the request. /// /// The request. /// A http request message. private HttpRequestMessage BuildRequest(BaseRestRequest request) { var req = new HttpRequestMessage(new HttpMethod(request.Method.ToString()), request.Url); if (request.Headers != null && request.Headers.Any()) foreach (var kvp in request.Headers) req.Headers.Add(kvp.Key, kvp.Value); if (request is RestRequest nmprequest && !string.IsNullOrWhiteSpace(nmprequest.Payload)) { this._logger.LogTrace(LoggerEvents.RestTx, nmprequest.Payload); req.Content = new StringContent(nmprequest.Payload); req.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); } if (request is MultipartWebRequest mprequest) { this._logger.LogTrace(LoggerEvents.RestTx, ""); var boundary = "---------------------------" + DateTime.Now.Ticks.ToString("x"); req.Headers.Add("Connection", "keep-alive"); req.Headers.Add("Keep-Alive", "600"); var content = new MultipartFormDataContent(boundary); if (mprequest.Values != null && mprequest.Values.Any()) foreach (var kvp in mprequest.Values) content.Add(new StringContent(kvp.Value), kvp.Key); var fileId = mprequest.OverwriteFileIdStart ?? 0; if (mprequest.Files != null && mprequest.Files.Any()) { foreach (var f in mprequest.Files) { var name = $"files[{fileId.ToString(CultureInfo.InvariantCulture)}]"; content.Add(new StreamContent(f.Value), name, f.Key); fileId++; } } req.Content = content; } if (request is MultipartStickerWebRequest mpsrequest) { this._logger.LogTrace(LoggerEvents.RestTx, ""); var boundary = "---------------------------" + DateTime.Now.Ticks.ToString("x"); req.Headers.Add("Connection", "keep-alive"); req.Headers.Add("Keep-Alive", "600"); var sc = new StreamContent(mpsrequest.File.Stream); if (mpsrequest.File.ContentType != null) sc.Headers.ContentType = new MediaTypeHeaderValue(mpsrequest.File.ContentType); var fileName = mpsrequest.File.FileName; if (mpsrequest.File.FileType != null) fileName += '.' + mpsrequest.File.FileType; var content = new MultipartFormDataContent(boundary) { { new StringContent(mpsrequest.Name), "name" }, { new StringContent(mpsrequest.Tags), "tags" }, { new StringContent(mpsrequest.Description), "description" }, { sc, "file", fileName } }; req.Content = content; } return req; } /// /// Handles the HTTP 429 status. /// /// The response. /// The wait task. /// If true, global. private void Handle429(RestResponse response, out Task waitTask, out bool global) { waitTask = null; global = false; if (response.Headers == null) return; var hs = response.Headers; // handle the wait if (hs.TryGetValue("Retry-After", out var retryAfterRaw)) { var retryAfter = TimeSpan.FromSeconds(int.Parse(retryAfterRaw, CultureInfo.InvariantCulture)); waitTask = Task.Delay(retryAfter); } // check if global b1nzy if (hs.TryGetValue("X-RateLimit-Global", out var isGlobal) && isGlobal.ToLowerInvariant() == "true") { // global global = true; } } /// /// Updates the bucket. /// /// The request. /// The response. /// The ratelimit task completion source. private void UpdateBucket(BaseRestRequest request, RestResponse response, TaskCompletionSource ratelimitTcs) { var bucket = request.RateLimitBucket; if (response.Headers == null) { if (response.ResponseCode != 429) // do not fail when ratelimit was or the next request will be scheduled hitting the rate limit again this.FailInitialRateLimitTest(request, ratelimitTcs); return; } var hs = response.Headers; if (hs.TryGetValue("X-RateLimit-Scope", out var scope)) { bucket.Scope = scope; } if (hs.TryGetValue("X-RateLimit-Global", out var isGlobal) && isGlobal.ToLowerInvariant() == "true") { if (response.ResponseCode != 429) { bucket.IsGlobal = true; this.FailInitialRateLimitTest(request, ratelimitTcs); } return; } var r1 = hs.TryGetValue("X-RateLimit-Limit", out var usesMax); var r2 = hs.TryGetValue("X-RateLimit-Remaining", out var usesLeft); var r3 = hs.TryGetValue("X-RateLimit-Reset", out var reset); var r4 = hs.TryGetValue("X-Ratelimit-Reset-After", out var resetAfter); var r5 = hs.TryGetValue("X-Ratelimit-Bucket", out var hash); if (!r1 || !r2 || !r3 || !r4) { //If the limits were determined before this request, make the bucket initial again. if (response.ResponseCode != 429) this.FailInitialRateLimitTest(request, ratelimitTcs, ratelimitTcs == null); return; } var clientTime = DateTimeOffset.UtcNow; var resetTime = new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero).AddSeconds(double.Parse(reset, CultureInfo.InvariantCulture)); var serverTime = clientTime; if (hs.TryGetValue("Date", out var rawDate)) serverTime = DateTimeOffset.Parse(rawDate, CultureInfo.InvariantCulture).ToUniversalTime(); var resetDelta = resetTime - serverTime; //var difference = clientTime - serverTime; //if (Math.Abs(difference.TotalSeconds) >= 1) //// this.Logger.LogMessage(LogLevel.DebugBaseDiscordClient.RestEventId, $"Difference between machine and server time: {difference.TotalMilliseconds.ToString("#,##0.00", CultureInfo.InvariantCulture)}ms", DateTime.Now); //else // difference = TimeSpan.Zero; if (request.RateLimitWaitOverride.HasValue) resetDelta = TimeSpan.FromSeconds(request.RateLimitWaitOverride.Value); var newReset = clientTime + resetDelta; if (this._useResetAfter) { bucket.ResetAfter = TimeSpan.FromSeconds(double.Parse(resetAfter, CultureInfo.InvariantCulture)); newReset = clientTime + bucket.ResetAfter.Value + (request.RateLimitWaitOverride.HasValue ? resetDelta : TimeSpan.Zero); bucket.ResetAfterOffset = newReset; } else bucket.Reset = newReset; var maximum = int.Parse(usesMax, CultureInfo.InvariantCulture); var remaining = int.Parse(usesLeft, CultureInfo.InvariantCulture); if (ratelimitTcs != null) { // initial population of the ratelimit data bucket.SetInitialValues(maximum, remaining, newReset); _ = Task.Run(() => ratelimitTcs.TrySetResult(true)); } else { // only update the bucket values if this request was for a newer interval than the one // currently in the bucket, to avoid issues with concurrent requests in one bucket // remaining is reset by TryResetLimit and not the response, just allow that to happen when it is time if (bucket.NextReset == 0) bucket.NextReset = newReset.UtcTicks; } this.UpdateHashCaches(request, bucket, hash); } /// /// Updates the hash caches. /// /// The request. /// The bucket. /// The new hash. private void UpdateHashCaches(BaseRestRequest request, RateLimitBucket bucket, string newHash = null) { var hashKey = RateLimitBucket.GenerateHashKey(request.Method, request.Route); if (!this._routesToHashes.TryGetValue(hashKey, out var oldHash)) return; // This is an unlimited bucket, which we don't need to keep track of. if (newHash == null) { _ = this._routesToHashes.TryRemove(hashKey, out _); _ = this._hashesToBuckets.TryRemove(bucket.BucketId, out _); return; } // Only update the hash once, due to a bug on Discord's end. // This will cause issues if the bucket hashes are dynamically changed from the API while running, // in which case, Dispose will need to be called to clear the caches. if (bucket.IsUnlimited && newHash != oldHash) { this._logger.LogDebug(LoggerEvents.RestHashMover, "Updating hash in {0}: \"{1}\" -> \"{2}\"", hashKey, oldHash, newHash); var bucketId = RateLimitBucket.GenerateBucketId(newHash, bucket.GuildId, bucket.ChannelId, bucket.WebhookId); _ = this._routesToHashes.AddOrUpdate(hashKey, newHash, (key, oldHash) => { bucket.Hash = newHash; var oldBucketId = RateLimitBucket.GenerateBucketId(oldHash, bucket.GuildId, bucket.ChannelId, bucket.WebhookId); // Remove the old unlimited bucket. _ = this._hashesToBuckets.TryRemove(oldBucketId, out _); _ = this._hashesToBuckets.AddOrUpdate(bucketId, bucket, (key, oldBucket) => bucket); return newHash; }); } return; } /// /// Cleans the buckets. /// private async Task CleanupBucketsAsync() { while (!this._bucketCleanerTokenSource.IsCancellationRequested) { try { await Task.Delay(this._bucketCleanupDelay, this._bucketCleanerTokenSource.Token).ConfigureAwait(false); } catch { } if (this._disposed) return; //Check and clean request queue first in case it wasn't removed properly during requests. foreach (var key in this._requestQueue.Keys) { var bucket = this._hashesToBuckets.Values.FirstOrDefault(x => x.RouteHashes.Contains(key)); if (bucket == null || (bucket != null && bucket.LastAttemptAt.AddSeconds(5) < DateTimeOffset.UtcNow)) _ = this._requestQueue.TryRemove(key, out _); } var removedBuckets = 0; StringBuilder bucketIdStrBuilder = default; foreach (var kvp in this._hashesToBuckets) { if (bucketIdStrBuilder == null) bucketIdStrBuilder = new StringBuilder(); var key = kvp.Key; var value = kvp.Value; // Don't remove the bucket if it's currently being handled by the rest client, unless it's an unlimited bucket. if (this._requestQueue.ContainsKey(value.BucketId) && !value.IsUnlimited) continue; var resetOffset = this._useResetAfter ? value.ResetAfterOffset : value.Reset; // Don't remove the bucket if it's reset date is less than now + the additional wait time, unless it's an unlimited bucket. if (!value.IsUnlimited && (resetOffset > DateTimeOffset.UtcNow || DateTimeOffset.UtcNow - resetOffset < this._bucketCleanupDelay)) continue; _ = this._hashesToBuckets.TryRemove(key, out _); removedBuckets++; bucketIdStrBuilder.Append(value.BucketId + ", "); } if (removedBuckets > 0) this._logger.LogDebug(LoggerEvents.RestCleaner, "Removed {0} unused bucket{1}: [{2}]", removedBuckets, removedBuckets > 1 ? "s" : string.Empty, bucketIdStrBuilder.ToString().TrimEnd(',', ' ')); if (this._hashesToBuckets.Count == 0) break; } if (!this._bucketCleanerTokenSource.IsCancellationRequested) this._bucketCleanerTokenSource.Cancel(); this._cleanerRunning = false; this._logger.LogDebug(LoggerEvents.RestCleaner, "Bucket cleaner task stopped."); } ~RestClient() { this.Dispose(); } /// /// Disposes the rest client. /// public void Dispose() { if (this._disposed) return; this._disposed = true; this._globalRateLimitEvent.Reset(); if (this._bucketCleanerTokenSource?.IsCancellationRequested == false) { this._bucketCleanerTokenSource?.Cancel(); this._logger.LogDebug(LoggerEvents.RestCleaner, "Bucket cleaner task stopped."); } try { this._cleanerTask?.Dispose(); this._bucketCleanerTokenSource?.Dispose(); this.HttpClient?.Dispose(); } catch { } this._routesToHashes.Clear(); this._hashesToBuckets.Clear(); this._requestQueue.Clear(); } } diff --git a/DisCatSharp/Net/Serialization/DiscordComponentJsonConverter.cs b/DisCatSharp/Net/Serialization/DiscordComponentJsonConverter.cs index 4073a234e..570007189 100644 --- a/DisCatSharp/Net/Serialization/DiscordComponentJsonConverter.cs +++ b/DisCatSharp/Net/Serialization/DiscordComponentJsonConverter.cs @@ -1,92 +1,92 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021-2022 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using DisCatSharp.Entities; using DisCatSharp.Enums; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace DisCatSharp.Net.Serialization; /// /// Represents a discord component json converter. /// internal sealed class DiscordComponentJsonConverter : JsonConverter { /// /// Whether the converter can write. /// public override bool CanWrite => false; /// /// Writes the json. /// /// The writer. /// The value. /// The serializer. public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) => throw new NotImplementedException(); /// /// Reads the json. /// /// The reader. /// The object type. /// The existing value. /// The serializer. public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { if (reader.TokenType == JsonToken.Null) return null; var job = JObject.Load(reader); var type = job["type"]?.ToObject(); if (type == null) throw new ArgumentException($"Value {reader} does not have a component type specifier"); - DiscordComponent cmp; + DiscordComponent cmp; cmp = type switch { ComponentType.ActionRow => new DiscordActionRowComponent(), ComponentType.Button when (string)job["url"] is not null => new DiscordLinkButtonComponent(), ComponentType.Button => new DiscordButtonComponent(), ComponentType.Select => new DiscordSelectComponent(), ComponentType.InputText => new DiscordTextComponent(), _ => new DiscordComponent() { Type = type.Value } }; // Populate the existing component with the values in the JObject. This avoids a recursive JsonConverter loop using var jreader = job.CreateReader(); - serializer.Populate(jreader, cmp); + serializer.Populate(jreader, cmp); return cmp; } /// /// Whether the json can convert. /// /// The object type. public override bool CanConvert(Type objectType) => typeof(DiscordComponent).IsAssignableFrom(objectType); }