diff --git a/DisCatSharp.ApplicationCommands/ApplicationCommandsConfiguration.cs b/DisCatSharp.ApplicationCommands/ApplicationCommandsConfiguration.cs index 358b25eb2..dd7bc65d0 100644 --- a/DisCatSharp.ApplicationCommands/ApplicationCommandsConfiguration.cs +++ b/DisCatSharp.ApplicationCommands/ApplicationCommandsConfiguration.cs @@ -1,75 +1,75 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using Microsoft.Extensions.DependencyInjection; namespace DisCatSharp.ApplicationCommands { /// /// A configuration for a /// public class ApplicationCommandsConfiguration { /// /// Sets the service provider. /// Objects in this provider are used when instantiating application command modules. This allows passing data around without resorting to static members. /// Defaults to null. /// public IServiceProvider ServiceProvider { internal get; set; } = new ServiceCollection().BuildServiceProvider(true); /// /// Sets whether to enable default help command. /// Disabling this will allow you to make your own help command. /// /// /// Defaults to true. /// public bool EnableDefaultHelp { internal get; set; } = true; /// /// Debugs the startups expected and actual count. /// - public bool DebugStartupCounts { internal get; set; } = false; + public bool DebugStartupCounts { internal get; set; } /// /// Initializes a new instance of the class. /// /// The service provider. [ActivatorUtilitiesConstructor] public ApplicationCommandsConfiguration(IServiceProvider provider = null) { this.ServiceProvider = provider; } /// /// Creates a new instance of , copying the properties of another configuration. /// /// Configuration the properties of which are to be copied. public ApplicationCommandsConfiguration(ApplicationCommandsConfiguration acc) { this.EnableDefaultHelp = acc.EnableDefaultHelp; this.ServiceProvider = acc.ServiceProvider; this.DebugStartupCounts = acc.DebugStartupCounts; } } } diff --git a/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs b/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs index 927d3a333..36a2aa9b5 100644 --- a/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs +++ b/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs @@ -1,1776 +1,1774 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.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(); + private static List s_commandMethods { get; set; } = new(); /// /// List of groups. /// - private static List s_groupCommands { get; set; } = new List(); + private static List s_groupCommands { get; set; } = new(); /// /// List of groups with subgroups. /// - private static List s_subGroupCommands { get; set; } = new List(); + private static List s_subGroupCommands { get; set; } = new(); /// /// List of context menus. /// - private static List s_contextMenuCommands { get; set; } = new List(); + private static List s_contextMenuCommands { get; set; } = new(); /// /// List of global commands on discords backend. /// - internal static List GlobalDiscordCommands { get; set; } = null; + internal static List GlobalDiscordCommands { get; set; } /// /// List of guild commands on discords backend. /// - internal static Dictionary> GuildDiscordCommands { get; set; } = null; + internal static Dictionary> GuildDiscordCommands { get; set; } /// /// Singleton modules. /// - private static List s_singletonModules { get; set; } = new List(); + 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; } = false; + private static bool s_errored { get; set; } /// /// Gets a list of registered commands. The key is the guild id (null if global). /// public IReadOnlyList>> RegisteredCommands => s_registeredCommands; private static readonly List>> s_registeredCommands = new(); /// /// Gets a list of registered global commands. /// public IReadOnlyList GlobalCommands => GlobalCommandsInternal; internal static readonly List GlobalCommandsInternal = new(); /// /// Gets a list of registered guild commands mapped by guild id. /// public IReadOnlyDictionary> GuildCommands => GuildCommandsInternal; internal static readonly Dictionary> GuildCommandsInternal = new(); /// /// Gets the registration count. /// - private static int s_registrationCount { get; set; } = 0; + private static int s_registrationCount { get; set; } /// /// Gets the expected count. /// - private static int s_expectedCount { get; set; } = 0; + 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. /// private static bool s_debugEnabled { get; set; } /// /// Initializes a new instance of the class. /// /// The configuration. internal ApplicationCommandsExtension(ApplicationCommandsConfiguration configuration) { Configuration = configuration; s_debugEnabled = configuration.DebugStartupCounts; } /// /// 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; } /// /// Registers a command class. /// /// The command class to register. /// The guild id to register it on. If you want global commands, leave it null. public void RegisterCommands(ulong? guildId = null) where T : ApplicationCommandsModule { if (this.Client.ShardId == 0) this._updateList.Add(new KeyValuePair(guildId, new ApplicationCommandsModuleConfiguration(typeof(T)))); } /// /// Registers a command class. /// /// The of the command class to register. /// The guild id to register it on. If you want global commands, leave it null. public void RegisterCommands(Type type, ulong? guildId = null) { if (!typeof(ApplicationCommandsModule).IsAssignableFrom(type)) throw new ArgumentException("Command classes have to inherit from ApplicationCommandsModule", nameof(type)); //If sharding, only register for shard 0 if (this.Client.ShardId == 0) this._updateList.Add(new KeyValuePair(guildId, new ApplicationCommandsModuleConfiguration(type))); } /// /// Cleans all guild application commands. /// public async Task CleanGuildCommandsAsync() { foreach (var guild in this.Client.Guilds.Values) { await this.Client.BulkOverwriteGuildApplicationCommandsAsync(guild.Id, Array.Empty()); } } /// /// Cleans the global application commands. /// public async Task CleanGlobalCommandsAsync() => await this.Client.BulkOverwriteGlobalApplicationCommandsAsync(Array.Empty()); /// /// Registers a command class with permission and translation setup. /// /// The command class to register. /// The guild id to register it on. /// A callback to setup permissions with. /// A callback to setup translations with. public void RegisterCommands(ulong guildId, Action permissionSetup = null, Action translationSetup = null) where T : ApplicationCommandsModule { if (this.Client.ShardId == 0) this._updateList.Add(new KeyValuePair(guildId, new ApplicationCommandsModuleConfiguration(typeof(T), permissionSetup, translationSetup))); } /// /// Registers a command class with permission and translation setup. /// /// The of the command class to register. /// The guild id to register it on. /// A callback to setup permissions with. /// A callback to setup translations with. public void RegisterCommands(Type type, ulong guildId, Action permissionSetup = null, Action translationSetup = null) { if (!typeof(ApplicationCommandsModule).IsAssignableFrom(type)) throw new ArgumentException("Command classes have to inherit from ApplicationCommandsModule", nameof(type)); //If sharding, only register for shard 0 if (this.Client.ShardId == 0) this._updateList.Add(new KeyValuePair(guildId, new ApplicationCommandsModuleConfiguration(type, permissionSetup, translationSetup))); } /* /// /// Registers a command class with permission setup but without a guild id. /// /// The command class to register. /// A callback to setup permissions with. public void RegisterCommands(Action permissionSetup = null) where T : ApplicationCommandsModule { if (this.Client.ShardId == 0) this._updateList.Add(new KeyValuePair(null, new ApplicationCommandsModuleConfiguration(typeof(T), permissionSetup))); } /// /// Registers a command class with permission setup but without a guild id. /// /// The of the command class to register. /// A callback to setup permissions with. public void RegisterCommands(Type type, Action permissionSetup = null) { if (!typeof(ApplicationCommandsModule).IsAssignableFrom(type)) throw new ArgumentException("Command classes have to inherit from ApplicationCommandsModule", nameof(type)); //If sharding, only register for shard 0 if (this.Client.ShardId == 0) this._updateList.Add(new KeyValuePair(null, new ApplicationCommandsModuleConfiguration(type, permissionSetup))); } */ /// /// Fired when the application commands module is ready. /// public event AsyncEventHandler ApplicationCommandsModuleReady { - add { this._applicationCommandsModuleReady.Register(value); } - remove { this._applicationCommandsModuleReady.Unregister(value); } + 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); } + 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); } + 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); } + 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(); + GlobalDiscordCommands = new List(); + GuildDiscordCommands = new Dictionary>(); var commandsPending = this._updateList.Select(x => x.Key).Distinct(); s_expectedCount = commandsPending.Count(); if (s_debugEnabled) this.Client.Logger.LogDebug($"Expected Count: {s_expectedCount}"); List failedGuilds = new(); IEnumerable globalCommands = null; globalCommands = await this.Client.GetGlobalApplicationCommandsAsync() ?? null; foreach (var guild in this.Client.Guilds.Keys) { IEnumerable commands = null; var unauthorized = false; try { commands = await this.Client.GetGuildApplicationCommandsAsync(guild) ?? 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)) { foreach (var key in commandsPending.ToList()) { this._updateList.Add(new KeyValuePair (key, new ApplicationCommandsModuleConfiguration(typeof(DefaultHelpModule)))); } } if (globalCommands != null && globalCommands.Any()) GlobalDiscordCommands.AddRange(globalCommands); foreach (var key in commandsPending.ToList()) { this.Client.Logger.LogDebug(key.HasValue ? $"Registering commands in guild {key.Value}" : "Registering global commands."); this.RegisterCommands(this._updateList.Where(x => x.Key == key).Select(x => x.Value), key); } this._missingScopeGuildIds = failedGuilds; await this._applicationCommandsModuleReady.InvokeAsync(this, new ApplicationCommandsModuleReadyEventArgs(Configuration?.ServiceProvider) { Handled = true, GuildsWithoutScope = failedGuilds }); } } /// /// Method for registering commands for a target from modules. /// /// The types. /// The optional guild id. private void RegisterCommands(IEnumerable types, ulong? guildid) { //Initialize empty lists to be added to the global ones at the end var commandMethods = new List(); var groupCommands = new List(); var subGroupCommands = new List(); var contextMenuCommands = new List(); var updateList = new List(); var commandTypeSources = new List>(); _ = Task.Run(async () => { //Iterates over all the modules foreach (var config in types) { var type = config.Type; try { var module = type.GetTypeInfo(); var classes = new List(); 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 slashGroupsTulpe = NestedCommandWorker.ParseSlashGroupsAsync(type, classes, guildid, groupTranslations).Result; if (slashGroupsTulpe.applicationCommands != null && slashGroupsTulpe.applicationCommands.Any()) updateList.AddRange(slashGroupsTulpe.applicationCommands); if (slashGroupsTulpe.commandTypeSources != null && slashGroupsTulpe.commandTypeSources.Any()) commandTypeSources.AddRange(slashGroupsTulpe.commandTypeSources); if (slashGroupsTulpe.singletonModules != null && slashGroupsTulpe.singletonModules.Any()) s_singletonModules.AddRange(slashGroupsTulpe.singletonModules); if (slashGroupsTulpe.groupCommands != null && slashGroupsTulpe.groupCommands.Any()) groupCommands.AddRange(slashGroupsTulpe.groupCommands); if (slashGroupsTulpe.subGroupCommands != null && slashGroupsTulpe.subGroupCommands.Any()) subGroupCommands.AddRange(slashGroupsTulpe.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 = await CommandWorker.ParseContextMenuCommands(type, contextMethods, commandTranslations); if (contextCommands.applicationCommands != null && contextCommands.applicationCommands.Any()) updateList.AddRange(contextCommands.applicationCommands); if (contextCommands.commandTypeSources != null && contextCommands.commandTypeSources.Any()) commandTypeSources.AddRange(contextCommands.commandTypeSources); if (contextCommands.contextMenuCommands != null && contextCommands.contextMenuCommands.Any()) contextMenuCommands.AddRange(contextCommands.contextMenuCommands); //Accounts for lifespans if (module.GetCustomAttribute() != null && module.GetCustomAttribute().Lifespan == ApplicationCommandModuleLifespan.Singleton) { s_singletonModules.Add(CreateInstance(module, Configuration?.ServiceProvider)); } } } catch (Exception ex) { if (ex is BadRequestException brex) this.Client.Logger.LogCritical(brex, $"There was an error registering application commands: {brex.JsonMessage}"); else this.Client.Logger.LogCritical(ex, $"There was an error registering application commands"); s_errored = true; } } if (!s_errored) { try { List commands = new(); try { if (guildid == null) { if (updateList != null && updateList.Any()) { var regCommands = await RegistrationWorker.RegisterGlobalCommandsAsync(updateList); var actualCommands = regCommands.Distinct().ToList(); commands.AddRange(actualCommands); GlobalCommandsInternal.AddRange(actualCommands); } else { foreach (var cmd in GlobalDiscordCommands) { try { await this.Client.DeleteGlobalApplicationCommandAsync(cmd.Id); } catch (NotFoundException) { this.Client.Logger.LogError($"Could not delete global command {cmd.Id}. Please clean up manually"); } } } } else { if (updateList != null && updateList.Any()) { var regCommands = await RegistrationWorker.RegisterGuilldCommandsAsync(guildid.Value, updateList); var actualCommands = regCommands.Distinct().ToList(); commands.AddRange(actualCommands); GuildCommandsInternal.Add(guildid.Value, actualCommands); if (this.Client.Guilds.TryGetValue(guildid.Value, out var guild)) { - guild.InternalRegisteredApplicationCommands = new(); + guild.InternalRegisteredApplicationCommands = new List(); 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.LogError($"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; var source = commandTypeSources.FirstOrDefault(f => f.Key == com.Method.DeclaringType); await PermissionWorker.UpdateCommandPermissionAsync(types, guildid, command.Id, com.Name, source.Value, source.Key); } else if (groupCommands.GetFirstValueWhere(x => x.Name == command.Name, out var groupCom)) { groupCom.CommandId = command.Id; foreach (var gCom in groupCom.Methods) { var source = commandTypeSources.FirstOrDefault(f => f.Key == gCom.Value.DeclaringType); await PermissionWorker.UpdateCommandPermissionAsync(types, guildid, groupCom.CommandId, gCom.Key, source.Key, source.Value); } } else if (subGroupCommands.GetFirstValueWhere(x => x.Name == command.Name, out var subCom)) { subCom.CommandId = command.Id; foreach (var groupComs in subCom.SubCommands) { foreach (var gCom in groupComs.Methods) { var source = commandTypeSources.FirstOrDefault(f => f.Key == gCom.Value.DeclaringType); await PermissionWorker.UpdateCommandPermissionAsync(types, guildid, subCom.CommandId, gCom.Key, source.Key, source.Value); } } } else if (contextMenuCommands.GetFirstValueWhere(x => x.Name == command.Name, out var cmCom)) { cmCom.CommandId = command.Id; var source = commandTypeSources.First(f => f.Key == cmCom.Method.DeclaringType); await PermissionWorker.UpdateCommandPermissionAsync(types, guildid, command.Id, cmCom.Name, source.Value, source.Key); } } //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); } s_registrationCount++; if (s_debugEnabled) this.Client.Logger.LogDebug($"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 }); } this.CheckRegistrationStartup(); } catch (Exception ex) { if (ex is BadRequestException brex) this.Client.Logger.LogCritical(brex, $"There was an error registering application commands: {brex.JsonMessage}"); else this.Client.Logger.LogCritical(ex, $"There was an error registering application commands"); s_errored = true; } } }); } private async void CheckRegistrationStartup() { if (s_debugEnabled) this.Client.Logger.LogDebug($"Checking counts...\n\nExpected Count: {s_expectedCount}\nCurrent Count: {s_registrationCount}"); if (s_registrationCount == s_expectedCount) { await this._applicationCommandsModuleStartupFinished.InvokeAsync(this, new ApplicationCommandsModuleStartupFinishedEventArgs(Configuration?.ServiceProvider) { 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 method = methods.First().Method; var group = subgroups.First().SubCommands.First(x => x.Name == command.Name); var focusedOption = command.Options.First(x => x.Name == group.Name).Options.First(o => o.Focused); this.Client.Logger.LogDebug("SUBGROUP::" + focusedOption.Name + ": " + focusedOption.RawValue); var option = group.Methods.First(p => p.Value.GetCustomAttribute().Name == focusedOption.Name).Value; var provider = option.GetCustomAttribute().ProviderType; var providerMethod = provider.GetMethod(nameof(IAutocompleteProvider.Provider)); var providerInstance = Activator.CreateInstance(provider); var context = new AutocompleteContext { Interaction = e.Interaction, Services = this._configuration?.Services, ApplicationCommandsExtension = this, Guild = e.Interaction.Guild, Channel = e.Interaction.Channel, User = e.Interaction.User, Options = command.Options.First(x => x.Name == group.Name).Options.ToList(), FocusedOption = focusedOption }; var choices = await (Task>) providerMethod.Invoke(providerInstance, new[] { context }); await e.Interaction.CreateResponseAsync(InteractionResponseType.AutoCompleteResult, new DiscordInteractionResponseBuilder().AddAutoCompleteChoices(choices)); }*/ } catch (Exception ex) { this.Client.Logger.LogError(ex, "Error in autocomplete interaction"); } } }); return Task.CompletedTask; } /// /// Context menu handler. /// /// The client. /// The event args. private Task ContextMenuHandler(DiscordClient client, ContextMenuInteractionCreateEventArgs e) { _ = Task.Run(async () => { //Creates the context var context = new ContextMenuContext { Interaction = e.Interaction, Channel = e.Interaction.Channel, Client = client, Services = 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) { await (Task)method.Invoke(classInstance, args.ToArray()); await (module?.AfterSlashExecutionAsync(slashContext) ?? Task.CompletedTask); } } // Context menus if (context is ContextMenuContext contextMenuContext) { await this.RunPreexecutionChecksAsync(method, contextMenuContext); var shouldExecute = await (module?.BeforeContextMenuExecutionAsync(contextMenuContext) ?? Task.FromResult(true)); if (shouldExecute) { await (Task)method.Invoke(classInstance, args.ToArray()); await (module?.AfterContextMenuExecutionAsync(contextMenuContext) ?? Task.CompletedTask); } } } /// /// Property injection /// /// 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 if (e.Interaction.Data.Resolved.Attachments != null && e.Interaction.Data.Resolved.Attachments.TryGetValue((ulong)option.Value, out var attachment)) args.Add(attachment); else throw new ArgumentException("Error resolving mentionable option."); } else throw new ArgumentException($"Error resolving interaction."); } } return args; } /// /// Runs the preexecution checks. /// /// The method info. /// The base context. private async Task RunPreexecutionChecksAsync(MethodInfo method, BaseContext context) { if (context is InteractionContext ctx) { //Gets all attributes from parent classes as well and stuff var attributes = new List(); attributes.AddRange(method.GetCustomAttributes(true)); attributes.AddRange(method.DeclaringType.GetCustomAttributes()); if (method.DeclaringType.DeclaringType != null) { attributes.AddRange(method.DeclaringType.DeclaringType.GetCustomAttributes()); if (method.DeclaringType.DeclaringType.DeclaringType != null) { attributes.AddRange(method.DeclaringType.DeclaringType.DeclaringType.GetCustomAttributes()); } } var dict = new Dictionary(); foreach (var att in attributes) { //Runs the check and adds the result to a list var result = await att.ExecuteChecksAsync(ctx); dict.Add(att, result); } //Checks if any failed, and throws an exception if (dict.Any(x => x.Value == false)) throw new SlashExecutionChecksFailedException { FailedChecks = dict.Where(x => x.Value == false).Select(x => x.Key).ToList() }; } if (context is ContextMenuContext cMctx) { var attributes = new List(); attributes.AddRange(method.GetCustomAttributes(true)); attributes.AddRange(method.DeclaringType.GetCustomAttributes()); if (method.DeclaringType.DeclaringType != null) { attributes.AddRange(method.DeclaringType.DeclaringType.GetCustomAttributes()); if (method.DeclaringType.DeclaringType.DeclaringType != null) { attributes.AddRange(method.DeclaringType.DeclaringType.DeclaringType.GetCustomAttributes()); } } var dict = new Dictionary(); foreach (var att in attributes) { //Runs the check and adds the result to a list var result = await att.ExecuteChecksAsync(cMctx); dict.Add(att, result); } //Checks if any failed, and throws an exception if (dict.Any(x => x.Value == false)) throw new ContextMenuExecutionChecksFailedException { FailedChecks = dict.Where(x => x.Value == false).Select(x => x.Key).ToList() }; } } /// /// Gets the choice attributes from choice provider. /// /// The custom attributes. /// The optional guild id private 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(DiscordChannel) ? ApplicationCommandOptionType.Channel : type == typeof(DiscordUser) ? ApplicationCommandOptionType.User : type == typeof(DiscordRole) ? ApplicationCommandOptionType.Role : type == typeof(SnowflakeObject) ? ApplicationCommandOptionType.Mentionable : type == typeof(DiscordAttachment) ? ApplicationCommandOptionType.Attachment : type.IsEnum ? ApplicationCommandOptionType.String : throw new ArgumentException("Cannot convert type! Argument types must be string, int, long, bool, double, DiscordChannel, DiscordUser, DiscordRole, SnowflakeObject, DiscordAttachment or an Enum."); return parametertype; } /// /// Gets the choice attributes from parameter. /// /// The choice attributes. - private static List GetChoiceAttributesFromParameter(IEnumerable choiceattributes) - { - return !choiceattributes.Any() + 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(); GlobalDiscordCommands = null; GuildDiscordCommands = null; GuildCommandsInternal.Clear(); GlobalCommandsInternal.Clear(); await this.UpdateAsync(); } /// /// Fires when the execution of a slash command fails. /// public event AsyncEventHandler SlashCommandErrored { - add { this._slashError.Register(value); } - remove { this._slashError.Unregister(value); } + 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); } + 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); } + 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); } + add => this._contextMenuExecuted.Register(value); + remove => this._contextMenuExecuted.Unregister(value); } private AsyncEvent _contextMenuExecuted; } /// /// Holds configuration data for setting up an application command. /// internal class ApplicationCommandsModuleConfiguration { /// /// The type of the command module. /// public Type Type { get; } /// /// The permission setup. /// public Action Setup { get; } public Action Translations { get; } /// /// Creates a new command configuration. /// /// The type of the command module. /// The permission setup callback. /// The translation setup callback. public ApplicationCommandsModuleConfiguration(Type type, Action setup = null, Action translations = null) { this.Type = type; this.Setup = setup; this.Translations = translations; } } /// /// Links a command to its original command module. /// internal class ApplicationCommandSourceLink { /// /// The command. /// public DiscordApplicationCommand ApplicationCommand { get; set; } /// /// The base/root module the command is contained in. /// public Type RootCommandContainerType { get; set; } /// /// The direct group the command is contained in. /// public Type CommandContainerType { get; set; } } /// /// The command method. /// internal class CommandMethod { /// /// Gets or sets the command id. /// public ulong CommandId { get; set; } /// /// Gets or sets the name. /// public string Name { get; set; } /// /// Gets or sets the method. /// public MethodInfo Method { get; set; } } /// /// The group command. /// internal class GroupCommand { /// /// Gets or sets the command id. /// public ulong CommandId { get; set; } /// /// Gets or sets the name. /// public string Name { get; set; } /// /// Gets or sets the methods. /// public List> Methods { get; set; } = null; } /// /// The sub group command. /// internal class SubGroupCommand { /// /// Gets or sets the command id. /// public ulong CommandId { get; set; } /// /// Gets or sets the name. /// public string Name { get; set; } /// /// Gets or sets the sub commands. /// - public List SubCommands { get; set; } = new List(); + 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. /// public class DefaultHelpModule : ApplicationCommandsModule { public class DefaultHelpAutoCompleteProvider : IAutocompleteProvider { public async Task> Provider(AutocompleteContext context) { var options = new List(); var globalCommandsTask = context.Client.GetGlobalApplicationCommandsAsync(); var guildCommandsTask= context.Client.GetGuildApplicationCommandsAsync(context.Guild.Id); await Task.WhenAll(globalCommandsTask, guildCommandsTask); var 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(); 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(); var globalCommandsTask = context.Client.GetGlobalApplicationCommandsAsync(); var guildCommandsTask= context.Client.GetGuildApplicationCommandsAsync(context.Guild.Id); await Task.WhenAll(globalCommandsTask, guildCommandsTask); var slashCommands = globalCommandsTask.Result.Concat(guildCommandsTask.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(); var globalCommandsTask = context.Client.GetGlobalApplicationCommandsAsync(); var guildCommandsTask= context.Client.GetGuildApplicationCommandsAsync(context.Guild.Id); await Task.WhenAll(globalCommandsTask, guildCommandsTask); var slashCommands = globalCommandsTask.Result.Concat(guildCommandsTask.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")] public 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("Arguments", sb.ToString().Trim(), false); } 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("Arguments", sb.ToString().Trim(), false); } 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("Arguments", sb.ToString().Trim(), false); } await ctx.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder().AddEmbed(discordEmbed).AsEphemeral(true)); } } } #endregion } diff --git a/DisCatSharp.ApplicationCommands/Workers/ApplicationCommandWorker.cs b/DisCatSharp.ApplicationCommands/Workers/ApplicationCommandWorker.cs index 78f8b7235..27ae79ac3 100644 --- a/DisCatSharp.ApplicationCommands/Workers/ApplicationCommandWorker.cs +++ b/DisCatSharp.ApplicationCommands/Workers/ApplicationCommandWorker.cs @@ -1,382 +1,382 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Threading.Tasks; using DisCatSharp.Entities; using DisCatSharp.Enums; namespace DisCatSharp.ApplicationCommands { /// /// Represents a . /// internal class CommandWorker { /// /// Parses context menu application commands. /// /// The type. /// List of method infos. /// The optional command translations. /// Too much. internal static Task< ( List applicationCommands, List> commandTypeSources, List contextMenuCommands ) > ParseContextMenuCommands(Type type, IEnumerable methods, List translator = null) { List commands = new(); List> commandTypeSources = new(); List contextMenuCommands = new(); foreach (var contextMethod in methods) { var contextAttribute = contextMethod.GetCustomAttribute(); DiscordApplicationCommandLocalization nameLocalizations = null; var commandTranslation = translator?.Single(c => c.Name == contextAttribute.Name && c.Type == contextAttribute.Type); if (commandTranslation != null) nameLocalizations = commandTranslation.NameTranslations; var command = new DiscordApplicationCommand(contextAttribute.Name, null, null, contextAttribute.DefaultPermission, contextAttribute.Type, nameLocalizations); var parameters = contextMethod.GetParameters(); if (parameters.Length == 0 || parameters == null || !ReferenceEquals(parameters.FirstOrDefault()?.ParameterType, typeof(ContextMenuContext))) throw new ArgumentException($"The first argument must be a ContextMenuContext!"); if (parameters.Length > 1) throw new ArgumentException($"A context menu cannot have parameters!"); contextMenuCommands.Add(new ContextMenuCommand { Method = contextMethod, Name = contextAttribute.Name }); commands.Add(command); commandTypeSources.Add(new KeyValuePair(type, type)); } return Task.FromResult((commands, commandTypeSources, contextMenuCommands)); } /// /// Parses single application commands. /// /// The type. /// List of method infos. /// The optional guild id. /// The optional command translations. /// Too much. internal static async Task< ( List applicationCommands, List> commandTypeSources, List commandMethods ) > ParseBasicSlashCommandsAsync(Type type, IEnumerable methods, ulong? guildId = null, List translator = null) { List commands = new(); List> commandTypeSources = new(); List commandMethods = new(); foreach (var method in methods) { var commandattribute = method.GetCustomAttribute(); var parameters = method.GetParameters(); if (parameters.Length == 0 || parameters == null || !ReferenceEquals(parameters.FirstOrDefault()?.ParameterType, typeof(InteractionContext))) throw new ArgumentException($"The first argument must be an InteractionContext!"); parameters = parameters.Skip(1).ToArray(); var options = await ApplicationCommandsExtension.ParseParametersAsync(parameters, guildId); commandMethods.Add(new CommandMethod { Method = method, Name = commandattribute.Name }); DiscordApplicationCommandLocalization nameLocalizations = null; DiscordApplicationCommandLocalization descriptionLocalizations = null; List localizisedOptions = null; var commandTranslation = translator?.Single(c => c.Name == commandattribute.Name && c.Type == ApplicationCommandType.ChatInput); if (commandTranslation != null && commandTranslation.Options != null) { - localizisedOptions = new(options.Count); + localizisedOptions = new List(options.Count); foreach (var option in options) { - List choices = option.Choices != null ? new(option.Choices.Count()) : null; + List choices = option.Choices != null ? new List(option.Choices.Count()) : null; if (option.Choices != null) { foreach (var choice in option.Choices) { choices.Add(new DiscordApplicationCommandOptionChoice(choice.Name, choice.Value, commandTranslation.Options.Single(o => o.Name == option.Name).Choices.Single(c => c.Name == choice.Name).NameTranslations)); } } localizisedOptions.Add(new DiscordApplicationCommandOption(option.Name, option.Description, option.Type, option.Required, choices, option.Options, option.ChannelTypes, option.AutoComplete, option.MinimumValue, option.MaximumValue, commandTranslation.Options.Single(o => o.Name == option.Name).NameTranslations, commandTranslation.Options.Single(o => o.Name == option.Name).DescriptionTranslations )); } nameLocalizations = commandTranslation.NameTranslations; descriptionLocalizations = commandTranslation.DescriptionTranslations; } var payload = new DiscordApplicationCommand(commandattribute.Name, commandattribute.Description, localizisedOptions ?? options, commandattribute.DefaultPermission, ApplicationCommandType.ChatInput, nameLocalizations, descriptionLocalizations); commands.Add(payload); commandTypeSources.Add(new KeyValuePair(type, type)); } return (commands, commandTypeSources, commandMethods); } } /// /// Represents a . /// internal class NestedCommandWorker { /// /// Parses application command groups. /// /// The type. /// List of type infos. /// The optional guild id. /// The optional group translations. /// Too much. internal static async Task< ( List applicationCommands, List> commandTypeSources, List singletonModules, List groupCommands, List subGroupCommands ) > ParseSlashGroupsAsync(Type type, List types, ulong? guildId = null, List translator = null) { List commands = new(); List> commandTypeSources = new(); List groupCommands = new(); List subGroupCommands = new(); List singletonModules = new(); //Handles groups foreach (var subclassinfo in types) { //Gets the attribute and methods in the group var groupAttribute = subclassinfo.GetCustomAttribute(); var submethods = subclassinfo.DeclaredMethods.Where(x => x.GetCustomAttribute() != null).ToList(); var subclasses = subclassinfo.DeclaredNestedTypes.Where(x => x.GetCustomAttribute() != null).ToList(); if (subclasses.Any() && submethods.Any()) { throw new ArgumentException("Slash command groups cannot have both subcommands and subgroups!"); } DiscordApplicationCommandLocalization nameLocalizations = null; DiscordApplicationCommandLocalization descriptionLocalizations = null; if (translator != null) { var commandTranslation = translator.Single(c => c.Name == groupAttribute.Name); if (commandTranslation != null) { nameLocalizations = commandTranslation.NameTranslations; descriptionLocalizations = commandTranslation.DescriptionTranslations; } } //Initializes the command var payload = new DiscordApplicationCommand(groupAttribute.Name, groupAttribute.Description, defaultPermission: groupAttribute.DefaultPermission, nameLocalizations: nameLocalizations, descriptionLocalizations: descriptionLocalizations); commandTypeSources.Add(new KeyValuePair(type, type)); var commandmethods = new List>(); //Handles commands in the group foreach (var submethod in submethods) { var commandAttribute = submethod.GetCustomAttribute(); //Gets the paramaters and accounts for InteractionContext var parameters = submethod.GetParameters(); if (parameters.Length == 0 || parameters == null || !ReferenceEquals(parameters.First().ParameterType, typeof(InteractionContext))) throw new ArgumentException($"The first argument must be an InteractionContext!"); parameters = parameters.Skip(1).ToArray(); var options = await ApplicationCommandsExtension.ParseParametersAsync(parameters, guildId); DiscordApplicationCommandLocalization subNameLocalizations = null; DiscordApplicationCommandLocalization subDescriptionLocalizations = null; List localizisedOptions = null; var commandTranslation = translator?.Single(c => c.Name == payload.Name); if (commandTranslation?.Commands != null) { var subCommandTranslation = commandTranslation.Commands.Single(sc => sc.Name == commandAttribute.Name); if (subCommandTranslation.Options != null) { - localizisedOptions = new(options.Count); + localizisedOptions = new List(options.Count); foreach (var option in options) { - List choices = option.Choices != null ? new(option.Choices.Count()) : null; + List choices = option.Choices != null ? new List(option.Choices.Count()) : null; if (option.Choices != null) { foreach (var choice in option.Choices) { choices.Add(new DiscordApplicationCommandOptionChoice(choice.Name, choice.Value, subCommandTranslation.Options.Single(o => o.Name == option.Name).Choices.Single(c => c.Name == choice.Name).NameTranslations)); } } localizisedOptions.Add(new DiscordApplicationCommandOption(option.Name, option.Description, option.Type, option.Required, choices, option.Options, option.ChannelTypes, option.AutoComplete, option.MinimumValue, option.MaximumValue, subCommandTranslation.Options.Single(o => o.Name == option.Name).NameTranslations, subCommandTranslation.Options.Single(o => o.Name == option.Name).DescriptionTranslations )); } } subNameLocalizations = subCommandTranslation.NameTranslations; subDescriptionLocalizations = subCommandTranslation.DescriptionTranslations; } //Creates the subcommand and adds it to the main command var subpayload = new DiscordApplicationCommandOption(commandAttribute.Name, commandAttribute.Description, ApplicationCommandOptionType.SubCommand, null, null, localizisedOptions ?? options, nameLocalizations: subNameLocalizations, descriptionLocalizations: subDescriptionLocalizations); payload = new DiscordApplicationCommand(payload.Name, payload.Description, payload.Options?.Append(subpayload) ?? new[] { subpayload }, payload.DefaultPermission, nameLocalizations: payload.NameLocalizations, descriptionLocalizations: payload.DescriptionLocalizations); commandTypeSources.Add(new KeyValuePair(subclassinfo, type)); //Adds it to the method lists commandmethods.Add(new KeyValuePair(commandAttribute.Name, submethod)); groupCommands.Add(new GroupCommand { Name = groupAttribute.Name, Methods = commandmethods }); } var command = new SubGroupCommand { Name = groupAttribute.Name }; //Handles subgroups foreach (var subclass in subclasses) { var subGroupAttribute = subclass.GetCustomAttribute(); var subsubmethods = subclass.DeclaredMethods.Where(x => x.GetCustomAttribute() != null); var options = new List(); var currentMethods = new List>(); DiscordApplicationCommandLocalization subNameLocalizations = null; DiscordApplicationCommandLocalization subDescriptionLocalizations = null; if (translator != null) { var commandTranslation = translator.Single(c => c.Name == payload.Name); if (commandTranslation != null && commandTranslation.SubGroups != null) { var subCommandTranslation = commandTranslation.SubGroups.Single(sc => sc.Name == subGroupAttribute.Name); if (subCommandTranslation != null) { subNameLocalizations = subCommandTranslation.NameTranslations; subDescriptionLocalizations = subCommandTranslation.DescriptionTranslations; } } } //Similar to the one for regular groups foreach (var subsubmethod in subsubmethods) { var suboptions = new List(); var commatt = subsubmethod.GetCustomAttribute(); var parameters = subsubmethod.GetParameters(); if (parameters.Length == 0 || parameters == null || !ReferenceEquals(parameters.First().ParameterType, typeof(InteractionContext))) throw new ArgumentException($"The first argument must be an InteractionContext!"); parameters = parameters.Skip(1).ToArray(); suboptions = suboptions.Concat(await ApplicationCommandsExtension.ParseParametersAsync(parameters, guildId)).ToList(); DiscordApplicationCommandLocalization subSubNameLocalizations = null; DiscordApplicationCommandLocalization subSubDescriptionLocalizations = null; List localizisedOptions = null; var commandTranslation = translator?.Single(c => c.Name == payload.Name); var subCommandTranslation = commandTranslation?.SubGroups?.Single(sc => sc.Name == commatt.Name); var subSubCommandTranslation = subCommandTranslation?.Commands.Single(sc => sc.Name == commatt.Name); if (subSubCommandTranslation != null && subSubCommandTranslation.Options != null) { - localizisedOptions = new(suboptions.Count); + localizisedOptions = new List(suboptions.Count); foreach (var option in suboptions) { - List choices = option.Choices != null ? new(option.Choices.Count) : null; + List choices = option.Choices != null ? new List(option.Choices.Count) : null; if (option.Choices != null) { foreach (var choice in option.Choices) { choices.Add(new DiscordApplicationCommandOptionChoice(choice.Name, choice.Value, subSubCommandTranslation.Options.Single(o => o.Name == option.Name).Choices.Single(c => c.Name == choice.Name).NameTranslations)); } } localizisedOptions.Add(new DiscordApplicationCommandOption(option.Name, option.Description, option.Type, option.Required, choices, option.Options, option.ChannelTypes, option.AutoComplete, option.MinimumValue, option.MaximumValue, subSubCommandTranslation.Options.Single(o => o.Name == option.Name).NameTranslations, subSubCommandTranslation.Options.Single(o => o.Name == option.Name).DescriptionTranslations )); } subSubNameLocalizations = subSubCommandTranslation.NameTranslations; subSubDescriptionLocalizations = subSubCommandTranslation.DescriptionTranslations; } var subsubpayload = new DiscordApplicationCommandOption(commatt.Name, commatt.Description, ApplicationCommandOptionType.SubCommand, null, null, localizisedOptions ?? suboptions, nameLocalizations: subSubNameLocalizations, descriptionLocalizations: subSubDescriptionLocalizations); options.Add(subsubpayload); commandmethods.Add(new KeyValuePair(commatt.Name, subsubmethod)); currentMethods.Add(new KeyValuePair(commatt.Name, subsubmethod)); } //Adds the group to the command and method lists var subpayload = new DiscordApplicationCommandOption(subGroupAttribute.Name, subGroupAttribute.Description, ApplicationCommandOptionType.SubCommandGroup, null, null, options, nameLocalizations: subNameLocalizations, descriptionLocalizations: subDescriptionLocalizations); command.SubCommands.Add(new GroupCommand { Name = subGroupAttribute.Name, Methods = currentMethods }); payload = new DiscordApplicationCommand(payload.Name, payload.Description, payload.Options?.Append(subpayload) ?? new[] { subpayload }, payload.DefaultPermission, nameLocalizations: payload.NameLocalizations, descriptionLocalizations: payload.DescriptionLocalizations); commandTypeSources.Add(new KeyValuePair(subclass, type)); //Accounts for lifespans for the sub group if (subclass.GetCustomAttribute() != null && subclass.GetCustomAttribute().Lifespan == ApplicationCommandModuleLifespan.Singleton) { singletonModules.Add(ApplicationCommandsExtension.CreateInstance(subclass, ApplicationCommandsExtension.Configuration?.ServiceProvider)); } } if (command.SubCommands.Any()) subGroupCommands.Add(command); commands.Add(payload); //Accounts for lifespans if (subclassinfo.GetCustomAttribute() != null && subclassinfo.GetCustomAttribute().Lifespan == ApplicationCommandModuleLifespan.Singleton) { singletonModules.Add(ApplicationCommandsExtension.CreateInstance(subclassinfo, ApplicationCommandsExtension.Configuration?.ServiceProvider)); } } return (commands, commandTypeSources, singletonModules, groupCommands, subGroupCommands); } } } diff --git a/DisCatSharp.CommandsNext/CommandsNextConfiguration.cs b/DisCatSharp.CommandsNext/CommandsNextConfiguration.cs index aab2f2755..cc5fae289 100644 --- a/DisCatSharp.CommandsNext/CommandsNextConfiguration.cs +++ b/DisCatSharp.CommandsNext/CommandsNextConfiguration.cs @@ -1,159 +1,159 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using DisCatSharp.CommandsNext.Attributes; using DisCatSharp.Entities; using Microsoft.Extensions.DependencyInjection; namespace DisCatSharp.CommandsNext { /// /// Represents a delegate for a function that takes a message, and returns the position of the start of command invocation in the message. It has to return -1 if prefix is not present. /// /// It is recommended that helper methods and /// be used internally for checking. Their output can be passed through. /// /// /// Message to check for prefix. /// Position of the command invocation or -1 if not present. public delegate Task PrefixResolverDelegate(DiscordMessage msg); /// /// Represents a configuration for . /// public sealed class CommandsNextConfiguration { /// /// Sets the string prefixes used for commands. /// Defaults to no value (disabled). /// public IEnumerable StringPrefixes { internal get; set; } /// /// Sets the custom prefix resolver used for commands. /// Defaults to none (disabled). /// - public PrefixResolverDelegate PrefixResolver { internal get; set; } = null; + public PrefixResolverDelegate PrefixResolver { internal get; set; } /// /// Sets whether to allow mentioning the bot to be used as command prefix. /// Defaults to true. /// public bool EnableMentionPrefix { internal get; set; } = true; /// /// Sets whether strings should be matched in a case-sensitive manner. /// This switch affects the behaviour of default prefix resolver, command searching, and argument conversion. /// Defaults to false. /// - public bool CaseSensitive { internal get; set; } = false; + public bool CaseSensitive { internal get; set; } /// /// Sets whether to enable default help command. /// Disabling this will allow you to make your own help command. /// /// Modifying default help can be achieved via custom help formatters (see and for more details). /// It is recommended to use help formatter instead of disabling help. /// /// Defaults to true. /// public bool EnableDefaultHelp { internal get; set; } = true; /// /// Controls whether the default help will be sent via DMs or not. /// Enabling this will make the bot respond with help via direct messages. /// Defaults to false. /// - public bool DmHelp { internal get; set; } = false; + public bool DmHelp { internal get; set; } /// /// Sets the default pre-execution checks for the built-in help command. /// Only applicable if default help is enabled. /// Defaults to null. /// - public IEnumerable DefaultHelpChecks { internal get; set; } = null; + public IEnumerable DefaultHelpChecks { internal get; set; } /// /// Sets whether commands sent via direct messages should be processed. /// Defaults to true. /// public bool EnableDms { internal get; set; } = true; /// /// Sets the service provider for this CommandsNext instance. /// Objects in this provider are used when instantiating command modules. This allows passing data around without resorting to static members. /// Defaults to null. /// public IServiceProvider ServiceProvider { internal get; set; } = new ServiceCollection().BuildServiceProvider(true); /// /// Gets whether any extra arguments passed to commands should be ignored or not. If this is set to false, extra arguments will throw, otherwise they will be ignored. /// Defaults to false. /// - public bool IgnoreExtraArguments { internal get; set; } = false; + public bool IgnoreExtraArguments { internal get; set; } /// /// Gets or sets whether to automatically enable handling commands. /// If this is set to false, you will need to manually handle each incoming message and pass it to CommandsNext. /// Defaults to true. /// public bool UseDefaultCommandHandler { internal get; set; } = true; /// /// Creates a new instance of . /// public CommandsNextConfiguration() { } /// /// Initializes a new instance of the class. /// /// The service provider. [ActivatorUtilitiesConstructor] public CommandsNextConfiguration(IServiceProvider provider) { this.ServiceProvider = provider; } /// /// Creates a new instance of , copying the properties of another configuration. /// /// Configuration the properties of which are to be copied. public CommandsNextConfiguration(CommandsNextConfiguration other) { this.CaseSensitive = other.CaseSensitive; this.PrefixResolver = other.PrefixResolver; this.DefaultHelpChecks = other.DefaultHelpChecks; this.EnableDefaultHelp = other.EnableDefaultHelp; this.EnableDms = other.EnableDms; this.EnableMentionPrefix = other.EnableMentionPrefix; this.IgnoreExtraArguments = other.IgnoreExtraArguments; this.UseDefaultCommandHandler = other.UseDefaultCommandHandler; this.ServiceProvider = other.ServiceProvider; this.StringPrefixes = other.StringPrefixes?.ToArray(); this.DmHelp = other.DmHelp; } } } diff --git a/DisCatSharp.CommandsNext/CommandsNextEvents.cs b/DisCatSharp.CommandsNext/CommandsNextEvents.cs index 880eabebc..8de2c0b4e 100644 --- a/DisCatSharp.CommandsNext/CommandsNextEvents.cs +++ b/DisCatSharp.CommandsNext/CommandsNextEvents.cs @@ -1,42 +1,42 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using Microsoft.Extensions.Logging; namespace DisCatSharp.CommandsNext { /// /// Contains well-defined event IDs used by CommandsNext. /// public static class CommandsNextEvents { /// /// Miscellaneous events, that do not fit in any other category. /// - internal static EventId Misc { get; } = new EventId(200, "CommandsNext"); + internal static EventId Misc { get; } = new(200, "CommandsNext"); /// /// Events pertaining to Gateway Intents. Typically diagnostic information. /// - internal static EventId Intents { get; } = new EventId(201, nameof(Intents)); + internal static EventId Intents { get; } = new(201, nameof(Intents)); } } diff --git a/DisCatSharp.CommandsNext/CommandsNextExtension.cs b/DisCatSharp.CommandsNext/CommandsNextExtension.cs index 47461404b..32eb0429e 100644 --- a/DisCatSharp.CommandsNext/CommandsNextExtension.cs +++ b/DisCatSharp.CommandsNext/CommandsNextExtension.cs @@ -1,1083 +1,1083 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Reflection; using System.Threading.Tasks; using DisCatSharp.CommandsNext.Attributes; using DisCatSharp.CommandsNext.Builders; using DisCatSharp.CommandsNext.Converters; using DisCatSharp.CommandsNext.Entities; using DisCatSharp.CommandsNext.Exceptions; using DisCatSharp.Common.Utilities; using DisCatSharp.Entities; using DisCatSharp.EventArgs; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace DisCatSharp.CommandsNext { /// /// This is the class which handles command registration, management, and execution. /// public class CommandsNextExtension : BaseExtension { /// /// Gets the config. /// private readonly CommandsNextConfiguration _config; /// /// Gets the help formatter. /// private readonly HelpFormatterFactory _helpFormatter; /// /// Gets the convert generic. /// private readonly MethodInfo _convertGeneric; /// /// Gets the user friendly type names. /// private readonly Dictionary _userFriendlyTypeNames; /// /// Gets the argument converters. /// internal Dictionary ArgumentConverters { get; } /// /// Gets the service provider this CommandsNext module was configured with. /// public IServiceProvider Services => this._config.ServiceProvider; /// /// Initializes a new instance of the class. /// /// The cfg. internal CommandsNextExtension(CommandsNextConfiguration cfg) { this._config = new CommandsNextConfiguration(cfg); this._topLevelCommands = new Dictionary(); this._registeredCommandsLazy = new Lazy>(() => new ReadOnlyDictionary(this._topLevelCommands)); this._helpFormatter = new HelpFormatterFactory(); this._helpFormatter.SetFormatterType(); this.ArgumentConverters = new Dictionary { [typeof(string)] = new StringConverter(), [typeof(bool)] = new BoolConverter(), [typeof(sbyte)] = new Int8Converter(), [typeof(byte)] = new Uint8Converter(), [typeof(short)] = new Int16Converter(), [typeof(ushort)] = new Uint16Converter(), [typeof(int)] = new Int32Converter(), [typeof(uint)] = new Uint32Converter(), [typeof(long)] = new Int64Converter(), [typeof(ulong)] = new Uint64Converter(), [typeof(float)] = new Float32Converter(), [typeof(double)] = new Float64Converter(), [typeof(decimal)] = new Float128Converter(), [typeof(DateTime)] = new DateTimeConverter(), [typeof(DateTimeOffset)] = new DateTimeOffsetConverter(), [typeof(TimeSpan)] = new TimeSpanConverter(), [typeof(Uri)] = new UriConverter(), [typeof(DiscordUser)] = new DiscordUserConverter(), [typeof(DiscordMember)] = new DiscordMemberConverter(), [typeof(DiscordRole)] = new DiscordRoleConverter(), [typeof(DiscordChannel)] = new DiscordChannelConverter(), [typeof(DiscordGuild)] = new DiscordGuildConverter(), [typeof(DiscordMessage)] = new DiscordMessageConverter(), [typeof(DiscordEmoji)] = new DiscordEmojiConverter(), [typeof(DiscordThreadChannel)] = new DiscordThreadChannelConverter(), [typeof(DiscordInvite)] = new DiscordInviteConverter(), [typeof(DiscordColor)] = new DiscordColorConverter(), [typeof(DiscordScheduledEvent)] = new DiscordScheduledEventConverter(), }; this._userFriendlyTypeNames = new Dictionary() { [typeof(string)] = "string", [typeof(bool)] = "boolean", [typeof(sbyte)] = "signed byte", [typeof(byte)] = "byte", [typeof(short)] = "short", [typeof(ushort)] = "unsigned short", [typeof(int)] = "int", [typeof(uint)] = "unsigned int", [typeof(long)] = "long", [typeof(ulong)] = "unsigned long", [typeof(float)] = "float", [typeof(double)] = "double", [typeof(decimal)] = "decimal", [typeof(DateTime)] = "date and time", [typeof(DateTimeOffset)] = "date and time", [typeof(TimeSpan)] = "time span", [typeof(Uri)] = "URL", [typeof(DiscordUser)] = "user", [typeof(DiscordMember)] = "member", [typeof(DiscordRole)] = "role", [typeof(DiscordChannel)] = "channel", [typeof(DiscordGuild)] = "guild", [typeof(DiscordMessage)] = "message", [typeof(DiscordEmoji)] = "emoji", [typeof(DiscordThreadChannel)] = "thread", [typeof(DiscordInvite)] = "invite", [typeof(DiscordColor)] = "color", [typeof(DiscordScheduledEvent)] = "event" }; var ncvt = typeof(NullableConverter<>); var nt = typeof(Nullable<>); var cvts = this.ArgumentConverters.Keys.ToArray(); foreach (var xt in cvts) { var xti = xt.GetTypeInfo(); if (!xti.IsValueType) continue; var xcvt = ncvt.MakeGenericType(xt); var xnt = nt.MakeGenericType(xt); if (this.ArgumentConverters.ContainsKey(xcvt)) continue; var xcv = Activator.CreateInstance(xcvt) as IArgumentConverter; this.ArgumentConverters[xnt] = xcv; this._userFriendlyTypeNames[xnt] = this._userFriendlyTypeNames[xt]; } var t = typeof(CommandsNextExtension); var ms = t.GetTypeInfo().DeclaredMethods; var m = ms.FirstOrDefault(xm => xm.Name == "ConvertArgument" && xm.ContainsGenericParameters && !xm.IsStatic && xm.IsPublic); this._convertGeneric = m; } /// /// Sets the help formatter to use with the default help command. /// /// Type of the formatter to use. public void SetHelpFormatter() where T : BaseHelpFormatter => this._helpFormatter.SetFormatterType(); #region DiscordClient Registration /// /// DO NOT USE THIS MANUALLY. /// /// DO NOT USE THIS MANUALLY. /// protected internal override void Setup(DiscordClient client) { if (this.Client != null) throw new InvalidOperationException("What did I tell you?"); this.Client = client; this._executed = new AsyncEvent("COMMAND_EXECUTED", TimeSpan.Zero, this.Client.EventErrorHandler); this._error = new AsyncEvent("COMMAND_ERRORED", TimeSpan.Zero, this.Client.EventErrorHandler); if (this._config.UseDefaultCommandHandler) this.Client.MessageCreated += this.HandleCommandsAsync; else this.Client.Logger.LogWarning(CommandsNextEvents.Misc, "Not attaching default command handler - if this is intentional, you can ignore this message"); if (this._config.EnableDefaultHelp) { this.RegisterCommands(typeof(DefaultHelpModule), null, null, out var tcmds); if (this._config.DefaultHelpChecks != null) { var checks = this._config.DefaultHelpChecks.ToArray(); for (var i = 0; i < tcmds.Count; i++) tcmds[i].WithExecutionChecks(checks); } if (tcmds != null) foreach (var xc in tcmds) this.AddToCommandDictionary(xc.Build(null)); } } #endregion #region Command Handling /// /// Handles the commands async. /// /// The sender. /// The e. /// A Task. private async Task HandleCommandsAsync(DiscordClient sender, MessageCreateEventArgs e) { if (e.Author.IsBot) // bad bot return; if (!this._config.EnableDms && e.Channel.IsPrivate) return; var mpos = -1; if (this._config.EnableMentionPrefix) mpos = e.Message.GetMentionPrefixLength(this.Client.CurrentUser); if (this._config.StringPrefixes?.Any() == true) foreach (var pfix in this._config.StringPrefixes) if (mpos == -1 && !string.IsNullOrWhiteSpace(pfix)) mpos = e.Message.GetStringPrefixLength(pfix, this._config.CaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase); if (mpos == -1 && this._config.PrefixResolver != null) mpos = await this._config.PrefixResolver(e.Message).ConfigureAwait(false); if (mpos == -1) return; var pfx = e.Message.Content[..mpos]; var cnt = e.Message.Content[mpos..]; var __ = 0; var fname = cnt.ExtractNextArgument(ref __); var cmd = this.FindCommand(cnt, out var args); var ctx = this.CreateContext(e.Message, pfx, cmd, args); if (cmd == null) { await this._error.InvokeAsync(this, new CommandErrorEventArgs(this.Client.ServiceProvider) { Context = ctx, Exception = new CommandNotFoundException(fname) }).ConfigureAwait(false); return; } _ = Task.Run(async () => await this.ExecuteCommandAsync(ctx).ConfigureAwait(false)); } /// /// Finds a specified command by its qualified name, then separates arguments. /// /// Qualified name of the command, optionally with arguments. /// Separated arguments. /// Found command or null if none was found. public Command FindCommand(string commandString, out string rawArguments) { rawArguments = null; var ignoreCase = !this._config.CaseSensitive; var pos = 0; var next = commandString.ExtractNextArgument(ref pos); if (next == null) return null; if (!this.RegisteredCommands.TryGetValue(next, out var cmd)) { if (!ignoreCase) return null; next = next.ToLowerInvariant(); var cmdKvp = this.RegisteredCommands.FirstOrDefault(x => x.Key.ToLowerInvariant() == next); if (cmdKvp.Value == null) return null; cmd = cmdKvp.Value; } if (cmd is not CommandGroup) { rawArguments = commandString[pos..].Trim(); return cmd; } while (cmd is CommandGroup) { var cm2 = cmd as CommandGroup; var oldPos = pos; next = commandString.ExtractNextArgument(ref pos); if (next == null) break; if (ignoreCase) { next = next.ToLowerInvariant(); cmd = cm2.Children.FirstOrDefault(x => x.Name.ToLowerInvariant() == next || x.Aliases?.Any(xx => xx.ToLowerInvariant() == next) == true); } else { cmd = cm2.Children.FirstOrDefault(x => x.Name == next || x.Aliases?.Contains(next) == true); } if (cmd == null) { cmd = cm2; pos = oldPos; break; } } rawArguments = commandString[pos..].Trim(); return cmd; } /// /// Creates a command execution context from specified arguments. /// /// Message to use for context. /// Command prefix, used to execute commands. /// Command to execute. /// Raw arguments to pass to command. /// Created command execution context. public CommandContext CreateContext(DiscordMessage msg, string prefix, Command cmd, string rawArguments = null) { var ctx = new CommandContext { Client = this.Client, Command = cmd, Message = msg, Config = this._config, RawArgumentString = rawArguments ?? "", Prefix = prefix, CommandsNext = this, Services = this.Services }; if (cmd != null && (cmd.Module is TransientCommandModule || cmd.Module == null)) { var scope = ctx.Services.CreateScope(); ctx.ServiceScopeContext = new CommandContext.ServiceContext(ctx.Services, scope); ctx.Services = scope.ServiceProvider; } return ctx; } /// /// Executes specified command from given context. /// /// Context to execute command from. /// public async Task ExecuteCommandAsync(CommandContext ctx) { try { var cmd = ctx.Command; await this.RunAllChecksAsync(cmd, ctx).ConfigureAwait(false); var res = await cmd.ExecuteAsync(ctx).ConfigureAwait(false); if (res.IsSuccessful) await this._executed.InvokeAsync(this, new CommandExecutionEventArgs(this.Client.ServiceProvider) { Context = res.Context }).ConfigureAwait(false); else await this._error.InvokeAsync(this, new CommandErrorEventArgs(this.Client.ServiceProvider) { Context = res.Context, Exception = res.Exception }).ConfigureAwait(false); } catch (Exception ex) { await this._error.InvokeAsync(this, new CommandErrorEventArgs(this.Client.ServiceProvider) { Context = ctx, Exception = ex }).ConfigureAwait(false); } finally { if (ctx.ServiceScopeContext.IsInitialized) ctx.ServiceScopeContext.Dispose(); } } /// /// Runs the all checks async. /// /// The cmd. /// The ctx. /// A Task. private async Task RunAllChecksAsync(Command cmd, CommandContext ctx) { if (cmd.Parent != null) await this.RunAllChecksAsync(cmd.Parent, ctx).ConfigureAwait(false); var fchecks = await cmd.RunChecksAsync(ctx, false).ConfigureAwait(false); if (fchecks.Any()) throw new ChecksFailedException(cmd, ctx, fchecks); } #endregion #region Command Registration /// /// Gets a dictionary of registered top-level commands. /// public IReadOnlyDictionary RegisteredCommands => this._registeredCommandsLazy.Value; /// /// Gets or sets the top level commands. /// private readonly Dictionary _topLevelCommands; private readonly Lazy> _registeredCommandsLazy; /// /// Registers all commands from a given assembly. The command classes need to be public to be considered for registration. /// /// Assembly to register commands from. public void RegisterCommands(Assembly assembly) { var types = assembly.ExportedTypes.Where(xt => { var xti = xt.GetTypeInfo(); return xti.IsModuleCandidateType() && !xti.IsNested; }); foreach (var xt in types) this.RegisterCommands(xt); } /// /// Registers all commands from a given command class. /// /// Class which holds commands to register. public void RegisterCommands() where T : BaseCommandModule { var t = typeof(T); this.RegisterCommands(t); } /// /// Registers all commands from a given command class. /// /// Type of the class which holds commands to register. public void RegisterCommands(Type t) { if (t == null) throw new ArgumentNullException(nameof(t), "Type cannot be null."); if (!t.IsModuleCandidateType()) throw new ArgumentNullException(nameof(t), "Type must be a class, which cannot be abstract or static."); this.RegisterCommands(t, null, null, out var tempCommands); if (tempCommands != null) foreach (var command in tempCommands) this.AddToCommandDictionary(command.Build(null)); } /// /// Registers the commands. /// /// The type. /// The current parent. /// The inherited checks. /// The found commands. private void RegisterCommands(Type t, CommandGroupBuilder currentParent, IEnumerable inheritedChecks, out List foundCommands) { var ti = t.GetTypeInfo(); var lifespan = ti.GetCustomAttribute(); var moduleLifespan = lifespan != null ? lifespan.Lifespan : ModuleLifespan.Singleton; var module = new CommandModuleBuilder() .WithType(t) .WithLifespan(moduleLifespan) .Build(this.Services); // restrict parent lifespan to more or equally restrictive if (currentParent?.Module is TransientCommandModule && moduleLifespan != ModuleLifespan.Transient) throw new InvalidOperationException("In a transient module, child modules can only be transient."); // check if we are anything var groupBuilder = new CommandGroupBuilder(module); var isModule = false; var moduleAttributes = ti.GetCustomAttributes(); var moduleHidden = false; var moduleChecks = new List(); foreach (var xa in moduleAttributes) { switch (xa) { case GroupAttribute g: isModule = true; var moduleName = g.Name; if (moduleName == null) { moduleName = ti.Name; if (moduleName.EndsWith("Group") && moduleName != "Group") moduleName = moduleName[0..^5]; else if (moduleName.EndsWith("Module") && moduleName != "Module") moduleName = moduleName[0..^6]; else if (moduleName.EndsWith("Commands") && moduleName != "Commands") moduleName = moduleName[0..^8]; } if (!this._config.CaseSensitive) moduleName = moduleName.ToLowerInvariant(); groupBuilder.WithName(moduleName); if (inheritedChecks != null) foreach (var chk in inheritedChecks) groupBuilder.WithExecutionCheck(chk); foreach (var mi in ti.DeclaredMethods.Where(x => x.IsCommandCandidate(out _) && x.GetCustomAttribute() != null)) groupBuilder.WithOverload(new CommandOverloadBuilder(mi)); break; case AliasesAttribute a: foreach (var xalias in a.Aliases) groupBuilder.WithAlias(this._config.CaseSensitive ? xalias : xalias.ToLowerInvariant()); break; case HiddenAttribute h: groupBuilder.WithHiddenStatus(true); moduleHidden = true; break; case DescriptionAttribute d: groupBuilder.WithDescription(d.Description); break; case CheckBaseAttribute c: moduleChecks.Add(c); groupBuilder.WithExecutionCheck(c); break; default: groupBuilder.WithCustomAttribute(xa); break; } } if (!isModule) { groupBuilder = null; if (inheritedChecks != null) moduleChecks.AddRange(inheritedChecks); } // candidate methods var methods = ti.DeclaredMethods; var commands = new List(); var commandBuilders = new Dictionary(); foreach (var m in methods) { if (!m.IsCommandCandidate(out _)) continue; var attrs = m.GetCustomAttributes(); if (attrs.FirstOrDefault(xa => xa is CommandAttribute) is not CommandAttribute cattr) continue; var commandName = cattr.Name; if (commandName == null) { commandName = m.Name; if (commandName.EndsWith("Async") && commandName != "Async") commandName = commandName[0..^5]; } if (!this._config.CaseSensitive) commandName = commandName.ToLowerInvariant(); if (!commandBuilders.TryGetValue(commandName, out var commandBuilder)) { commandBuilders.Add(commandName, commandBuilder = new CommandBuilder(module).WithName(commandName)); if (!isModule) if (currentParent != null) currentParent.WithChild(commandBuilder); else commands.Add(commandBuilder); else groupBuilder.WithChild(commandBuilder); } commandBuilder.WithOverload(new CommandOverloadBuilder(m)); if (!isModule && moduleChecks.Any()) foreach (var chk in moduleChecks) commandBuilder.WithExecutionCheck(chk); foreach (var xa in attrs) { switch (xa) { case AliasesAttribute a: foreach (var xalias in a.Aliases) commandBuilder.WithAlias(this._config.CaseSensitive ? xalias : xalias.ToLowerInvariant()); break; case CheckBaseAttribute p: commandBuilder.WithExecutionCheck(p); break; case DescriptionAttribute d: commandBuilder.WithDescription(d.Description); break; case HiddenAttribute h: commandBuilder.WithHiddenStatus(true); break; default: commandBuilder.WithCustomAttribute(xa); break; } } if (!isModule && moduleHidden) commandBuilder.WithHiddenStatus(true); } // candidate types var types = ti.DeclaredNestedTypes .Where(xt => xt.IsModuleCandidateType() && xt.DeclaredConstructors.Any(xc => xc.IsPublic)); foreach (var type in types) { this.RegisterCommands(type.AsType(), groupBuilder, !isModule ? moduleChecks : null, out var tempCommands); if (isModule) foreach (var chk in moduleChecks) groupBuilder.WithExecutionCheck(chk); if (isModule && tempCommands != null) foreach (var xtcmd in tempCommands) groupBuilder.WithChild(xtcmd); else if (tempCommands != null) commands.AddRange(tempCommands); } if (isModule && currentParent == null) commands.Add(groupBuilder); else if (isModule) currentParent.WithChild(groupBuilder); foundCommands = commands; } /// /// Builds and registers all supplied commands. /// /// Commands to build and register. public void RegisterCommands(params CommandBuilder[] cmds) { foreach (var cmd in cmds) this.AddToCommandDictionary(cmd.Build(null)); } /// /// Unregisters specified commands from CommandsNext. /// /// Commands to unregister. public void UnregisterCommands(params Command[] cmds) { if (cmds.Any(x => x.Parent != null)) throw new InvalidOperationException("Cannot unregister nested commands."); var keys = this.RegisteredCommands.Where(x => cmds.Contains(x.Value)).Select(x => x.Key).ToList(); foreach (var key in keys) this._topLevelCommands.Remove(key); } /// /// Adds the to command dictionary. /// /// The cmd. private void AddToCommandDictionary(Command cmd) { if (cmd.Parent != null) return; if (this._topLevelCommands.ContainsKey(cmd.Name) || (cmd.Aliases != null && cmd.Aliases.Any(xs => this._topLevelCommands.ContainsKey(xs)))) throw new DuplicateCommandException(cmd.QualifiedName); this._topLevelCommands[cmd.Name] = cmd; if (cmd.Aliases != null) foreach (var xs in cmd.Aliases) this._topLevelCommands[xs] = cmd; } #endregion #region Default Help /// /// Represents the default help module. /// [ModuleLifespan(ModuleLifespan.Transient)] public class DefaultHelpModule : BaseCommandModule { /// /// Defaults the help async. /// /// The ctx. /// The command. /// A Task. [Command("help"), Description("Displays command help.")] public async Task DefaultHelpAsync(CommandContext ctx, [Description("Command to provide help for.")] params string[] command) { var topLevel = ctx.CommandsNext._topLevelCommands.Values.Distinct(); var helpBuilder = ctx.CommandsNext._helpFormatter.Create(ctx); if (command != null && command.Any()) { Command cmd = null; var searchIn = topLevel; foreach (var c in command) { if (searchIn == null) { cmd = null; break; } cmd = ctx.Config.CaseSensitive ? searchIn.FirstOrDefault(xc => xc.Name == c || (xc.Aliases != null && xc.Aliases.Contains(c))) : searchIn.FirstOrDefault(xc => xc.Name.ToLowerInvariant() == c.ToLowerInvariant() || (xc.Aliases != null && xc.Aliases.Select(xs => xs.ToLowerInvariant()).Contains(c.ToLowerInvariant()))); if (cmd == null) break; var failedChecks = await cmd.RunChecksAsync(ctx, true).ConfigureAwait(false); if (failedChecks.Any()) throw new ChecksFailedException(cmd, ctx, failedChecks); searchIn = cmd is CommandGroup ? (cmd as CommandGroup).Children : null; } if (cmd == null) throw new CommandNotFoundException(string.Join(" ", command)); helpBuilder.WithCommand(cmd); if (cmd is CommandGroup group) { var commandsToSearch = group.Children.Where(xc => !xc.IsHidden); var eligibleCommands = new List(); foreach (var candidateCommand in commandsToSearch) { if (candidateCommand.ExecutionChecks == null || !candidateCommand.ExecutionChecks.Any()) { eligibleCommands.Add(candidateCommand); continue; } var candidateFailedChecks = await candidateCommand.RunChecksAsync(ctx, true).ConfigureAwait(false); if (!candidateFailedChecks.Any()) eligibleCommands.Add(candidateCommand); } if (eligibleCommands.Any()) helpBuilder.WithSubcommands(eligibleCommands.OrderBy(xc => xc.Name)); } } else { var commandsToSearch = topLevel.Where(xc => !xc.IsHidden); var eligibleCommands = new List(); foreach (var sc in commandsToSearch) { if (sc.ExecutionChecks == null || !sc.ExecutionChecks.Any()) { eligibleCommands.Add(sc); continue; } var candidateFailedChecks = await sc.RunChecksAsync(ctx, true).ConfigureAwait(false); if (!candidateFailedChecks.Any()) eligibleCommands.Add(sc); } if (eligibleCommands.Any()) helpBuilder.WithSubcommands(eligibleCommands.OrderBy(xc => xc.Name)); } var helpMessage = helpBuilder.Build(); var builder = new DiscordMessageBuilder().WithContent(helpMessage.Content).WithEmbed(helpMessage.Embed); if (!ctx.Config.DmHelp || ctx.Channel is DiscordDmChannel || ctx.Guild == null) await ctx.RespondAsync(builder).ConfigureAwait(false); else await ctx.Member.SendMessageAsync(builder).ConfigureAwait(false); } } #endregion #region Sudo /// /// Creates a fake command context to execute commands with. /// /// The user or member to use as message author. /// The channel the message is supposed to appear from. /// Contents of the message. /// Command prefix, used to execute commands. /// Command to execute. /// Raw arguments to pass to command. /// Created fake context. public CommandContext CreateFakeContext(DiscordUser actor, DiscordChannel channel, string messageContents, string prefix, Command cmd, string rawArguments = null) { var epoch = new DateTimeOffset(2015, 1, 1, 0, 0, 0, TimeSpan.Zero); var now = DateTimeOffset.UtcNow; var timeSpan = (ulong)(now - epoch).TotalMilliseconds; // create fake message var msg = new DiscordMessage { Discord = this.Client, Author = actor, ChannelId = channel.Id, Content = messageContents, Id = timeSpan << 22, Pinned = false, MentionEveryone = messageContents.Contains("@everyone"), IsTts = false, AttachmentsInternal = new List(), EmbedsInternal = new List(), TimestampRaw = now.ToString("yyyy-MM-ddTHH:mm:sszzz"), ReactionsInternal = new List() }; var mentionedUsers = new List(); var mentionedRoles = msg.Channel.Guild != null ? new List() : null; var mentionedChannels = msg.Channel.Guild != null ? new List() : null; if (!string.IsNullOrWhiteSpace(msg.Content)) { if (msg.Channel.Guild != null) { mentionedUsers = Utilities.GetUserMentions(msg).Select(xid => msg.Channel.Guild.MembersInternal.TryGetValue(xid, out var member) ? member : null).Cast().ToList(); mentionedRoles = Utilities.GetRoleMentions(msg).Select(xid => msg.Channel.Guild.GetRole(xid)).ToList(); mentionedChannels = Utilities.GetChannelMentions(msg).Select(xid => msg.Channel.Guild.GetChannel(xid)).ToList(); } else { mentionedUsers = Utilities.GetUserMentions(msg).Select(this.Client.GetCachedOrEmptyUserInternal).ToList(); } } msg.MentionedUsersInternal = mentionedUsers; msg.MentionedRolesInternal = mentionedRoles; msg.MentionedChannelsInternal = mentionedChannels; var ctx = new CommandContext { Client = this.Client, Command = cmd, Message = msg, Config = this._config, RawArgumentString = rawArguments ?? "", Prefix = prefix, CommandsNext = this, Services = this.Services }; if (cmd != null && (cmd.Module is TransientCommandModule || cmd.Module == null)) { var scope = ctx.Services.CreateScope(); ctx.ServiceScopeContext = new CommandContext.ServiceContext(ctx.Services, scope); ctx.Services = scope.ServiceProvider; } return ctx; } #endregion #region Type Conversion /// /// Converts a string to specified type. /// /// Type to convert to. /// Value to convert. /// Context in which to convert to. /// Converted object. public async Task ConvertArgument(string value, CommandContext ctx) { var t = typeof(T); if (!this.ArgumentConverters.ContainsKey(t)) throw new ArgumentException("There is no converter specified for given type.", nameof(T)); if (this.ArgumentConverters[t] is not IArgumentConverter cv) throw new ArgumentException("Invalid converter registered for this type.", nameof(T)); var cvr = await cv.ConvertAsync(value, ctx).ConfigureAwait(false); return !cvr.HasValue ? throw new ArgumentException("Could not convert specified value to given type.", nameof(value)) : cvr.Value; } /// /// Converts a string to specified type. /// /// Value to convert. /// Context in which to convert to. /// Type to convert to. /// Converted object. public async Task ConvertArgument(string value, CommandContext ctx, Type type) { var m = this._convertGeneric.MakeGenericMethod(type); try { return await (m.Invoke(this, new object[] { value, ctx }) as Task).ConfigureAwait(false); } catch (TargetInvocationException ex) { throw ex.InnerException; } } /// /// Registers an argument converter for specified type. /// /// Type for which to register the converter. /// Converter to register. public void RegisterConverter(IArgumentConverter converter) { if (converter == null) throw new ArgumentNullException(nameof(converter), "Converter cannot be null."); var t = typeof(T); var ti = t.GetTypeInfo(); this.ArgumentConverters[t] = converter; if (!ti.IsValueType) return; var nullableConverterType = typeof(NullableConverter<>).MakeGenericType(t); var nullableType = typeof(Nullable<>).MakeGenericType(t); if (this.ArgumentConverters.ContainsKey(nullableType)) return; var nullableConverter = Activator.CreateInstance(nullableConverterType) as IArgumentConverter; this.ArgumentConverters[nullableType] = nullableConverter; } /// /// Unregisters an argument converter for specified type. /// /// Type for which to unregister the converter. public void UnregisterConverter() { var t = typeof(T); var ti = t.GetTypeInfo(); if (this.ArgumentConverters.ContainsKey(t)) this.ArgumentConverters.Remove(t); if (this._userFriendlyTypeNames.ContainsKey(t)) this._userFriendlyTypeNames.Remove(t); if (!ti.IsValueType) return; var nullableType = typeof(Nullable<>).MakeGenericType(t); if (!this.ArgumentConverters.ContainsKey(nullableType)) return; this.ArgumentConverters.Remove(nullableType); this._userFriendlyTypeNames.Remove(nullableType); } /// /// Registers a user-friendly type name. /// /// Type to register the name for. /// Name to register. public void RegisterUserFriendlyTypeName(string value) { if (string.IsNullOrWhiteSpace(value)) throw new ArgumentNullException(nameof(value), "Name cannot be null or empty."); var t = typeof(T); var ti = t.GetTypeInfo(); if (!this.ArgumentConverters.ContainsKey(t)) throw new InvalidOperationException("Cannot register a friendly name for a type which has no associated converter."); this._userFriendlyTypeNames[t] = value; if (!ti.IsValueType) return; var nullableType = typeof(Nullable<>).MakeGenericType(t); this._userFriendlyTypeNames[nullableType] = value; } /// /// Converts a type into user-friendly type name. /// /// Type to convert. /// User-friendly type name. public string GetUserFriendlyTypeName(Type t) { if (this._userFriendlyTypeNames.ContainsKey(t)) return this._userFriendlyTypeNames[t]; var ti = t.GetTypeInfo(); if (ti.IsGenericTypeDefinition && t.GetGenericTypeDefinition() == typeof(Nullable<>)) { var tn = ti.GenericTypeArguments[0]; return this._userFriendlyTypeNames.ContainsKey(tn) ? this._userFriendlyTypeNames[tn] : tn.Name; } return t.Name; } #endregion #region Helpers /// /// Gets the configuration-specific string comparer. This returns or , /// depending on whether is set to or . /// /// A string comparer. internal IEqualityComparer GetStringComparer() => this._config.CaseSensitive ? StringComparer.Ordinal : StringComparer.OrdinalIgnoreCase; #endregion #region Events /// /// Triggered whenever a command executes successfully. /// public event AsyncEventHandler CommandExecuted { - add { this._executed.Register(value); } - remove { this._executed.Unregister(value); } + add => this._executed.Register(value); + remove => this._executed.Unregister(value); } private AsyncEvent _executed; /// /// Triggered whenever a command throws an exception during execution. /// public event AsyncEventHandler CommandErrored { - add { this._error.Register(value); } - remove { this._error.Unregister(value); } + add => this._error.Register(value); + remove => this._error.Unregister(value); } private AsyncEvent _error; /// /// Ons the command executed. /// /// The e. /// A Task. private Task OnCommandExecuted(CommandExecutionEventArgs e) => this._executed.InvokeAsync(this, e); /// /// Ons the command errored. /// /// The e. /// A Task. private Task OnCommandErrored(CommandErrorEventArgs e) => this._error.InvokeAsync(this, e); #endregion } } diff --git a/DisCatSharp.CommandsNext/Converters/NumericConverters.cs b/DisCatSharp.CommandsNext/Converters/NumericConverters.cs index edfcdab83..876b8f969 100644 --- a/DisCatSharp.CommandsNext/Converters/NumericConverters.cs +++ b/DisCatSharp.CommandsNext/Converters/NumericConverters.cs @@ -1,244 +1,220 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System.Globalization; using System.Threading.Tasks; using DisCatSharp.Entities; namespace DisCatSharp.CommandsNext.Converters { /// /// The bool converter. /// public class BoolConverter : IArgumentConverter { /// /// Converts a string. /// /// The string to convert. /// The command context. - Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) - { - return bool.TryParse(value, out var result) + Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) => + bool.TryParse(value, out var result) ? Task.FromResult(Optional.FromValue(result)) : Task.FromResult(Optional.FromNoValue()); - } } /// /// The int8 converter. /// public class Int8Converter : IArgumentConverter { /// /// Converts a string. /// /// The string to convert. /// The command context. - Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) - { - return sbyte.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result) + Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) => + sbyte.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result) ? Task.FromResult(Optional.FromValue(result)) : Task.FromResult(Optional.FromNoValue()); - } } /// /// The uint8 converter. /// public class Uint8Converter : IArgumentConverter { /// /// Converts a string. /// /// The string to convert. /// The command context. - Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) - { - return byte.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result) + Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) => + byte.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result) ? Task.FromResult(Optional.FromValue(result)) : Task.FromResult(Optional.FromNoValue()); - } } /// /// The int16 converter. /// public class Int16Converter : IArgumentConverter { /// /// Converts a string. /// /// The string to convert. /// The command context. - Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) - { - return short.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result) + Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) => + short.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result) ? Task.FromResult(Optional.FromValue(result)) : Task.FromResult(Optional.FromNoValue()); - } } /// /// The uint16 converter. /// public class Uint16Converter : IArgumentConverter { /// /// Converts a string. /// /// The string to convert. /// The command context. - Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) - { - return ushort.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result) + Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) => + ushort.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result) ? Task.FromResult(Optional.FromValue(result)) : Task.FromResult(Optional.FromNoValue()); - } } /// /// The int32 converter. /// public class Int32Converter : IArgumentConverter { /// /// Converts a string. /// /// The string to convert. /// The command context. - Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) - { - return int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result) + Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) => + int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result) ? Task.FromResult(Optional.FromValue(result)) : Task.FromResult(Optional.FromNoValue()); - } } /// /// The uint32 converter. /// public class Uint32Converter : IArgumentConverter { /// /// Converts a string. /// /// The string to convert. /// The command context. - Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) - { - return uint.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result) + Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) => + uint.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result) ? Task.FromResult(Optional.FromValue(result)) : Task.FromResult(Optional.FromNoValue()); - } } /// /// The int64 converter. /// public class Int64Converter : IArgumentConverter { /// /// Converts a string. /// /// The string to convert. /// The command context. - Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) - { - return long.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result) + Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) => + long.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result) ? Task.FromResult(Optional.FromValue(result)) : Task.FromResult(Optional.FromNoValue()); - } } /// /// The uint64 converter. /// public class Uint64Converter : IArgumentConverter { /// /// Converts a string. /// /// The string to convert. /// The command context. - Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) - { - return ulong.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result) + Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) => + ulong.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result) ? Task.FromResult(Optional.FromValue(result)) : Task.FromResult(Optional.FromNoValue()); - } } /// /// The float32 converter. /// public class Float32Converter : IArgumentConverter { /// /// Converts a string. /// /// The string to convert. /// The command context. - Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) - { - return float.TryParse(value, NumberStyles.Number, CultureInfo.InvariantCulture, out var result) + Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) => + float.TryParse(value, NumberStyles.Number, CultureInfo.InvariantCulture, out var result) ? Task.FromResult(Optional.FromValue(result)) : Task.FromResult(Optional.FromNoValue()); - } } /// /// The float64 converter. /// public class Float64Converter : IArgumentConverter { /// /// Converts a string. /// /// The string to convert. /// The command context. - Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) - { - return double.TryParse(value, NumberStyles.Number, CultureInfo.InvariantCulture, out var result) + Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) => + double.TryParse(value, NumberStyles.Number, CultureInfo.InvariantCulture, out var result) ? Task.FromResult(Optional.FromValue(result)) : Task.FromResult(Optional.FromNoValue()); - } } /// /// The float128 converter. /// public class Float128Converter : IArgumentConverter { /// /// Converts a string. /// /// The string to convert. /// The command context. - Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) - { - return decimal.TryParse(value, NumberStyles.Number, CultureInfo.InvariantCulture, out var result) + Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) => + decimal.TryParse(value, NumberStyles.Number, CultureInfo.InvariantCulture, out var result) ? Task.FromResult(Optional.FromValue(result)) : Task.FromResult(Optional.FromNoValue()); - } } } diff --git a/DisCatSharp.CommandsNext/Converters/TimeConverters.cs b/DisCatSharp.CommandsNext/Converters/TimeConverters.cs index 3d479471f..84f83d8ba 100644 --- a/DisCatSharp.CommandsNext/Converters/TimeConverters.cs +++ b/DisCatSharp.CommandsNext/Converters/TimeConverters.cs @@ -1,145 +1,141 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Globalization; using System.Text.RegularExpressions; using System.Threading.Tasks; using DisCatSharp.Common.RegularExpressions; using DisCatSharp.Entities; namespace DisCatSharp.CommandsNext.Converters { /// /// Represents a date time converter. /// public class DateTimeConverter : IArgumentConverter { /// /// Converts a string. /// /// The string to convert. /// The command context. - Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) - { - return DateTime.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.None, out var result) + Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) => + DateTime.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.None, out var result) ? Task.FromResult(new Optional(result)) : Task.FromResult(Optional.FromNoValue()); - } } /// /// Represents a date time offset converter. /// public class DateTimeOffsetConverter : IArgumentConverter { /// /// Converts a string. /// /// The string to convert. /// The command context. - Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) - { - return DateTimeOffset.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.None, out var result) + Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) => + DateTimeOffset.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.None, out var result) ? Task.FromResult(Optional.FromValue(result)) : Task.FromResult(Optional.FromNoValue()); - } } /// /// Represents a time span converter. /// public class TimeSpanConverter : IArgumentConverter { /// /// Gets or sets the time span regex. /// private static Regex s_timeSpanRegex { get; set; } /// /// Initializes a new instance of the class. /// static TimeSpanConverter() { s_timeSpanRegex = CommonRegEx.TimeSpan; } /// /// Converts a string. /// /// The string to convert. /// The command context. Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) { if (value == "0") return Task.FromResult(Optional.FromValue(TimeSpan.Zero)); if (int.TryParse(value, NumberStyles.Number, CultureInfo.InvariantCulture, out _)) return Task.FromResult(Optional.FromNoValue()); if (!ctx.Config.CaseSensitive) value = value.ToLowerInvariant(); if (TimeSpan.TryParse(value, CultureInfo.InvariantCulture, out var result)) return Task.FromResult(Optional.FromValue(result)); var gps = new string[] { "days", "hours", "minutes", "seconds" }; var mtc = s_timeSpanRegex.Match(value); if (!mtc.Success) return Task.FromResult(Optional.FromNoValue()); var d = 0; var h = 0; var m = 0; var s = 0; foreach (var gp in gps) { var gpc = mtc.Groups[gp].Value; if (string.IsNullOrWhiteSpace(gpc)) continue; var gpt = gpc[^1]; int.TryParse(gpc[0..^1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var val); switch (gpt) { case 'd': d = val; break; case 'h': h = val; break; case 'm': m = val; break; case 's': s = val; break; } } result = new TimeSpan(d, h, m, s); return Task.FromResult(Optional.FromValue(result)); } } } diff --git a/DisCatSharp.CommandsNext/Entities/Builders/CommandModuleBuilder.cs b/DisCatSharp.CommandsNext/Entities/Builders/CommandModuleBuilder.cs index 7da8deeba..359a3b3ef 100644 --- a/DisCatSharp.CommandsNext/Entities/Builders/CommandModuleBuilder.cs +++ b/DisCatSharp.CommandsNext/Entities/Builders/CommandModuleBuilder.cs @@ -1,89 +1,87 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using DisCatSharp.CommandsNext.Attributes; using DisCatSharp.CommandsNext.Entities; namespace DisCatSharp.CommandsNext.Builders { /// /// Represents an interface to build a command module. /// public sealed class CommandModuleBuilder { /// /// Gets the type this build will construct a module out of. /// public Type Type { get; private set; } /// /// Gets the lifespan for the built module. /// public ModuleLifespan Lifespan { get; private set; } /// /// Creates a new command module builder. /// public CommandModuleBuilder() { } /// /// Sets the type this builder will construct a module out of. /// /// Type to build a module out of. It has to derive from . /// This builder. public CommandModuleBuilder WithType(Type t) { if (!t.IsModuleCandidateType()) throw new ArgumentException("Specified type is not a valid module type.", nameof(t)); this.Type = t; return this; } /// /// Lifespan to give this module. /// /// Lifespan for this module. /// This builder. public CommandModuleBuilder WithLifespan(ModuleLifespan lifespan) { this.Lifespan = lifespan; return this; } /// /// Builds the command module. /// /// The services. - internal ICommandModule Build(IServiceProvider services) - { - return this.Lifespan switch + internal ICommandModule Build(IServiceProvider services) => + this.Lifespan switch { ModuleLifespan.Singleton => new SingletonCommandModule(this.Type, services), ModuleLifespan.Transient => new TransientCommandModule(this.Type), _ => throw new NotSupportedException("Module lifespans other than transient and singleton are not supported."), }; - } } } diff --git a/DisCatSharp.CommandsNext/Entities/Command.cs b/DisCatSharp.CommandsNext/Entities/Command.cs index 8c381acc2..990595bd2 100644 --- a/DisCatSharp.CommandsNext/Entities/Command.cs +++ b/DisCatSharp.CommandsNext/Entities/Command.cs @@ -1,234 +1,232 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using DisCatSharp.CommandsNext.Attributes; using DisCatSharp.CommandsNext.Entities; namespace DisCatSharp.CommandsNext { /// /// Represents a command. /// public class Command { /// /// Gets this command's name. /// public string Name { get; internal set; } /// /// Gets this command's qualified name (i.e. one that includes all module names). /// public string QualifiedName => this.Parent != null ? string.Concat(this.Parent.QualifiedName, " ", this.Name) : this.Name; /// /// Gets this command's aliases. /// public IReadOnlyList Aliases { get; internal set; } /// /// Gets this command's parent module, if any. /// public CommandGroup Parent { get; internal set; } /// /// Gets this command's description. /// public string Description { get; internal set; } /// /// Gets whether this command is hidden. /// public bool IsHidden { get; internal set; } /// /// Gets a collection of pre-execution checks for this command. /// public IReadOnlyList ExecutionChecks { get; internal set; } /// /// Gets a collection of this command's overloads. /// public IReadOnlyList Overloads { get; internal set; } /// /// Gets the module in which this command is defined. /// public ICommandModule Module { get; internal set; } /// /// Gets the custom attributes defined on this command. /// public IReadOnlyList CustomAttributes { get; internal set; } /// /// Initializes a new instance of the class. /// internal Command() { } /// /// Executes this command with specified context. /// /// Context to execute the command in. /// Command's execution results. public virtual async Task ExecuteAsync(CommandContext ctx) { CommandResult res = default; try { var executed = false; foreach (var ovl in this.Overloads.OrderByDescending(x => x.Priority)) { ctx.Overload = ovl; var args = await CommandsNextUtilities.BindArguments(ctx, ctx.Config.IgnoreExtraArguments).ConfigureAwait(false); if (!args.IsSuccessful) continue; ctx.RawArguments = args.Raw; var mdl = ovl.InvocationTarget ?? this.Module?.GetInstance(ctx.Services); if (mdl is BaseCommandModule bcmBefore) await bcmBefore.BeforeExecutionAsync(ctx).ConfigureAwait(false); args.Converted[0] = mdl; var ret = (Task)ovl.Callable.DynamicInvoke(args.Converted); await ret.ConfigureAwait(false); executed = true; res = new CommandResult { IsSuccessful = true, Context = ctx }; if (mdl is BaseCommandModule bcmAfter) await bcmAfter.AfterExecutionAsync(ctx).ConfigureAwait(false); break; } if (!executed) throw new ArgumentException("Could not find a suitable overload for the command."); } catch (Exception ex) { res = new CommandResult { IsSuccessful = false, Exception = ex, Context = ctx }; } return res; } /// /// Runs pre-execution checks for this command and returns any that fail for given context. /// /// Context in which the command is executed. /// Whether this check is being executed from help or not. This can be used to probe whether command can be run without setting off certain fail conditions (such as cooldowns). /// Pre-execution checks that fail for given context. public async Task> RunChecksAsync(CommandContext ctx, bool help) { var fchecks = new List(); if (this.ExecutionChecks != null && this.ExecutionChecks.Any()) foreach (var ec in this.ExecutionChecks) if (!await ec.ExecuteCheckAsync(ctx, help).ConfigureAwait(false)) fchecks.Add(ec); return fchecks; } /// /// Checks whether this command is equal to another one. /// /// Command to compare to. /// Command to compare. /// Whether the two commands are equal. public static bool operator ==(Command cmd1, Command cmd2) { var o1 = cmd1 as object; var o2 = cmd2 as object; if (o1 == null && o2 != null) return false; else if (o1 != null && o2 == null) return false; else if (o1 == null && o2 == null) return true; return cmd1.QualifiedName == cmd2.QualifiedName; } /// /// Checks whether this command is not equal to another one. /// /// Command to compare to. /// Command to compare. /// Whether the two commands are not equal. public static bool operator !=(Command cmd1, Command cmd2) => !(cmd1 == cmd2); /// /// Checks whether this command equals another object. /// /// Object to compare to. /// Whether this command is equal to another object. public override bool Equals(object obj) { var o1 = obj as object; var o2 = this as object; if (o1 == null && o2 != null) return false; else if (o1 != null && o2 == null) return false; else if (o1 == null && o2 == null) return true; return obj is Command cmd && cmd.QualifiedName == this.QualifiedName; } /// /// Gets this command's hash code. /// /// This command's hash code. public override int GetHashCode() => this.QualifiedName.GetHashCode(); /// /// Returns a string representation of this command. /// /// String representation of this command. - public override string ToString() - { - return this is CommandGroup g + public override string ToString() => + this is CommandGroup g ? $"Command Group: {this.QualifiedName}, {g.Children.Count} top-level children" : $"Command: {this.QualifiedName}"; - } } } diff --git a/DisCatSharp.CommandsNext/Exceptions/InvalidOverloadException.cs b/DisCatSharp.CommandsNext/Exceptions/InvalidOverloadException.cs index dcefab6b6..cad9f92c5 100644 --- a/DisCatSharp.CommandsNext/Exceptions/InvalidOverloadException.cs +++ b/DisCatSharp.CommandsNext/Exceptions/InvalidOverloadException.cs @@ -1,77 +1,75 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Reflection; namespace DisCatSharp.CommandsNext.Exceptions { /// /// Thrown when the command service fails to build a command due to a problem with its overload. /// public sealed class InvalidOverloadException : Exception { /// /// Gets the method that caused this exception. /// public MethodInfo Method { get; } /// /// Gets or sets the argument that caused the problem. This can be null. /// public ParameterInfo Parameter { get; } /// /// Creates a new . /// /// Exception message. /// Method that caused the problem. /// Method argument that caused the problem. public InvalidOverloadException(string message, MethodInfo method, ParameterInfo parameter) : base(message) { this.Method = method; this.Parameter = parameter; } /// /// Creates a new . /// /// Exception message. /// Method that caused the problem. public InvalidOverloadException(string message, MethodInfo method) : this(message, method, null) { } /// /// Returns a string representation of this . /// /// A string representation. - public override string ToString() - { + public override string ToString() => // much like System.ArgumentNullException works - return this.Parameter == null + this.Parameter == null ? $"{this.GetType()}: {this.Message}\nMethod: {this.Method} (declared in {this.Method.DeclaringType})" : $"{this.GetType()}: {this.Message}\nMethod: {this.Method} (declared in {this.Method.DeclaringType})\nArgument: {this.Parameter.ParameterType} {this.Parameter.Name}"; - } } } diff --git a/DisCatSharp.Common/Types/CharSpanLookupDictionary.cs b/DisCatSharp.Common/Types/CharSpanLookupDictionary.cs index 206be380f..ff204e923 100644 --- a/DisCatSharp.Common/Types/CharSpanLookupDictionary.cs +++ b/DisCatSharp.Common/Types/CharSpanLookupDictionary.cs @@ -1,832 +1,829 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections; using System.Collections.Generic; using System.Collections.Immutable; namespace DisCatSharp.Common { /// /// Represents collection of string keys and values, allowing the use of for dictionary operations. /// /// Type of items in this dictionary. public sealed class CharSpanLookupDictionary : IDictionary, IReadOnlyDictionary, IDictionary { /// /// Gets the collection of all keys present in this dictionary. /// public IEnumerable Keys => this.GetKeysInternal(); /// /// Gets the keys. /// ICollection IDictionary.Keys => this.GetKeysInternal(); /// /// Gets the keys. /// ICollection IDictionary.Keys => this.GetKeysInternal(); /// /// Gets the collection of all values present in this dictionary. /// public IEnumerable Values => this.GetValuesInternal(); /// /// Gets the values. /// ICollection IDictionary.Values => this.GetValuesInternal(); /// /// Gets the values. /// ICollection IDictionary.Values => this.GetValuesInternal(); /// /// Gets the total number of items in this dictionary. /// - public int Count { get; private set; } = 0; + public int Count { get; private set; } /// /// Gets whether this dictionary is read-only. /// public bool IsReadOnly => false; /// /// Gets whether this dictionary has a fixed size. /// public bool IsFixedSize => false; /// /// Gets whether this dictionary is considered thread-safe. /// public bool IsSynchronized => false; /// /// Gets the object which allows synchronizing access to this dictionary. /// - public object SyncRoot { get; } = new object(); + public object SyncRoot { get; } = new(); /// /// Gets or sets a value corresponding to given key in this dictionary. /// /// Key to get or set the value for. /// Value matching the supplied key, if applicable. public TValue this[string key] { get { if (key == null) throw new ArgumentNullException(nameof(key)); if (!this.TryRetrieveInternal(key.AsSpan(), out var value)) throw new KeyNotFoundException($"The given key '{key}' was not present in the dictionary."); return value; } set { if (key == null) throw new ArgumentNullException(nameof(key)); this.TryInsertInternal(key, value, true); } } /// /// Gets or sets a value corresponding to given key in this dictionary. /// /// Key to get or set the value for. /// Value matching the supplied key, if applicable. public TValue this[ReadOnlySpan key] { get { if (!this.TryRetrieveInternal(key, out var value)) throw new KeyNotFoundException($"The given key was not present in the dictionary."); return value; } #if NETCOREAPP set => this.TryInsertInternal(new string(key), value, true); #else set { unsafe { fixed (char* chars = &key.GetPinnableReference()) this.TryInsertInternal(new string(chars, 0, key.Length), value, true); } } #endif } object IDictionary.this[object key] { get { if (!(key is string tkey)) throw new ArgumentException("Key needs to be an instance of a string."); if (!this.TryRetrieveInternal(tkey.AsSpan(), out var value)) throw new KeyNotFoundException($"The given key '{tkey}' was not present in the dictionary."); return value; } set { if (!(key is string tkey)) throw new ArgumentException("Key needs to be an instance of a string."); if (!(value is TValue tvalue)) { tvalue = default; if (tvalue != null) throw new ArgumentException($"Value needs to be an instance of {typeof(TValue)}."); } this.TryInsertInternal(tkey, tvalue, true); } } /// /// Gets the internal buckets. /// private readonly Dictionary _internalBuckets; /// /// Creates a new, empty with string keys and items of type . /// public CharSpanLookupDictionary() { this._internalBuckets = new Dictionary(); } /// /// Creates a new, empty with string keys and items of type and sets its initial capacity to specified value. /// /// Initial capacity of the dictionary. public CharSpanLookupDictionary(int initialCapacity) { this._internalBuckets = new Dictionary(initialCapacity); } /// /// Creates a new with string keys and items of type and populates it with key-value pairs from supplied dictionary. /// /// Dictionary containing items to populate this dictionary with. public CharSpanLookupDictionary(IDictionary values) : this(values.Count) { foreach (var (k, v) in values) this.Add(k, v); } /// /// Creates a new with string keys and items of type and populates it with key-value pairs from supplied dictionary. /// /// Dictionary containing items to populate this dictionary with. public CharSpanLookupDictionary(IReadOnlyDictionary values) : this(values.Count) { foreach (var (k, v) in values) this.Add(k, v); } /// /// Creates a new with string keys and items of type and populates it with key-value pairs from supplied key-value collection. /// /// Dictionary containing items to populate this dictionary with. public CharSpanLookupDictionary(IEnumerable> values) : this() { foreach (var (k, v) in values) this.Add(k, v); } /// /// Inserts a specific key and corresponding value into this dictionary. /// /// Key to insert. /// Value corresponding to this key. public void Add(string key, TValue value) { if (!this.TryInsertInternal(key, value, false)) throw new ArgumentException("Given key is already present in the dictionary.", nameof(key)); } /// /// Inserts a specific key and corresponding value into this dictionary. /// /// Key to insert. /// Value corresponding to this key. public void Add(ReadOnlySpan key, TValue value) #if NETCOREAPP { if (!this.TryInsertInternal(new string(key), value, false)) throw new ArgumentException("Given key is already present in the dictionary.", nameof(key)); } #else { unsafe { fixed (char* chars = &key.GetPinnableReference()) if (!this.TryInsertInternal(new string(chars, 0, key.Length), value, false)) throw new ArgumentException("Given key is already present in the dictionary.", nameof(key)); } } #endif /// /// Attempts to insert a specific key and corresponding value into this dictionary. /// /// Key to insert. /// Value corresponding to this key. /// Whether the operation was successful. public bool TryAdd(string key, TValue value) => this.TryInsertInternal(key, value, false); /// /// Attempts to insert a specific key and corresponding value into this dictionary. /// /// Key to insert. /// Value corresponding to this key. /// Whether the operation was successful. public bool TryAdd(ReadOnlySpan key, TValue value) #if NETCOREAPP => this.TryInsertInternal(new string(key), value, false); #else { unsafe { fixed (char* chars = &key.GetPinnableReference()) return this.TryInsertInternal(new string(chars, 0, key.Length), value, false); } } #endif /// /// Attempts to retrieve a value corresponding to the supplied key from this dictionary. /// /// Key to retrieve the value for. /// Retrieved value. /// Whether the operation was successful. public bool TryGetValue(string key, out TValue value) { if (key == null) throw new ArgumentNullException(nameof(key)); return this.TryRetrieveInternal(key.AsSpan(), out value); } /// /// Attempts to retrieve a value corresponding to the supplied key from this dictionary. /// /// Key to retrieve the value for. /// Retrieved value. /// Whether the operation was successful. public bool TryGetValue(ReadOnlySpan key, out TValue value) => this.TryRetrieveInternal(key, out value); /// /// Attempts to remove a value corresponding to the supplied key from this dictionary. /// /// Key to remove the value for. /// Removed value. /// Whether the operation was successful. public bool TryRemove(string key, out TValue value) { if (key == null) throw new ArgumentNullException(nameof(key)); return this.TryRemoveInternal(key.AsSpan(), out value); } /// /// Attempts to remove a value corresponding to the supplied key from this dictionary. /// /// Key to remove the value for. /// Removed value. /// Whether the operation was successful. public bool TryRemove(ReadOnlySpan key, out TValue value) => this.TryRemoveInternal(key, out value); /// /// Checks whether this dictionary contains the specified key. /// /// Key to check for in this dictionary. /// Whether the key was present in the dictionary. public bool ContainsKey(string key) => this.ContainsKeyInternal(key.AsSpan()); /// /// Checks whether this dictionary contains the specified key. /// /// Key to check for in this dictionary. /// Whether the key was present in the dictionary. public bool ContainsKey(ReadOnlySpan key) => this.ContainsKeyInternal(key); /// /// Removes all items from this dictionary. /// public void Clear() { this._internalBuckets.Clear(); this.Count = 0; } /// /// Gets an enumerator over key-value pairs in this dictionary. /// /// public IEnumerator> GetEnumerator() => new Enumerator(this); /// /// Removes the. /// /// The key. /// A bool. bool IDictionary.Remove(string key) => this.TryRemove(key.AsSpan(), out _); /// /// Adds the. /// /// The key. /// The value. void IDictionary.Add(object key, object value) { if (!(key is string tkey)) throw new ArgumentException("Key needs to be an instance of a string."); if (!(value is TValue tvalue)) { tvalue = default; if (tvalue != null) throw new ArgumentException($"Value needs to be an instance of {typeof(TValue)}."); } this.Add(tkey, tvalue); } /// /// Removes the. /// /// The key. void IDictionary.Remove(object key) { if (!(key is string tkey)) throw new ArgumentException("Key needs to be an instance of a string."); this.TryRemove(tkey, out _); } /// /// Contains the. /// /// The key. /// A bool. bool IDictionary.Contains(object key) { if (!(key is string tkey)) throw new ArgumentException("Key needs to be an instance of a string."); return this.ContainsKey(tkey); } /// /// Gets the enumerator. /// /// An IDictionaryEnumerator. IDictionaryEnumerator IDictionary.GetEnumerator() => new Enumerator(this); /// /// Adds the. /// /// The item. void ICollection>.Add(KeyValuePair item) => this.Add(item.Key, item.Value); /// /// Removes the. /// /// The item. /// A bool. bool ICollection>.Remove(KeyValuePair item) => this.TryRemove(item.Key, out _); /// /// Contains the. /// /// The item. /// A bool. bool ICollection>.Contains(KeyValuePair item) => this.TryGetValue(item.Key, out var value) && EqualityComparer.Default.Equals(value, item.Value); /// /// Copies the to. /// /// The array. /// The array index. void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) { if (array.Length - arrayIndex < this.Count) throw new ArgumentException("Target array is too small.", nameof(array)); var i = arrayIndex; foreach (var (k, v) in this._internalBuckets) { var kdv = v; while (kdv != null) { array[i++] = new KeyValuePair(kdv.Key, kdv.Value); kdv = kdv.Next; } } } /// /// Copies the to. /// /// The array. /// The array index. void ICollection.CopyTo(Array array, int arrayIndex) { if (array is KeyValuePair[] tarray) { (this as ICollection>).CopyTo(tarray, arrayIndex); return; } if (array is not object[]) throw new ArgumentException($"Array needs to be an instance of {typeof(TValue[])} or object[]."); var i = arrayIndex; foreach (var (k, v) in this._internalBuckets) { var kdv = v; while (kdv != null) { array.SetValue(new KeyValuePair(kdv.Key, kdv.Value), i++); kdv = kdv.Next; } } } /// /// Gets the enumerator. /// /// An IEnumerator. IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); /// /// Tries the insert internal. /// /// The key. /// The value. /// If true, replace. /// A bool. private bool TryInsertInternal(string key, TValue value, bool replace) { if (key == null) throw new ArgumentNullException(nameof(key), "Key cannot be null."); var hash = key.CalculateKnuthHash(); if (!this._internalBuckets.ContainsKey(hash)) { this._internalBuckets.Add(hash, new KeyedValue(key, hash, value)); this.Count++; return true; } var kdv = this._internalBuckets[hash]; var kdvLast = kdv; while (kdv != null) { if (kdv.Key == key) { if (!replace) return false; kdv.Value = value; return true; } kdvLast = kdv; kdv = kdv.Next; } kdvLast.Next = new KeyedValue(key, hash, value); this.Count++; return true; } /// /// Tries the retrieve internal. /// /// The key. /// The value. /// A bool. private bool TryRetrieveInternal(ReadOnlySpan key, out TValue value) { value = default; var hash = key.CalculateKnuthHash(); if (!this._internalBuckets.TryGetValue(hash, out var kdv)) return false; while (kdv != null) { if (key.SequenceEqual(kdv.Key.AsSpan())) { value = kdv.Value; return true; } } return false; } /// /// Tries the remove internal. /// /// The key. /// The value. /// A bool. private bool TryRemoveInternal(ReadOnlySpan key, out TValue value) { value = default; var hash = key.CalculateKnuthHash(); if (!this._internalBuckets.TryGetValue(hash, out var kdv)) return false; if (kdv.Next == null && key.SequenceEqual(kdv.Key.AsSpan())) { // Only bucket under this hash and key matches, pop the entire bucket value = kdv.Value; this._internalBuckets.Remove(hash); this.Count--; return true; } else if (kdv.Next == null) { // Only bucket under this hash and key does not match, cannot remove return false; } else if (key.SequenceEqual(kdv.Key.AsSpan())) { // First key in the bucket matches, pop it and set its child as current bucket value = kdv.Value; this._internalBuckets[hash] = kdv.Next; this.Count--; return true; } var kdvLast = kdv; kdv = kdv.Next; while (kdv != null) { if (key.SequenceEqual(kdv.Key.AsSpan())) { // Key matched, remove this bucket from the chain value = kdv.Value; kdvLast.Next = kdv.Next; this.Count--; return true; } kdvLast = kdv; kdv = kdv.Next; } return false; } /// /// Contains the key internal. /// /// The key. /// A bool. private bool ContainsKeyInternal(ReadOnlySpan key) { var hash = key.CalculateKnuthHash(); if (!this._internalBuckets.TryGetValue(hash, out var kdv)) return false; while (kdv != null) { if (key.SequenceEqual(kdv.Key.AsSpan())) return true; kdv = kdv.Next; } return false; } /// /// Gets the keys internal. /// /// An ImmutableArray. private ImmutableArray GetKeysInternal() { var builder = ImmutableArray.CreateBuilder(this.Count); foreach (var value in this._internalBuckets.Values) { var kdv = value; while (kdv != null) { builder.Add(kdv.Key); kdv = kdv.Next; } } return builder.MoveToImmutable(); } /// /// Gets the values internal. /// /// An ImmutableArray. private ImmutableArray GetValuesInternal() { var builder = ImmutableArray.CreateBuilder(this.Count); foreach (var value in this._internalBuckets.Values) { var kdv = value; while (kdv != null) { builder.Add(kdv.Value); kdv = kdv.Next; } } return builder.MoveToImmutable(); } /// /// The keyed value. /// private class KeyedValue { /// /// Gets the key hash. /// public ulong KeyHash { get; } /// /// Gets the key. /// public string Key { get; } /// /// Gets or sets the value. /// public TValue Value { get; set; } /// /// Gets or sets the next. /// public KeyedValue Next { get; set; } /// /// Initializes a new instance of the class. /// /// The key. /// The key hash. /// The value. public KeyedValue(string key, ulong keyHash, TValue value) { this.KeyHash = keyHash; this.Key = key; this.Value = value; } } /// /// The enumerator. /// private class Enumerator : IEnumerator>, IDictionaryEnumerator { /// /// Gets the current. /// public KeyValuePair Current { get; private set; } /// /// Gets the current. /// object IEnumerator.Current => this.Current; /// /// Gets the key. /// object IDictionaryEnumerator.Key => this.Current.Key; /// /// Gets the value. /// object IDictionaryEnumerator.Value => this.Current.Value; /// /// Gets the entry. /// - DictionaryEntry IDictionaryEnumerator.Entry => new DictionaryEntry(this.Current.Key, this.Current.Value); + DictionaryEntry IDictionaryEnumerator.Entry => new(this.Current.Key, this.Current.Value); /// /// Gets the internal dictionary. /// private readonly CharSpanLookupDictionary _internalDictionary; /// /// Gets the internal enumerator. /// private readonly IEnumerator> _internalEnumerator; /// /// Gets or sets the current value. /// private KeyedValue _currentValue; /// /// Initializes a new instance of the class. /// /// The sp dict. public Enumerator(CharSpanLookupDictionary spDict) { this._internalDictionary = spDict; this._internalEnumerator = this._internalDictionary._internalBuckets.GetEnumerator(); } /// /// Moves the next. /// /// A bool. public bool MoveNext() { var kdv = this._currentValue; if (kdv == null) { if (!this._internalEnumerator.MoveNext()) return false; kdv = this._internalEnumerator.Current.Value; this.Current = new KeyValuePair(kdv.Key, kdv.Value); this._currentValue = kdv.Next; return true; } this.Current = new KeyValuePair(kdv.Key, kdv.Value); this._currentValue = kdv.Next; return true; } /// /// Resets the. /// public void Reset() { this._internalEnumerator.Reset(); this.Current = default; this._currentValue = null; } /// /// Disposes the. /// - public void Dispose() - { - this.Reset(); - } + public void Dispose() => this.Reset(); } } } diff --git a/DisCatSharp.Common/Types/CharSpanLookupReadOnlyDictionary.cs b/DisCatSharp.Common/Types/CharSpanLookupReadOnlyDictionary.cs index b65618d41..50dc9b79d 100644 --- a/DisCatSharp.Common/Types/CharSpanLookupReadOnlyDictionary.cs +++ b/DisCatSharp.Common/Types/CharSpanLookupReadOnlyDictionary.cs @@ -1,419 +1,416 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections; using System.Collections.Generic; using System.Collections.Immutable; using System.Collections.ObjectModel; namespace DisCatSharp.Common { /// /// Represents collection of string keys and values, allowing the use of for dictionary operations. /// /// Type of items in this dictionary. public sealed class CharSpanLookupReadOnlyDictionary : IReadOnlyDictionary { /// /// Gets the collection of all keys present in this dictionary. /// public IEnumerable Keys => this.GetKeysInternal(); /// /// Gets the collection of all values present in this dictionary. /// public IEnumerable Values => this.GetValuesInternal(); /// /// Gets the total number of items in this dictionary. /// public int Count { get; } /// /// Gets a value corresponding to given key in this dictionary. /// /// Key to get or set the value for. /// Value matching the supplied key, if applicable. public TValue this[string key] { get { if (key == null) throw new ArgumentNullException(nameof(key)); if (!this.TryRetrieveInternal(key.AsSpan(), out var value)) throw new KeyNotFoundException($"The given key '{key}' was not present in the dictionary."); return value; } } /// /// Gets a value corresponding to given key in this dictionary. /// /// Key to get or set the value for. /// Value matching the supplied key, if applicable. public TValue this[ReadOnlySpan key] { get { if (!this.TryRetrieveInternal(key, out var value)) throw new KeyNotFoundException($"The given key was not present in the dictionary."); return value; } } /// /// Gets the internal buckets. /// private readonly IReadOnlyDictionary _internalBuckets; /// /// Creates a new with string keys and items of type and populates it with key-value pairs from supplied dictionary. /// /// Dictionary containing items to populate this dictionary with. public CharSpanLookupReadOnlyDictionary(IDictionary values) : this(values as IEnumerable>) { } /// /// Creates a new with string keys and items of type and populates it with key-value pairs from supplied dictionary. /// /// Dictionary containing items to populate this dictionary with. public CharSpanLookupReadOnlyDictionary(IReadOnlyDictionary values) : this(values as IEnumerable>) { } /// /// Creates a new with string keys and items of type and populates it with key-value pairs from supplied key-value collection. /// /// Dictionary containing items to populate this dictionary with. public CharSpanLookupReadOnlyDictionary(IEnumerable> values) { this._internalBuckets = PrepareItems(values, out var count); this.Count = count; } /// /// Attempts to retrieve a value corresponding to the supplied key from this dictionary. /// /// Key to retrieve the value for. /// Retrieved value. /// Whether the operation was successful. public bool TryGetValue(string key, out TValue value) { if (key == null) throw new ArgumentNullException(nameof(key)); return this.TryRetrieveInternal(key.AsSpan(), out value); } /// /// Attempts to retrieve a value corresponding to the supplied key from this dictionary. /// /// Key to retrieve the value for. /// Retrieved value. /// Whether the operation was successful. public bool TryGetValue(ReadOnlySpan key, out TValue value) => this.TryRetrieveInternal(key, out value); /// /// Checks whether this dictionary contains the specified key. /// /// Key to check for in this dictionary. /// Whether the key was present in the dictionary. public bool ContainsKey(string key) => this.ContainsKeyInternal(key.AsSpan()); /// /// Checks whether this dictionary contains the specified key. /// /// Key to check for in this dictionary. /// Whether the key was present in the dictionary. public bool ContainsKey(ReadOnlySpan key) => this.ContainsKeyInternal(key); /// /// Gets an enumerator over key-value pairs in this dictionary. /// /// public IEnumerator> GetEnumerator() => new Enumerator(this); /// /// Gets the enumerator. /// /// An IEnumerator. IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); /// /// Tries the retrieve internal. /// /// The key. /// The value. /// A bool. private bool TryRetrieveInternal(ReadOnlySpan key, out TValue value) { value = default; var hash = key.CalculateKnuthHash(); if (!this._internalBuckets.TryGetValue(hash, out var kdv)) return false; while (kdv != null) { if (key.SequenceEqual(kdv.Key.AsSpan())) { value = kdv.Value; return true; } } return false; } /// /// Contains the key internal. /// /// The key. /// A bool. private bool ContainsKeyInternal(ReadOnlySpan key) { var hash = key.CalculateKnuthHash(); if (!this._internalBuckets.TryGetValue(hash, out var kdv)) return false; while (kdv != null) { if (key.SequenceEqual(kdv.Key.AsSpan())) return true; kdv = kdv.Next; } return false; } /// /// Gets the keys internal. /// /// An ImmutableArray. private ImmutableArray GetKeysInternal() { var builder = ImmutableArray.CreateBuilder(this.Count); foreach (var value in this._internalBuckets.Values) { var kdv = value; while (kdv != null) { builder.Add(kdv.Key); kdv = kdv.Next; } } return builder.MoveToImmutable(); } /// /// Gets the values internal. /// /// An ImmutableArray. private ImmutableArray GetValuesInternal() { var builder = ImmutableArray.CreateBuilder(this.Count); foreach (var value in this._internalBuckets.Values) { var kdv = value; while (kdv != null) { builder.Add(kdv.Value); kdv = kdv.Next; } } return builder.MoveToImmutable(); } /// /// Prepares the items. /// /// The items. /// The count. /// An IReadOnlyDictionary. private static IReadOnlyDictionary PrepareItems(IEnumerable> items, out int count) { count = 0; var dict = new Dictionary(); foreach (var (k, v) in items) { if (k == null) throw new ArgumentException("Keys cannot be null.", nameof(items)); var hash = k.CalculateKnuthHash(); if (!dict.ContainsKey(hash)) { dict.Add(hash, new KeyedValue(k, hash, v)); count++; continue; } var kdv = dict[hash]; var kdvLast = kdv; while (kdv != null) { if (kdv.Key == k) throw new ArgumentException("Given key is already present in the dictionary.", nameof(items)); kdvLast = kdv; kdv = kdv.Next; } kdvLast.Next = new KeyedValue(k, hash, v); count++; } return new ReadOnlyDictionary(dict); } /// /// The keyed value. /// private class KeyedValue { /// /// Gets the key hash. /// public ulong KeyHash { get; } /// /// Gets the key. /// public string Key { get; } /// /// Gets or sets the value. /// public TValue Value { get; set; } /// /// Gets or sets the next. /// public KeyedValue Next { get; set; } /// /// Initializes a new instance of the class. /// /// The key. /// The key hash. /// The value. public KeyedValue(string key, ulong keyHash, TValue value) { this.KeyHash = keyHash; this.Key = key; this.Value = value; } } /// /// The enumerator. /// private class Enumerator : IEnumerator> { /// /// Gets the current. /// public KeyValuePair Current { get; private set; } /// /// Gets the current. /// object IEnumerator.Current => this.Current; /// /// Gets the internal dictionary. /// private readonly CharSpanLookupReadOnlyDictionary _internalDictionary; /// /// Gets the internal enumerator. /// private readonly IEnumerator> _internalEnumerator; /// /// Gets or sets the current value. /// private KeyedValue _currentValue; /// /// Initializes a new instance of the class. /// /// The sp dict. public Enumerator(CharSpanLookupReadOnlyDictionary spDict) { this._internalDictionary = spDict; this._internalEnumerator = this._internalDictionary._internalBuckets.GetEnumerator(); } /// /// Moves the next. /// /// A bool. public bool MoveNext() { var kdv = this._currentValue; if (kdv == null) { if (!this._internalEnumerator.MoveNext()) return false; kdv = this._internalEnumerator.Current.Value; this.Current = new KeyValuePair(kdv.Key, kdv.Value); this._currentValue = kdv.Next; return true; } this.Current = new KeyValuePair(kdv.Key, kdv.Value); this._currentValue = kdv.Next; return true; } /// /// Resets the. /// public void Reset() { this._internalEnumerator.Reset(); this.Current = default; this._currentValue = null; } /// /// Disposes the. /// - public void Dispose() - { - this.Reset(); - } + public void Dispose() => this.Reset(); } } } diff --git a/DisCatSharp.Common/Types/ContinuousMemoryBuffer.cs b/DisCatSharp.Common/Types/ContinuousMemoryBuffer.cs index e17c57268..176953f07 100644 --- a/DisCatSharp.Common/Types/ContinuousMemoryBuffer.cs +++ b/DisCatSharp.Common/Types/ContinuousMemoryBuffer.cs @@ -1,274 +1,274 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Buffers; using System.IO; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace DisCatSharp.Common.Types { /// /// Provides a resizable memory buffer analogous to , using a single continuous memory region instead. /// /// Type of item to hold in the buffer. public sealed class ContinuousMemoryBuffer : IMemoryBuffer where T : unmanaged { /// public ulong Capacity => (ulong)this._buff.Length; /// public ulong Length => (ulong)this._pos; /// public ulong Count => (ulong)(this._pos / this._itemSize); private readonly MemoryPool _pool; private IMemoryOwner _buffOwner; private Memory _buff; private readonly bool _clear; private int _pos; private readonly int _itemSize; private bool _isDisposed; /// /// Creates a new buffer with a specified segment size, specified number of initially-allocated segments, and supplied memory pool. /// /// Initial size of the buffer in bytes. Defaults to 64KiB. /// Memory pool to use for renting buffers. Defaults to . /// Determines whether the underlying buffers should be cleared on exit. If dealing with sensitive data, it might be a good idea to set this option to true. public ContinuousMemoryBuffer(int initialSize = 65536, MemoryPool memPool = default, bool clearOnDispose = false) { this._itemSize = Unsafe.SizeOf(); this._pool = memPool ?? MemoryPool.Shared; this._clear = clearOnDispose; this._buffOwner = this._pool.Rent(initialSize); this._buff = this._buffOwner.Memory; this._isDisposed = false; } /// public void Write(ReadOnlySpan data) { if (this._isDisposed) throw new ObjectDisposedException("This buffer is disposed."); var bytes = MemoryMarshal.AsBytes(data); this.EnsureSize(this._pos + bytes.Length); bytes.CopyTo(this._buff[this._pos..].Span); this._pos += bytes.Length; } /// public void Write(T[] data, int start, int count) => this.Write(data.AsSpan(start, count)); /// public void Write(ArraySegment data) => this.Write(data.AsSpan()); /// public void Write(Stream stream) { if (this._isDisposed) throw new ObjectDisposedException("This buffer is disposed."); if (stream.CanSeek) this.WriteStreamSeekable(stream); else this.WriteStreamUnseekable(stream); } /// /// Writes the stream seekable. /// /// The stream. private void WriteStreamSeekable(Stream stream) { if (stream.Length > int.MaxValue) throw new ArgumentException("Stream is too long.", nameof(stream)); this.EnsureSize(this._pos + (int)stream.Length); #if HAS_SPAN_STREAM_OVERLOADS stream.Read(this._buff.Slice(this._pos).Span); #else var memo = ArrayPool.Shared.Rent((int)stream.Length); try { var br = stream.Read(memo, 0, memo.Length); memo.AsSpan(0, br).CopyTo(this._buff[this._pos..].Span); } finally { ArrayPool.Shared.Return(memo); } #endif this._pos += (int)stream.Length; } /// /// Writes the stream unseekable. /// /// The stream. private void WriteStreamUnseekable(Stream stream) { #if HAS_SPAN_STREAM_OVERLOADS var br = 0; do { this.EnsureSize(this._pos + 4096); br = stream.Read(this._buff.Slice(this._pos).Span); this._pos += br; } while (br != 0); #else var memo = ArrayPool.Shared.Rent(4096); try { var br = 0; while ((br = stream.Read(memo, 0, memo.Length)) != 0) { this.EnsureSize(this._pos + br); memo.AsSpan(0, br).CopyTo(this._buff[this._pos..].Span); this._pos += br; } } finally { ArrayPool.Shared.Return(memo); } #endif } /// public bool Read(Span destination, ulong source, out int itemsWritten) { itemsWritten = 0; if (this._isDisposed) throw new ObjectDisposedException("This buffer is disposed."); source *= (ulong)this._itemSize; if (source > this.Count) throw new ArgumentOutOfRangeException(nameof(source), "Cannot copy data from beyond the buffer."); var start = (int)source; var sbuff = this._buff[start..this._pos ].Span; var dbuff = MemoryMarshal.AsBytes(destination); if (sbuff.Length > dbuff.Length) sbuff = sbuff[..dbuff.Length]; itemsWritten = sbuff.Length / this._itemSize; sbuff.CopyTo(dbuff); - return (this.Length - source) != (ulong)itemsWritten; + return this.Length - source != (ulong)itemsWritten; } /// public bool Read(T[] data, int start, int count, ulong source, out int itemsWritten) => this.Read(data.AsSpan(start, count), source, out itemsWritten); /// public bool Read(ArraySegment data, ulong source, out int itemsWritten) => this.Read(data.AsSpan(), source, out itemsWritten); /// public T[] ToArray() { if (this._isDisposed) throw new ObjectDisposedException("This buffer is disposed."); return MemoryMarshal.Cast(this._buff[..this._pos].Span).ToArray(); } /// public void CopyTo(Stream destination) { if (this._isDisposed) throw new ObjectDisposedException("This buffer is disposed."); #if HAS_SPAN_STREAM_OVERLOADS destination.Write(this._buff.Slice(0, this._pos).Span); #else var buff = this._buff[..this._pos].ToArray(); destination.Write(buff, 0, buff.Length); #endif } /// public void Clear() { if (this._isDisposed) throw new ObjectDisposedException("This buffer is disposed."); this._pos = 0; } /// /// Disposes of any resources claimed by this buffer. /// public void Dispose() { if (this._isDisposed) return; this._isDisposed = true; if (this._clear) this._buff.Span.Clear(); this._buffOwner.Dispose(); this._buff = default; } /// /// Ensures the size. /// /// The new capacity. private void EnsureSize(int newCapacity) { var cap = this._buff.Length; if (cap >= newCapacity) return; var factor = newCapacity / cap; if (newCapacity % cap != 0) ++factor; var newActualCapacity = cap * factor; var newBuffOwner = this._pool.Rent(newActualCapacity); var newBuff = newBuffOwner.Memory; this._buff.Span.CopyTo(newBuff.Span); if (this._clear) this._buff.Span.Clear(); this._buffOwner.Dispose(); this._buffOwner = newBuffOwner; this._buff = newBuff; } } } diff --git a/DisCatSharp.Common/Types/MemoryBuffer.cs b/DisCatSharp.Common/Types/MemoryBuffer.cs index 1d7020230..1b923ad82 100644 --- a/DisCatSharp.Common/Types/MemoryBuffer.cs +++ b/DisCatSharp.Common/Types/MemoryBuffer.cs @@ -1,356 +1,356 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Buffers; using System.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace DisCatSharp.Common.Types { /// /// Provides a resizable memory buffer, which can be read from and written to. It will automatically resize whenever required. /// /// Type of item to hold in the buffer. public sealed class MemoryBuffer : IMemoryBuffer where T : unmanaged { /// public ulong Capacity => this._segments.Aggregate(0UL, (a, x) => a + (ulong)x.Memory.Length); // .Sum() does only int /// public ulong Length { get; private set; } /// public ulong Count => this.Length / (ulong)this._itemSize; private readonly MemoryPool _pool; private readonly int _segmentSize; private int _lastSegmentLength; private int _segNo; private readonly bool _clear; private readonly List> _segments; private readonly int _itemSize; private bool _isDisposed; /// /// Creates a new buffer with a specified segment size, specified number of initially-allocated segments, and supplied memory pool. /// /// Byte size of an individual segment. Defaults to 64KiB. /// Number of segments to allocate. Defaults to 0. /// Memory pool to use for renting buffers. Defaults to . /// Determines whether the underlying buffers should be cleared on exit. If dealing with sensitive data, it might be a good idea to set this option to true. public MemoryBuffer(int segmentSize = 65536, int initialSegmentCount = 0, MemoryPool memPool = default, bool clearOnDispose = false) { this._itemSize = Unsafe.SizeOf(); if (segmentSize % this._itemSize != 0) throw new ArgumentException("Segment size must match size of individual item."); this._pool = memPool ?? MemoryPool.Shared; this._segmentSize = segmentSize; this._segNo = 0; this._lastSegmentLength = 0; this._clear = clearOnDispose; this._segments = new List>(initialSegmentCount + 1); for (var i = 0; i < initialSegmentCount; i++) this._segments.Add(this._pool.Rent(this._segmentSize)); this.Length = 0; this._isDisposed = false; } /// public void Write(ReadOnlySpan data) { if (this._isDisposed) throw new ObjectDisposedException("This buffer is disposed."); var src = MemoryMarshal.AsBytes(data); this.Grow(src.Length); while (this._segNo < this._segments.Count && src.Length > 0) { var seg = this._segments[this._segNo]; var mem = seg.Memory; var avs = mem.Length - this._lastSegmentLength; avs = avs > src.Length ? src.Length : avs; var dmem = mem[this._lastSegmentLength..]; src[..avs].CopyTo(dmem.Span); src = src[avs..]; this.Length += (ulong)avs; this._lastSegmentLength += avs; if (this._lastSegmentLength == mem.Length) { this._segNo++; this._lastSegmentLength = 0; } } } /// public void Write(T[] data, int start, int count) => this.Write(data.AsSpan(start, count)); /// public void Write(ArraySegment data) => this.Write(data.AsSpan()); /// public void Write(Stream stream) { if (this._isDisposed) throw new ObjectDisposedException("This buffer is disposed."); if (stream.CanSeek) this.WriteStreamSeekable(stream); else this.WriteStreamUnseekable(stream); } /// /// Writes the stream seekable. /// /// The stream. private void WriteStreamSeekable(Stream stream) { var len = (int)(stream.Length - stream.Position); this.Grow(len); #if !HAS_SPAN_STREAM_OVERLOADS var buff = new byte[this._segmentSize]; #endif while (this._segNo < this._segments.Count && len > 0) { var seg = this._segments[this._segNo]; var mem = seg.Memory; var avs = mem.Length - this._lastSegmentLength; avs = avs > len ? len : avs; var dmem = mem[this._lastSegmentLength..]; #if HAS_SPAN_STREAM_OVERLOADS stream.Read(dmem.Span); #else var lsl = this._lastSegmentLength; var slen = dmem.Span.Length - lsl; stream.Read(buff, 0, slen); buff.AsSpan(0, slen).CopyTo(dmem.Span); #endif len -= dmem.Span.Length; this.Length += (ulong)avs; this._lastSegmentLength += avs; if (this._lastSegmentLength == mem.Length) { this._segNo++; this._lastSegmentLength = 0; } } } /// /// Writes the stream unseekable. /// /// The stream. private void WriteStreamUnseekable(Stream stream) { var read = 0; #if HAS_SPAN_STREAM_OVERLOADS Span buffs = stackalloc byte[this._segmentSize]; while ((read = stream.Read(buffs)) != 0) #else var buff = new byte[this._segmentSize]; var buffs = buff.AsSpan(); while ((read = stream.Read(buff, 0, buff.Length - this._lastSegmentLength)) != 0) #endif this.Write(MemoryMarshal.Cast(buffs[..read])); } /// public bool Read(Span destination, ulong source, out int itemsWritten) { itemsWritten = 0; if (this._isDisposed) throw new ObjectDisposedException("This buffer is disposed."); source *= (ulong)this._itemSize; if (source > this.Count) throw new ArgumentOutOfRangeException(nameof(source), "Cannot copy data from beyond the buffer."); // Find where to begin var i = 0; for (; i < this._segments.Count; i++) { var seg = this._segments[i]; var mem = seg.Memory; if ((ulong)mem.Length > source) break; source -= (ulong)mem.Length; } // Do actual copy var dl = (int)(this.Length - source); var sri = (int)source; var dst = MemoryMarshal.AsBytes(destination); for (; i < this._segments.Count && dst.Length > 0; i++) { var seg = this._segments[i]; var mem = seg.Memory; var src = mem.Span; if (sri != 0) { src = src[sri..]; sri = 0; } if (itemsWritten + src.Length > dl) src = src[..(dl - itemsWritten)]; if (src.Length > dst.Length) src = src[..dst.Length]; src.CopyTo(dst); dst = dst[src.Length..]; itemsWritten += src.Length; } itemsWritten /= this._itemSize; - return (this.Length - source) != (ulong)itemsWritten; + return this.Length - source != (ulong)itemsWritten; } /// public bool Read(T[] data, int start, int count, ulong source, out int itemsWritten) => this.Read(data.AsSpan(start, count), source, out itemsWritten); /// public bool Read(ArraySegment data, ulong source, out int itemsWritten) => this.Read(data.AsSpan(), source, out itemsWritten); /// public T[] ToArray() { if (this._isDisposed) throw new ObjectDisposedException("This buffer is disposed."); var bytes = new T[this.Count]; this.Read(bytes, 0, out _); return bytes; } /// public void CopyTo(Stream destination) { if (this._isDisposed) throw new ObjectDisposedException("This buffer is disposed."); #if HAS_SPAN_STREAM_OVERLOADS foreach (var seg in this._segments) destination.Write(seg.Memory.Span); #else var longest = this._segments.Max(x => x.Memory.Length); var buff = new byte[longest]; foreach (var seg in this._segments) { var mem = seg.Memory.Span; var spn = buff.AsSpan(0, mem.Length); mem.CopyTo(spn); destination.Write(buff, 0, spn.Length); } #endif } /// public void Clear() { if (this._isDisposed) throw new ObjectDisposedException("This buffer is disposed."); this._segNo = 0; this._lastSegmentLength = 0; this.Length = 0; } /// /// Disposes of any resources claimed by this buffer. /// public void Dispose() { if (this._isDisposed) return; this._isDisposed = true; foreach (var segment in this._segments) { if (this._clear) segment.Memory.Span.Clear(); segment.Dispose(); } } /// /// Grows the. /// /// The min amount. private void Grow(int minAmount) { var capacity = this.Capacity; var length = this.Length; - var totalAmt = (length + (ulong)minAmount); + var totalAmt = length + (ulong)minAmount; if (capacity >= totalAmt) return; // we're good var amt = (int)(totalAmt - capacity); var segCount = amt / this._segmentSize; if (amt % this._segmentSize != 0) segCount++; // Basically List.EnsureCapacity // Default grow behaviour is minimum current*2 var segCap = this._segments.Count + segCount; if (segCap > this._segments.Capacity) this._segments.Capacity = segCap < this._segments.Capacity * 2 ? this._segments.Capacity * 2 : segCap; for (var i = 0; i < segCount; i++) this._segments.Add(this._pool.Rent(this._segmentSize)); } } } diff --git a/DisCatSharp.Common/Types/Optional.cs b/DisCatSharp.Common/Types/Optional.cs index 8d537369b..79c671b5d 100644 --- a/DisCatSharp.Common/Types/Optional.cs +++ b/DisCatSharp.Common/Types/Optional.cs @@ -1,226 +1,226 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Diagnostics; namespace DisCatSharp.Common { /// /// Represents a property with an optional value. /// /// Type of the value. [DebuggerDisplay(@"{DEBUGGER_DISPLAY,nq}")] public struct Optional : IEquatable>, IEquatable { /// /// Gets an initialized instance of which has no value set. /// - public static Optional Default { get; } = new Optional(); + public static Optional Default { get; } = new(); /// /// Gets whether the value of this is present. /// public bool HasValue { get; } /// /// Gets the value of this . Will throw if a value is not present. /// public T Value => this.HasValue ? this._value : throw new InvalidOperationException("This property has no value set."); private readonly T _value; /// /// Gets the debugger display. /// private string DEBUGGER_DISPLAY => this.HasValue ? $"Optional<{typeof(T)}> {this._value?.ToString() ?? ""}" : $"Optional<{typeof(T)}> "; /// /// Creates a new property with specified value. /// /// Value of this property. public Optional(T value) { this.HasValue = true; this._value = value; } /// /// Returns hash code of the underlying value. /// /// Hash code of the underlying value. public override int GetHashCode() => this.HasValue ? this._value?.GetHashCode() ?? 0 : 0; /// /// Checks whether the value of this property is equal to another value. /// /// Object to compare against. /// Whether the supplied object is equal to the value of this property. public override bool Equals(object obj) { if (obj is Optional opt) return this.Equals(opt); if (obj is T val) return this.Equals(val); if (!this.HasValue && obj == null) return true; if (this.HasValue) return object.Equals(this._value, obj); return false; } /// /// Checks whether this property is equal to another property. /// /// Property to compare against. /// Whether the supplied property is equal to this property. public bool Equals(Optional other) { if (!this.HasValue && !other.HasValue) return true; else if (this.HasValue != other.HasValue) return false; else return object.Equals(this._value, other._value); } /// /// Checks whether this proerty's value is equal to another value. /// /// Value to compare this property's value against. /// Whether the supplied value is equal to the value of this property. public bool Equals(T other) { if (!this.HasValue && other == null) return true; if (this.HasValue) return object.Equals(this._value, other); return false; } /// /// Returns a string representation of the underlying value, if present. /// /// String representation of the underlying value, if present. public override string ToString() => this.HasValue ? this._value?.ToString() : ""; /// /// Converts a specified value into an optional property of the value's type. The resulting property will have /// its value set to the supplied one. /// /// Value to convert into an optional property. public static implicit operator Optional(T value) - => new Optional(value); + => new(value); /// /// Compares two properties and returns whether they are equal. /// /// Property to compare against. /// Property to compare. /// Whether the two properties are equal. public static bool operator ==(Optional left, Optional right) => left.Equals(right); /// /// Compares two properties and returns whether they are not equal. /// /// Property to compare against. /// Property to compare. /// Whether the two properties are not equal. public static bool operator !=(Optional left, Optional right) => !left.Equals(right); /// /// Compares a property's value against another value, and returns whether they are equal. /// /// Property to compare against. /// Value to compare. /// Whether the property's value is equal to the specified value. public static bool operator ==(Optional left, T right) => left.Equals(right); /// /// Compares a property's value against another value, and returns whether they are not equal. /// /// Property to compare against. /// Value to compare. /// Whether this property's value is not equal to the specified value. public static bool operator !=(Optional left, T right) => !left.Equals(right); /// /// Checks whether specified property has a value. /// /// Property to check. /// Whether the property has a value. public static bool operator true(Optional opt) => opt.HasValue; /// /// Checks whether specified property has no value. /// /// Property to check. /// Whether the property has no value. public static bool operator false(Optional opt) => !opt.HasValue; } /// /// Utilities for creation of optional properties. /// public static class Optional { /// /// Creates a new from a value of type . /// /// Type of the value to create an optional property for. /// Value to set the property to. /// Created optional property, which has a specified value set. public static Optional FromValue(T value) - => new Optional(value); + => new(value); /// /// Creates a new from a default value for type . /// /// Type of the value to create an optional property for. /// Created optional property, which has a default value for set. public static Optional FromDefaultValue() - => new Optional(default); + => new(default); /// /// Creates a new which has no value. /// /// Type of the value to create an optional property for. /// Created optional property, which has no value set. public static Optional FromNoValue() => Optional.Default; } } diff --git a/DisCatSharp.Common/Types/SecureRandom.cs b/DisCatSharp.Common/Types/SecureRandom.cs index e27526a15..2d6ff778c 100644 --- a/DisCatSharp.Common/Types/SecureRandom.cs +++ b/DisCatSharp.Common/Types/SecureRandom.cs @@ -1,348 +1,342 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Buffers; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Security.Cryptography; namespace DisCatSharp.Common { /// /// Provides a cryptographically-secure pseudorandom number generator (CSPRNG) implementation compatible with . /// public sealed class SecureRandom : Random, IDisposable { /// /// Gets the r n g. /// private readonly RandomNumberGenerator _rng = RandomNumberGenerator.Create(); - private volatile bool _isDisposed = false; + private volatile bool _isDisposed; /// /// Creates a new instance of . /// public SecureRandom() { } /// /// Finalizes this instance by disposing it. /// ~SecureRandom() { this.Dispose(); } /// /// Fills a supplied buffer with random bytes. /// /// Buffer to fill with random bytes. - public void GetBytes(byte[] buffer) - { - this._rng.GetBytes(buffer); - } + public void GetBytes(byte[] buffer) => this._rng.GetBytes(buffer); /// /// Fills a supplied buffer with random nonzero bytes. /// /// Buffer to fill with random nonzero bytes. - public void GetNonZeroBytes(byte[] buffer) - { - this._rng.GetNonZeroBytes(buffer); - } + public void GetNonZeroBytes(byte[] buffer) => this._rng.GetNonZeroBytes(buffer); /// /// Fills a supplied memory region with random bytes. /// /// Memmory region to fill with random bytes. public void GetBytes(Span buffer) { #if NETCOREAPP this.RNG.GetBytes(buffer); #else var buff = ArrayPool.Shared.Rent(buffer.Length); try { var buffSpan = buff.AsSpan(0, buffer.Length); this._rng.GetBytes(buff); buffSpan.CopyTo(buffer); } finally { ArrayPool.Shared.Return(buff); } #endif } /// /// Fills a supplied memory region with random nonzero bytes. /// /// Memmory region to fill with random nonzero bytes. public void GetNonZeroBytes(Span buffer) { #if NETCOREAPP this.RNG.GetNonZeroBytes(buffer); #else var buff = ArrayPool.Shared.Rent(buffer.Length); try { var buffSpan = buff.AsSpan(0, buffer.Length); this._rng.GetNonZeroBytes(buff); buffSpan.CopyTo(buffer); } finally { ArrayPool.Shared.Return(buff); } #endif } /// /// Generates a signed 8-bit integer within specified range. /// /// Minimum value to generate. Defaults to 0. /// Maximum value to generate. Defaults to . /// Generated random value. public sbyte GetInt8(sbyte min = 0, sbyte max = sbyte.MaxValue) { if (max <= min) throw new ArgumentException("Maximum needs to be greater than minimum.", nameof(max)); var offset = (sbyte)(min < 0 ? -min : 0); min += offset; max += offset; return (sbyte)(Math.Abs(this.Generate()) % (max - min) + min - offset); } /// /// Generates a unsigned 8-bit integer within specified range. /// /// Minimum value to generate. Defaults to 0. /// Maximum value to generate. Defaults to . /// Generated random value. public byte GetUInt8(byte min = 0, byte max = byte.MaxValue) { if (max <= min) throw new ArgumentException("Maximum needs to be greater than minimum.", nameof(max)); return (byte)(this.Generate() % (max - min) + min); } /// /// Generates a signed 16-bit integer within specified range. /// /// Minimum value to generate. Defaults to 0. /// Maximum value to generate. Defaults to . /// Generated random value. public short GetInt16(short min = 0, short max = short.MaxValue) { if (max <= min) throw new ArgumentException("Maximum needs to be greater than minimum.", nameof(max)); var offset = (short)(min < 0 ? -min : 0); min += offset; max += offset; return (short)(Math.Abs(this.Generate()) % (max - min) + min - offset); } /// /// Generates a unsigned 16-bit integer within specified range. /// /// Minimum value to generate. Defaults to 0. /// Maximum value to generate. Defaults to . /// Generated random value. public ushort GetUInt16(ushort min = 0, ushort max = ushort.MaxValue) { if (max <= min) throw new ArgumentException("Maximum needs to be greater than minimum.", nameof(max)); return (ushort)(this.Generate() % (max - min) + min); } /// /// Generates a signed 32-bit integer within specified range. /// /// Minimum value to generate. Defaults to 0. /// Maximum value to generate. Defaults to . /// Generated random value. public int GetInt32(int min = 0, int max = int.MaxValue) { if (max <= min) throw new ArgumentException("Maximum needs to be greater than minimum.", nameof(max)); var offset = min < 0 ? -min : 0; min += offset; max += offset; return Math.Abs(this.Generate()) % (max - min) + min - offset; } /// /// Generates a unsigned 32-bit integer within specified range. /// /// Minimum value to generate. Defaults to 0. /// Maximum value to generate. Defaults to . /// Generated random value. public uint GetUInt32(uint min = 0, uint max = uint.MaxValue) { if (max <= min) throw new ArgumentException("Maximum needs to be greater than minimum.", nameof(max)); return this.Generate() % (max - min) + min; } /// /// Generates a signed 64-bit integer within specified range. /// /// Minimum value to generate. Defaults to 0. /// Maximum value to generate. Defaults to . /// Generated random value. public long GetInt64(long min = 0, long max = long.MaxValue) { if (max <= min) throw new ArgumentException("Maximum needs to be greater than minimum.", nameof(max)); var offset = min < 0 ? -min : 0; min += offset; max += offset; return Math.Abs(this.Generate()) % (max - min) + min - offset; } /// /// Generates a unsigned 64-bit integer within specified range. /// /// Minimum value to generate. Defaults to 0. /// Maximum value to generate. Defaults to . /// Generated random value. public ulong GetUInt64(ulong min = 0, ulong max = ulong.MaxValue) { if (max <= min) throw new ArgumentException("Maximum needs to be greater than minimum.", nameof(max)); return this.Generate() % (max - min) + min; } /// /// Generates a 32-bit floating-point number between 0.0 and 1.0. /// /// Generated 32-bit floating-point number. public float GetSingle() { var (i1, i2) = ((float)this.GetInt32(), (float)this.GetInt32()); return i1 / i2 % 1.0F; } /// /// Generates a 64-bit floating-point number between 0.0 and 1.0. /// /// Generated 64-bit floating-point number. public double GetDouble() { var (i1, i2) = ((double)this.GetInt64(), (double)this.GetInt64()); return i1 / i2 % 1.0; } /// /// Generates a 32-bit integer between 0 and . Upper end exclusive. /// /// Generated 32-bit integer. public override int Next() => this.GetInt32(); /// /// Generates a 32-bit integer between 0 and . Upper end exclusive. /// /// Maximum value of the generated integer. /// Generated 32-bit integer. public override int Next(int maxValue) => this.GetInt32(0, maxValue); /// /// Generates a 32-bit integer between and . Upper end exclusive. /// /// Minimum value of the generate integer. /// Maximum value of the generated integer. /// Generated 32-bit integer. public override int Next(int minValue, int maxValue) => this.GetInt32(minValue, maxValue); /// /// Generates a 64-bit floating-point number between 0.0 and 1.0. Upper end exclusive. /// /// Generated 64-bit floating-point number. public override double NextDouble() => this.GetDouble(); /// /// Fills specified buffer with random bytes. /// /// Buffer to fill with bytes. public override void NextBytes(byte[] buffer) => this.GetBytes(buffer); /// /// Fills specified memory region with random bytes. /// /// Memory region to fill with bytes. #if NETCOREAPP override #endif public new void NextBytes(Span buffer) => this.GetBytes(buffer); /// /// Disposes this instance and its resources. /// public void Dispose() { if (this._isDisposed) return; this._isDisposed = true; this._rng.Dispose(); } /// /// Generates a random 64-bit floating-point number between 0.0 and 1.0. Upper end exclusive. /// /// Generated 64-bit floating-point number. protected override double Sample() => this.GetDouble(); /// /// Generates the. /// /// A T. private T Generate() where T : struct { var size = Unsafe.SizeOf(); Span buff = stackalloc byte[size]; this.GetBytes(buff); return MemoryMarshal.Read(buff); } } } diff --git a/DisCatSharp.Common/Utilities/AsyncEvent/AsyncEvent.cs b/DisCatSharp.Common/Utilities/AsyncEvent/AsyncEvent.cs index ae92f6229..c8a0550db 100644 --- a/DisCatSharp.Common/Utilities/AsyncEvent/AsyncEvent.cs +++ b/DisCatSharp.Common/Utilities/AsyncEvent/AsyncEvent.cs @@ -1,204 +1,201 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Threading.Tasks; namespace DisCatSharp.Common.Utilities { /// /// ABC for , allowing for using instances thereof without knowing the underlying instance's type parameters. /// public abstract class AsyncEvent { /// /// Gets the name of this event. /// public string Name { get; } /// /// Prevents a default instance of the class from being created. /// /// The name. private protected AsyncEvent(string name) { this.Name = name; } } /// /// Implementation of asynchronous event. The handlers of such events are executed asynchronously, but sequentially. /// /// Type of the object that dispatches this event. /// Type of event argument object passed to this event's handlers. public sealed class AsyncEvent : AsyncEvent where TArgs : AsyncEventArgs { /// /// Gets the maximum alloted execution time for all handlers. Any event which causes the handler to time out /// will raise a non-fatal . /// public TimeSpan MaximumExecutionTime { get; } - private readonly object _lock = new object(); + private readonly object _lock = new(); private ImmutableArray> _handlers; private readonly AsyncEventExceptionHandler _exceptionHandler; /// /// Creates a new asynchronous event with specified name and exception handler. /// /// Name of this event. /// Maximum handler execution time. A value of means infinite. /// Delegate which handles exceptions caused by this event. public AsyncEvent(string name, TimeSpan maxExecutionTime, AsyncEventExceptionHandler exceptionHandler) : base(name) { this._handlers = ImmutableArray>.Empty; this._exceptionHandler = exceptionHandler; this.MaximumExecutionTime = maxExecutionTime; } /// /// Registers a new handler for this event. /// /// Handler to register for this event. public void Register(AsyncEventHandler handler) { if (handler == null) throw new ArgumentNullException(nameof(handler)); lock (this._lock) this._handlers = this._handlers.Add(handler); } /// /// Unregisters an existing handler from this event. /// /// Handler to unregister from the event. public void Unregister(AsyncEventHandler handler) { if (handler == null) throw new ArgumentNullException(nameof(handler)); lock (this._lock) this._handlers = this._handlers.Remove(handler); } /// /// Unregisters all existing handlers from this event. /// - public void UnregisterAll() - { - this._handlers = ImmutableArray>.Empty; - } + public void UnregisterAll() => this._handlers = ImmutableArray>.Empty; /// /// Raises this event by invoking all of its registered handlers, in order of registration. /// All exceptions throw during invocation will be handled by the event's registered exception handler. /// /// Object which raised this event. /// Arguments for this event. /// Defines what to do with exceptions caught from handlers. /// public async Task InvokeAsync(TSender sender, TArgs e, AsyncEventExceptionMode exceptionMode = AsyncEventExceptionMode.Default) { var handlers = this._handlers; if (handlers.Length == 0) return; // Collect exceptions List exceptions = null; if ((exceptionMode & AsyncEventExceptionMode.ThrowAll) != 0) exceptions = new List(handlers.Length * 2 /* timeout + regular */); // If we have a timeout configured, start the timeout task var timeout = this.MaximumExecutionTime > TimeSpan.Zero ? Task.Delay(this.MaximumExecutionTime) : null; for (var i = 0; i < handlers.Length; i++) { var handler = handlers[i]; try { // Start the handler execution var handlerTask = handler(sender, e); if (handlerTask != null && timeout != null) { // If timeout is configured, wait for any task to finish // If the timeout task finishes first, the handler is causing a timeout var result = await Task.WhenAny(timeout, handlerTask).ConfigureAwait(false); if (result == timeout) { timeout = null; var timeoutEx = new AsyncEventTimeoutException(this, handler); // Notify about the timeout and complete execution if ((exceptionMode & AsyncEventExceptionMode.HandleNonFatal) == AsyncEventExceptionMode.HandleNonFatal) this.HandleException(timeoutEx, handler, sender, e); if ((exceptionMode & AsyncEventExceptionMode.ThrowNonFatal) == AsyncEventExceptionMode.ThrowNonFatal) exceptions.Add(timeoutEx); await handlerTask.ConfigureAwait(false); } } else if (handlerTask != null) { // No timeout is configured, or timeout already expired, proceed as usual await handlerTask.ConfigureAwait(false); } if (e.Handled) break; } catch (Exception ex) { e.Handled = false; if ((exceptionMode & AsyncEventExceptionMode.HandleFatal) == AsyncEventExceptionMode.HandleFatal) this.HandleException(ex, handler, sender, e); if ((exceptionMode & AsyncEventExceptionMode.ThrowFatal) == AsyncEventExceptionMode.ThrowFatal) exceptions.Add(ex); } } if ((exceptionMode & AsyncEventExceptionMode.ThrowAll) != 0 && exceptions.Count > 0) throw new AggregateException("Exceptions were thrown during execution of the event's handlers.", exceptions); } /// /// Handles the exception. /// /// The ex. /// The handler. /// The sender. /// The args. private void HandleException(Exception ex, AsyncEventHandler handler, TSender sender, TArgs args) { if (this._exceptionHandler != null) this._exceptionHandler(this, ex, handler, sender, args); } } } diff --git a/DisCatSharp.Common/Utilities/AsyncExecutor.cs b/DisCatSharp.Common/Utilities/AsyncExecutor.cs index 4e3733d83..f695b980e 100644 --- a/DisCatSharp.Common/Utilities/AsyncExecutor.cs +++ b/DisCatSharp.Common/Utilities/AsyncExecutor.cs @@ -1,173 +1,173 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Threading; using System.Threading.Tasks; namespace DisCatSharp.Common.Utilities { /// /// Provides a simplified way of executing asynchronous code synchronously. /// public class AsyncExecutor { /// /// Creates a new instance of asynchronous executor. /// public AsyncExecutor() { } /// /// Executes a specified task in an asynchronous manner, waiting for its completion. /// /// Task to execute. public void Execute(Task task) { // create state object var taskState = new StateRef(new AutoResetEvent(false)); // queue a task and wait for it to finish executing task.ContinueWith(TaskCompletionHandler, taskState); taskState.Lock.WaitOne(); // check for and rethrow any exceptions if (taskState.Exception != null) throw taskState.Exception; // completion method void TaskCompletionHandler(Task t, object state) { // retrieve state data var stateRef = state as StateRef; // retrieve any exceptions or cancellation status if (t.IsFaulted) { if (t.Exception.InnerExceptions.Count == 1) // unwrap if 1 stateRef.Exception = t.Exception.InnerException; else stateRef.Exception = t.Exception; } else if (t.IsCanceled) { stateRef.Exception = new TaskCanceledException(t); } // signal that the execution is done stateRef.Lock.Set(); } } /// /// Executes a specified task in an asynchronous manner, waiting for its completion, and returning the result. /// /// Type of the Task's return value. /// Task to execute. /// Task's result. public T Execute(Task task) { // create state object var taskState = new StateRef(new AutoResetEvent(false)); // queue a task and wait for it to finish executing task.ContinueWith(TaskCompletionHandler, taskState); taskState.Lock.WaitOne(); // check for and rethrow any exceptions if (taskState.Exception != null) throw taskState.Exception; // return the result, if any if (taskState.HasResult) return taskState.Result; // throw exception if no result throw new Exception("Task returned no result."); // completion method void TaskCompletionHandler(Task t, object state) { // retrieve state data var stateRef = state as StateRef; // retrieve any exceptions or cancellation status if (t.IsFaulted) { if (t.Exception.InnerExceptions.Count == 1) // unwrap if 1 stateRef.Exception = t.Exception.InnerException; else stateRef.Exception = t.Exception; } else if (t.IsCanceled) { stateRef.Exception = new TaskCanceledException(t); } // return the result from the task, if any if (t.IsCompleted && !t.IsFaulted) { stateRef.HasResult = true; stateRef.Result = t.Result; } // signal that the execution is done stateRef.Lock.Set(); } } /// /// The state ref. /// private sealed class StateRef { /// /// Gets the lock used to wait for task's completion. /// public AutoResetEvent Lock { get; } /// /// Gets the exception that occured during task's execution, if any. /// public Exception Exception { get; set; } /// /// Gets the result returned by the task. /// public T Result { get; set; } /// /// Gets whether the task returned a result. /// - public bool HasResult { get; set; } = false; + public bool HasResult { get; set; } /// /// Initializes a new instance of the class. /// /// The lock. public StateRef(AutoResetEvent @lock) { this.Lock = @lock; } } } } diff --git a/DisCatSharp.Common/Utilities/Extensions.cs b/DisCatSharp.Common/Utilities/Extensions.cs index d5c946398..32ac6c0bc 100644 --- a/DisCatSharp.Common/Utilities/Extensions.cs +++ b/DisCatSharp.Common/Utilities/Extensions.cs @@ -1,490 +1,490 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Generic; using System.Runtime.CompilerServices; namespace DisCatSharp.Common { /// /// Assortment of various extension and utility methods, designed to make working with various types a little easier. /// public static class Extensions { /// /// Deconstructs a key-value pair item () into 2 separate variables. /// This allows for enumerating over dictionaries in foreach blocks by using a (k, v) tuple as the enumerator variable, instead of having to use a directly. /// /// Type of dictionary item key. /// Type of dictionary item value. /// Key-value pair to deconstruct. /// Deconstructed key. /// Deconstructed value. public static void Deconstruct(this KeyValuePair kvp, out TKey key, out TValue value) { key = kvp.Key; value = kvp.Value; } /// /// Calculates the length of string representation of given number in base 10 (including sign, if present). /// /// Number to calculate the length of. /// Calculated number length. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int CalculateLength(this sbyte num) => num == 0 ? 1 : (int)Math.Floor(Math.Log10(Math.Abs(num == sbyte.MinValue ? num + 1 : num))) + (num < 0 ? 2 /* include sign */ : 1); /// /// Calculates the length of string representation of given number in base 10 (including sign, if present). /// /// Number to calculate the length of. /// Calculated nuembr length. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int CalculateLength(this byte num) => num == 0 ? 1 : (int)Math.Floor(Math.Log10(num)) + 1; /// /// Calculates the length of string representation of given number in base 10 (including sign, if present). /// /// Number to calculate the length of. /// Calculated number length. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int CalculateLength(this short num) => num == 0 ? 1 : (int)Math.Floor(Math.Log10(Math.Abs(num == short.MinValue ? num + 1 : num))) + (num < 0 ? 2 /* include sign */ : 1); /// /// Calculates the length of string representation of given number in base 10 (including sign, if present). /// /// Number to calculate the length of. /// Calculated nuembr length. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int CalculateLength(this ushort num) => num == 0 ? 1 : (int)Math.Floor(Math.Log10(num)) + 1; /// /// Calculates the length of string representation of given number in base 10 (including sign, if present). /// /// Number to calculate the length of. /// Calculated number length. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int CalculateLength(this int num) => num == 0 ? 1 : (int)Math.Floor(Math.Log10(Math.Abs(num == int.MinValue ? num + 1 : num))) + (num < 0 ? 2 /* include sign */ : 1); /// /// Calculates the length of string representation of given number in base 10 (including sign, if present). /// /// Number to calculate the length of. /// Calculated nuembr length. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int CalculateLength(this uint num) => num == 0 ? 1 : (int)Math.Floor(Math.Log10(num)) + 1; /// /// Calculates the length of string representation of given number in base 10 (including sign, if present). /// /// Number to calculate the length of. /// Calculated number length. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int CalculateLength(this long num) => num == 0 ? 1 : (int)Math.Floor(Math.Log10(Math.Abs(num == long.MinValue ? num + 1 : num))) + (num < 0 ? 2 /* include sign */ : 1); /// /// Calculates the length of string representation of given number in base 10 (including sign, if present). /// /// Number to calculate the length of. /// Calculated nuembr length. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int CalculateLength(this ulong num) => num == 0 ? 1 : (int)Math.Floor(Math.Log10(num)) + 1; /// /// Tests wheter given value is in supplied range, optionally allowing it to be an exclusive check. /// /// Number to test. /// Lower bound of the range. /// Upper bound of the range. /// Whether the check is to be inclusive. /// Whether the value is in range. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsInRange(this sbyte num, sbyte min, sbyte max, bool inclusive = true) { if (min > max) { min ^= max; max ^= min; min ^= max; } - return inclusive ? (num >= min && num <= max) : (num > min && num < max); + return inclusive ? num >= min && num <= max : num > min && num < max; } /// /// Tests wheter given value is in supplied range, optionally allowing it to be an exclusive check. /// /// Number to test. /// Lower bound of the range. /// Upper bound of the range. /// Whether the check is to be inclusive. /// Whether the value is in range. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsInRange(this byte num, byte min, byte max, bool inclusive = true) { if (min > max) { min ^= max; max ^= min; min ^= max; } - return inclusive ? (num >= min && num <= max) : (num > min && num < max); + return inclusive ? num >= min && num <= max : num > min && num < max; } /// /// Tests wheter given value is in supplied range, optionally allowing it to be an exclusive check. /// /// Number to test. /// Lower bound of the range. /// Upper bound of the range. /// Whether the check is to be inclusive. /// Whether the value is in range. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsInRange(this short num, short min, short max, bool inclusive = true) { if (min > max) { min ^= max; max ^= min; min ^= max; } - return inclusive ? (num >= min && num <= max) : (num > min && num < max); + return inclusive ? num >= min && num <= max : num > min && num < max; } /// /// Tests wheter given value is in supplied range, optionally allowing it to be an exclusive check. /// /// Number to test. /// Lower bound of the range. /// Upper bound of the range. /// Whether the check is to be inclusive. /// Whether the value is in range. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsInRange(this ushort num, ushort min, ushort max, bool inclusive = true) { if (min > max) { min ^= max; max ^= min; min ^= max; } - return inclusive ? (num >= min && num <= max) : (num > min && num < max); + return inclusive ? num >= min && num <= max : num > min && num < max; } /// /// Tests wheter given value is in supplied range, optionally allowing it to be an exclusive check. /// /// Number to test. /// Lower bound of the range. /// Upper bound of the range. /// Whether the check is to be inclusive. /// Whether the value is in range. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsInRange(this int num, int min, int max, bool inclusive = true) { if (min > max) { min ^= max; max ^= min; min ^= max; } - return inclusive ? (num >= min && num <= max) : (num > min && num < max); + return inclusive ? num >= min && num <= max : num > min && num < max; } /// /// Tests wheter given value is in supplied range, optionally allowing it to be an exclusive check. /// /// Number to test. /// Lower bound of the range. /// Upper bound of the range. /// Whether the check is to be inclusive. /// Whether the value is in range. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsInRange(this uint num, uint min, uint max, bool inclusive = true) { if (min > max) { min ^= max; max ^= min; min ^= max; } - return inclusive ? (num >= min && num <= max) : (num > min && num < max); + return inclusive ? num >= min && num <= max : num > min && num < max; } /// /// Tests wheter given value is in supplied range, optionally allowing it to be an exclusive check. /// /// Number to test. /// Lower bound of the range. /// Upper bound of the range. /// Whether the check is to be inclusive. /// Whether the value is in range. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsInRange(this long num, long min, long max, bool inclusive = true) { if (min > max) { min ^= max; max ^= min; min ^= max; } - return inclusive ? (num >= min && num <= max) : (num > min && num < max); + return inclusive ? num >= min && num <= max : num > min && num < max; } /// /// Tests wheter given value is in supplied range, optionally allowing it to be an exclusive check. /// /// Number to test. /// Lower bound of the range. /// Upper bound of the range. /// Whether the check is to be inclusive. /// Whether the value is in range. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsInRange(this ulong num, ulong min, ulong max, bool inclusive = true) { if (min > max) { min ^= max; max ^= min; min ^= max; } - return inclusive ? (num >= min && num <= max) : (num > min && num < max); + return inclusive ? num >= min && num <= max : num > min && num < max; } /// /// Tests wheter given value is in supplied range, optionally allowing it to be an exclusive check. /// /// Number to test. /// Lower bound of the range. /// Upper bound of the range. /// Whether the check is to be inclusive. /// Whether the value is in range. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsInRange(this float num, float min, float max, bool inclusive = true) { if (min > max) return false; - return inclusive ? (num >= min && num <= max) : (num > min && num < max); + return inclusive ? num >= min && num <= max : num > min && num < max; } /// /// Tests wheter given value is in supplied range, optionally allowing it to be an exclusive check. /// /// Number to test. /// Lower bound of the range. /// Upper bound of the range. /// Whether the check is to be inclusive. /// Whether the value is in range. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsInRange(this double num, double min, double max, bool inclusive = true) { if (min > max) return false; - return inclusive ? (num >= min && num <= max) : (num > min && num < max); + return inclusive ? num >= min && num <= max : num > min && num < max; } /// /// Returns whether supplied character is in any of the following ranges: a-z, A-Z, 0-9. /// /// Character to test. /// Whether the character is in basic alphanumeric character range. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsBasicAlphanumeric(this char c) => (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9'); /// /// Returns whether supplied character is in the 0-9 range. /// /// Character to test. /// Whether the character is in basic numeric digit character range. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsBasicDigit(this char c) => c >= '0' && c <= '9'; /// /// Returns whether supplied character is in the a-z or A-Z range. /// /// Character to test. /// Whether the character is in basic letter character range. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsBasicLetter(this char c) => (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); /// /// Tests whether given string ends with given character. /// /// String to test. /// Character to test for. /// Whether the supplied string ends with supplied character. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool EndsWithCharacter(this string s, char c) => s.Length >= 1 && s[^1] == c; /// /// Tests whether given string starts with given character. /// /// String to test. /// Character to test for. /// Whether the supplied string starts with supplied character. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool StartsWithCharacter(this string s, char c) => s.Length >= 1 && s[0] == c; // https://stackoverflow.com/questions/9545619/a-fast-hash-function-for-string-in-c-sharp // Calls are inlined to call the underlying method directly /// /// Computes a 64-bit Knuth hash from supplied characters. /// /// Characters to compute the hash value from. /// Computer 64-bit Knuth hash. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ulong CalculateKnuthHash(this ReadOnlySpan chars) => Knuth(chars); /// /// Computes a 64-bit Knuth hash from supplied characters. /// /// Characters to compute the hash value from. /// Computer 64-bit Knuth hash. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ulong CalculateKnuthHash(this Span chars) => Knuth(chars); /// /// Computes a 64-bit Knuth hash from supplied characters. /// /// Characters to compute the hash value from. /// Computer 64-bit Knuth hash. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ulong CalculateKnuthHash(this ReadOnlyMemory chars) => Knuth(chars.Span); /// /// Computes a 64-bit Knuth hash from supplied characters. /// /// Characters to compute the hash value from. /// Computer 64-bit Knuth hash. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ulong CalculateKnuthHash(this Memory chars) => Knuth(chars.Span); /// /// Computes a 64-bit Knuth hash from supplied characters. /// /// Characters to compute the hash value from. /// Computer 64-bit Knuth hash. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ulong CalculateKnuthHash(this ArraySegment chars) => Knuth(chars.AsSpan()); /// /// Computes a 64-bit Knuth hash from supplied characters. /// /// Characters to compute the hash value from. /// Computer 64-bit Knuth hash. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ulong CalculateKnuthHash(this char[] chars) => Knuth(chars.AsSpan()); /// /// Computes a 64-bit Knuth hash from supplied characters. /// /// Characters to compute the hash value from. /// Offset in the array to start calculating from. /// Number of characters to compute the hash from. /// Computer 64-bit Knuth hash. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ulong CalculateKnuthHash(this char[] chars, int start, int count) => Knuth(chars.AsSpan(start, count)); /// /// Computes a 64-bit Knuth hash from supplied characters. /// /// Characters to compute the hash value from. /// Computer 64-bit Knuth hash. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ulong CalculateKnuthHash(this string chars) => Knuth(chars.AsSpan()); /// /// Computes a 64-bit Knuth hash from supplied characters. /// /// Characters to compute the hash value from. /// Offset in the array to start calculating from. /// Number of characters to compute the hash from. /// Computer 64-bit Knuth hash. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ulong CalculateKnuthHash(this string chars, int start, int count) => Knuth(chars.AsSpan(start, count)); /// /// Firsts the two or default. /// /// The enumerable. /// A (T first, T second) . internal static (T first, T second) FirstTwoOrDefault(this IEnumerable enumerable) { using var enumerator = enumerable.GetEnumerator(); if (!enumerator.MoveNext()) return (default, default); var first = enumerator.Current; if (!enumerator.MoveNext()) return (first, default); return (first, enumerator.Current); } /// /// Knuths the. /// /// The chars. /// An ulong. private static ulong Knuth(ReadOnlySpan chars) { var hash = 3074457345618258791ul; for (var i = 0; i < chars.Length; i++) hash = (hash + chars[i]) * 3074457345618258799ul; return hash; } } } diff --git a/DisCatSharp.Configuration.Tests/ConfigurationExtensionTests.cs b/DisCatSharp.Configuration.Tests/ConfigurationExtensionTests.cs index fa9b34c93..0796fd4f8 100644 --- a/DisCatSharp.Configuration.Tests/ConfigurationExtensionTests.cs +++ b/DisCatSharp.Configuration.Tests/ConfigurationExtensionTests.cs @@ -1,334 +1,334 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Generic; using System.Linq; using DisCatSharp.Configuration.Models; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Xunit; namespace DisCatSharp.Configuration.Tests { public class ConfigurationExtensionTests { #region Test Classes class SampleClass { public int Amount { get; set; } public string? Email { get; set; } } class ClassWithArray { public int[] Values { get; set; } = { 1, 2, 3, 4, 5 }; public string[] Strings { get; set; } = { "1", "2", "3", "4", "5" }; } class ClassWithEnumerable { public IEnumerable Values { get; set; } = new[] { 1, 2, 3, 4, 5 }; public IEnumerable Strings { get; set; } = new[] { "1", "2", "3", "4", "5" }; } class ClassWithList { - public List Strings { get; set; } = new List + public List Strings { get; set; } = new() { "1", "2", "3", "4", "5" }; - public List Values { get; set; } = new List + public List Values { get; set; } = new() { 1, 2, 3, 4, 5 }; } class SampleClass2 { public TimeSpan Timeout { get; set; } = TimeSpan.FromMinutes(7); public string Name { get; set; } = "Sample"; public string ConstructorValue { get; } public SampleClass2(string value) { this.ConstructorValue = value; } } #endregion private IConfiguration EnumerableTestConfiguration() => new ConfigurationBuilder() .AddJsonFile("enumerable-test.json") .Build(); private IConfiguration HasSectionWithSuffixConfiguration() => new ConfigurationBuilder() .AddJsonFile("section-with-suffix.json") .Build(); private IConfiguration HasSectionNoSuffixConfiguration() => new ConfigurationBuilder() .AddJsonFile("section-no-suffix.json") .Build(); private IConfiguration BasicDiscordConfiguration() => new ConfigurationBuilder() .AddJsonFile("default-discord.json") .Build(); private IConfiguration DiscordIntentsConfig() => new ConfigurationBuilder() .AddJsonFile("intents-discord.json") .Build(); private IConfiguration DiscordHaphazardConfig() => new ConfigurationBuilder() .AddJsonFile("haphazard-discord.json") .Build(); private IConfiguration SampleConfig() => new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { { "Sample:Amount", "200" }, { "Sample:Email", "test@gmail.com" } }) .Build(); private IConfiguration SampleClass2Configuration_Default() => new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { {"Random:Stuff", "Meow"}, {"SampleClass2:Name", "Purfection"} }) .Build(); private IConfiguration SampleClass2Configuration_Change() => new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { { "SampleClass:Timeout", "01:30:00" }, { "SampleClass:NotValid", "Something" } }) .Build(); private IConfiguration SampleClass2EnumerableTest() => new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { { "SampleClass:EnumerableTest", "[\"10\",\"20\",\"30\"]" } }) .Build(); private IConfiguration SampleClass2ArrayTest() => new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { { "SampleClass:ArrayTest", "[\"10\",\"20\",\"30\"]" } }) .Build(); private IConfiguration SampleClass2ListTest() => new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { { "SampleClass:ListTest", "[\"10\",\"20\",\"30\"]" } }) .Build(); [Fact] public void TestExtractDiscordConfig_Intents() { var source = this.DiscordIntentsConfig(); var config = source.ExtractConfig("Discord"); var expected = DiscordIntents.GuildEmojisAndStickers | DiscordIntents.GuildMembers | DiscordIntents.GuildInvites | DiscordIntents.GuildMessageReactions; Assert.Equal(expected, config.Intents); } [Fact] public void TestExtractDiscordConfig_Haphzard() { var source = this.DiscordHaphazardConfig(); var config = source.ExtractConfig("Discord"); var expectedIntents = DiscordIntents.GuildEmojisAndStickers | DiscordIntents.GuildMembers | DiscordIntents.Guilds; Assert.Equal(expectedIntents, config.Intents); Assert.True(config.MobileStatus); Assert.Equal(1000, config.LargeThreshold); Assert.Equal(TimeSpan.FromHours(10), config.HttpTimeout); } [Fact] public void TestExtractDiscordConfig_Default() { var source = this.BasicDiscordConfiguration(); var config = source.ExtractConfig("Discord"); Assert.Equal("1234567890", config.Token); Assert.Equal(TokenType.Bot, config.TokenType); Assert.Equal(LogLevel.Information, config.MinimumLogLevel); Assert.True(config.UseRelativeRatelimit); Assert.Equal("yyyy-MM-dd HH:mm:ss zzz", config.LogTimestampFormat); Assert.Equal(250, config.LargeThreshold); Assert.True(config.AutoReconnect); Assert.Equal(123123, config.ShardId); Assert.Equal(GatewayCompressionLevel.Stream, config.GatewayCompressionLevel); Assert.Equal(1024, config.MessageCacheSize); Assert.Equal(TimeSpan.FromSeconds(20), config.HttpTimeout); Assert.False(config.ReconnectIndefinitely); Assert.True(config.AlwaysCacheMembers); Assert.Equal(DiscordIntents.AllUnprivileged, config.Intents); Assert.False(config.MobileStatus); Assert.False(config.UseCanary); Assert.False(config.AutoRefreshChannelCache); } [Fact] public void TestSection() { var source = this.SampleConfig(); var config = source.ExtractConfig("Sample", null); Assert.Equal(200, config.Amount); Assert.Equal("test@gmail.com", config.Email); } [Fact] public void TestExtractConfig_V2_Default() { var source = this.SampleClass2Configuration_Default(); var config = (SampleClass2) source.ExtractConfig("SampleClass", () => new SampleClass2("Test"), null); Assert.Equal(TimeSpan.FromMinutes(7), config.Timeout); Assert.Equal("Test", config.ConstructorValue); Assert.Equal("Sample", config.Name); } [Fact] public void TestExtractConfig_V2_Change() { var source = this.SampleClass2Configuration_Change(); var config = (SampleClass2) source.ExtractConfig("SampleClass", () => new SampleClass2("Test123"), null); var span = new TimeSpan(0, 1, 30, 0); Assert.Equal(span, config.Timeout); Assert.Equal("Test123", config.ConstructorValue); Assert.Equal("Sample", config.Name); } [Fact] public void TestExtractConfig_V3_Default() { var source = this.SampleClass2Configuration_Default(); var config = (SampleClass2)new ConfigSection(ref source, "SampleClass", null).ExtractConfig(() => new SampleClass2("Meow")); Assert.Equal("Meow", config.ConstructorValue); Assert.Equal(TimeSpan.FromMinutes(7), config.Timeout); Assert.Equal("Sample", config.Name); } [Fact] public void TestExtractConfig_V3_Change() { var source = this.SampleClass2Configuration_Change(); var config = (SampleClass2)new ConfigSection(ref source, "SampleClass", null).ExtractConfig(() => new SampleClass2("Meow")); Assert.Equal("Meow", config.ConstructorValue); var span = new TimeSpan(0, 1, 30, 0); Assert.Equal(span, config.Timeout); Assert.Equal("Sample", config.Name); } [Fact] public void TestExtractConfig_Enumerable() { var source = this.EnumerableTestConfiguration(); var config = (ClassWithEnumerable)new ConfigSection(ref source, "ClassWithEnumerable", null).ExtractConfig(() => new ClassWithEnumerable()); Assert.NotNull(config.Values); Assert.Equal(3, config.Values.Count()); Assert.NotNull(config.Strings); Assert.Equal(3, config.Values.Count()); } [Fact] public void TestExtractConfig_Array() { var source = this.EnumerableTestConfiguration(); var config = (ClassWithArray)new ConfigSection(ref source, "ClassWithArray", null).ExtractConfig(() => new ClassWithArray()); Assert.NotNull(config.Values); Assert.Equal(3, config.Values.Length); Assert.NotNull(config.Strings); Assert.Equal(3, config.Values.Length); } [Fact] public void TestExtractConfig_List() { var source = this.EnumerableTestConfiguration(); var config = (ClassWithList)new ConfigSection(ref source, "ClassWithList", null).ExtractConfig(() => new ClassWithList()); Assert.NotNull(config.Values); Assert.Equal(3, config.Values.Count); Assert.NotNull(config.Strings); Assert.Equal(3, config.Values.Count); } [Fact] public void TestHasSectionWithSuffix() { var source = this.HasSectionWithSuffixConfiguration(); Assert.True(source.HasSection("DiscordConfiguration")); Assert.False(source.HasSection("Discord")); #pragma warning disable 8625 Assert.False(source.HasSection("DiscordConfiguration", null)); #pragma warning restore 8625 } [Fact] public void TestHasSectionNoSuffix() { var source = this.HasSectionNoSuffixConfiguration(); Assert.True(source.HasSection("Discord")); Assert.False(source.HasSection("DiscordConfiguration")); #pragma warning disable 8625 Assert.False(source.HasSection("Discord", null)); #pragma warning restore 8625 } } } diff --git a/DisCatSharp.Configuration/ConfigurationExtensions.cs b/DisCatSharp.Configuration/ConfigurationExtensions.cs index 845b6942a..528f63fd3 100644 --- a/DisCatSharp.Configuration/ConfigurationExtensions.cs +++ b/DisCatSharp.Configuration/ConfigurationExtensions.cs @@ -1,311 +1,311 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections; using System.Linq; using DisCatSharp.Configuration.Models; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; namespace DisCatSharp.Configuration { /// /// The configuration extensions. /// internal static class ConfigurationExtensions { /// /// The factory error message. /// private const string FACTORY_ERROR_MESSAGE = "Require a function which provides a default entity to work with"; /// /// The default root lib. /// public const string DEFAULT_ROOT_LIB = "DisCatSharp"; /// /// The config suffix. /// private const string CONFIG_SUFFIX = "Configuration"; /// /// Easily piece together paths that will work within /// /// (not used - only for adding context based functionality) /// The strings to piece together /// Strings joined together via ':' public static string ConfigPath(this IConfiguration config, params string[] values) => string.Join(":", values); /// /// Skims over the configuration section and only overrides values that are explicitly defined within the config /// /// Instance of config /// Section which contains values for private static void HydrateInstance(ref object config, ConfigSection section) { var props = config.GetType().GetProperties(); foreach (var prop in props) { // Must have a set method for this to work, otherwise continue on if (prop.SetMethod == null) continue; var entry = section.GetValue(prop.Name); object? value = null; if (typeof(string) == prop.PropertyType) { // We do NOT want to override value if nothing was provided if (!string.IsNullOrEmpty(entry)) prop.SetValue(config, entry); continue; } // We need to address collections a bit differently // They can come in the form of "root:section:name" with a string representation OR // "root:section:name:0" <--- this is not detectable when checking the above path if (typeof(IEnumerable).IsAssignableFrom(prop.PropertyType)) { value = string.IsNullOrEmpty(section.GetValue(prop.Name)) ? section.Config .GetSection(section.GetPath(prop.Name)).Get(prop.PropertyType) : Newtonsoft.Json.JsonConvert.DeserializeObject(entry, prop.PropertyType); if (value == null) continue; prop.SetValue(config, value); } // From this point onward we require the 'entry' value to have something useful if (string.IsNullOrEmpty(entry)) continue; try { // Primitive types are simple to convert if (prop.PropertyType.IsPrimitive) value = Convert.ChangeType(entry, prop.PropertyType); else { // The following types require a different approach if (prop.PropertyType.IsEnum) value = Enum.Parse(prop.PropertyType, entry); else if (typeof(TimeSpan) == prop.PropertyType) value = TimeSpan.Parse(entry); else if (typeof(DateTime) == prop.PropertyType) value = DateTime.Parse(entry); else if (typeof(DateTimeOffset) == prop.PropertyType) value = DateTimeOffset.Parse(entry); } // Update value within our config instance prop.SetValue(config, value); } catch (Exception ex) { Console.Error.WriteLine( $"Unable to convert value of '{entry}' to type '{prop.PropertyType.Name}' for prop '{prop.Name}' in config '{config.GetType().Name}'\n\t\t{ex.Message}"); } } } /// /// Instantiate an entity using then walk through the specified /// and translate user-defined config values to the instantiated instance from /// /// Section containing values for targeted config /// Function which generates a default entity /// Hydrated instance of an entity which contains user-defined values (if any) /// When is null public static object ExtractConfig(this ConfigSection section, Func factory) { if (factory == null) throw new ArgumentNullException(nameof(factory), FACTORY_ERROR_MESSAGE); // Create default instance var config = factory(); HydrateInstance(ref config, section); return config; } /// /// Instantiate an entity using then walk through the specified /// in . Translate user-defined config values to the instantiated instance from /// /// Loaded App Configuration /// Name of section to load /// Function which creates a default entity to work with /// (Optional) Used when section is nested within another. Default value is /// Hydrated instance of an entity which contains user-defined values (if any) /// When is null public static object ExtractConfig(this IConfiguration config, string sectionName, Func factory, string? rootSectionName = DEFAULT_ROOT_LIB) { if (factory == null) throw new ArgumentNullException(nameof(factory), FACTORY_ERROR_MESSAGE); // create default instance var instance = factory(); HydrateInstance(ref instance, new ConfigSection(ref config, sectionName, rootSectionName)); return instance; } /// /// Instantiate a new instance of , then walk through the specified /// in . Translate user-defined config values to the instance. /// /// Loaded App Configuration /// /// Name of section to load /// (Optional) Used when section is nested with another. Default value is /// Type of instance that represents /// Hydrated instance of which contains the user-defined values (if any). public static TConfig ExtractConfig(this IConfiguration config, IServiceProvider serviceProvider, string sectionName, string? rootSectionName = DEFAULT_ROOT_LIB) where TConfig : new() { // Default values should hopefully be provided from the constructor var configInstance = ActivatorUtilities.CreateInstance(serviceProvider, typeof(TConfig)); HydrateInstance(ref configInstance, new ConfigSection(ref config, sectionName, rootSectionName)); return (TConfig)configInstance; } /// /// Instantiate a new instance of , then walk through the specified /// in . Translate user-defined config values to the instance. /// /// Loaded App Configuration /// Name of section to load /// (Optional) Used when section is nested with another. Default value is /// Type of instance that represents /// Hydrated instance of which contains the user-defined values (if any). public static TConfig ExtractConfig(this IConfiguration config, string sectionName, string? rootSectionName = DEFAULT_ROOT_LIB) where TConfig : new() { // Default values should hopefully be provided from the constructor object configInstance = new TConfig(); HydrateInstance(ref configInstance, new ConfigSection(ref config, sectionName, rootSectionName)); return (TConfig)configInstance; } /// /// Determines if contains a particular section/object (not value) /// /// /// /// { /// "Discord": { // this is a section/object /// /// }, /// "Value": "something" // this is not a section/object /// } /// /// /// /// /// True if section exists, otherwise false public static bool HasSection(this IConfiguration config, params string[] values) { if (!values.Any()) return false; if (values.Length == 1) return config.GetChildren().Any(x => x.Key == values[0]); if (config.GetChildren().All(x => x.Key != values[0])) return false; var current = config.GetSection(values[0]); for (var i = 1; i < values.Length - 1; i++) { if (current.GetChildren().All(x => x.Key != values[i])) return false; current = current.GetSection(values[i]); } return current.GetChildren().Any(x => x.Key == values[^1]); } /// /// Instantiates an instance of , then consumes any custom /// configuration from user/developer from .
/// View remarks for more info ///
/// /// This is an example of how your JSON structure should look if you wish /// to override one or more of the default values from /// /// { /// "DisCatSharp": { /// "Discord": { } /// } /// } /// ///
/// Alternatively, you can use the type name itself /// /// { /// "DisCatSharp": { /// "DiscordConfiguration": { } /// } /// } /// /// /// { /// "botSectionName": { /// "DiscordConfiguration": { } /// } /// } /// ///
/// /// /// /// Instance of public static DiscordClient BuildClient(this IConfiguration config, IServiceProvider serviceProvider, string botSectionName = DEFAULT_ROOT_LIB) { var section = config.HasSection(botSectionName, "Discord") ? "Discord" : config.HasSection(botSectionName, $"Discord{CONFIG_SUFFIX}") ? $"Discord:{CONFIG_SUFFIX}" : null; return string.IsNullOrEmpty(section) - ? new DiscordClient(new(serviceProvider)) + ? new DiscordClient(new DiscordConfiguration(serviceProvider)) : new DiscordClient(config.ExtractConfig(serviceProvider, section, botSectionName)); } } } diff --git a/DisCatSharp.Configuration/Models/ConfigSection.cs b/DisCatSharp.Configuration/Models/ConfigSection.cs index 6b32a448a..82bdda9b1 100644 --- a/DisCatSharp.Configuration/Models/ConfigSection.cs +++ b/DisCatSharp.Configuration/Models/ConfigSection.cs @@ -1,94 +1,92 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using Microsoft.Extensions.Configuration; namespace DisCatSharp.Configuration.Models { /// /// Represents an object in /// internal readonly struct ConfigSection { /// /// Key within which represents an object containing multiple values /// public string SectionName { get; } /// /// Optional used to indicate this section is nested within another /// public string? Root { get; } /// /// Reference to used within application /// public IConfiguration Config { get; } /// /// Initializes a new instance of the class. /// /// Reference to config /// Section of interest /// (Optional) Indicates is nested within this name. Default value is DisCatSharp public ConfigSection(ref IConfiguration config, string sectionName, string? rootName = "DisCatSharp") { this.Config = config; this.SectionName = sectionName; this.Root = rootName; } /// /// Checks if key exists in /// /// Property / Key to search for in section /// True if key exists, otherwise false. Outputs path to config regardless public bool ContainsKey(string name) { var path = string.IsNullOrEmpty(this.Root) ? this.Config.ConfigPath(this.SectionName, name) : this.Config.ConfigPath(this.Root, this.SectionName, name); return !string.IsNullOrEmpty(this.Config[path]); } /// /// Attempts to get value associated to the config path.
Should be used in unison with ///
/// Config path to value /// Value found at public string GetValue(string propName) => this.Config[this.GetPath(propName)]; /// /// Gets the path. /// /// The value. /// A string. - public string GetPath(string value) - { - return string.IsNullOrEmpty(this.Root) + public string GetPath(string value) => + string.IsNullOrEmpty(this.Root) ? this.Config.ConfigPath(this.SectionName, value) : this.Config.ConfigPath(this.Root, this.SectionName, value); - } } } diff --git a/DisCatSharp.Interactivity/EventHandling/Components/ComponentEventWaiter.cs b/DisCatSharp.Interactivity/EventHandling/Components/ComponentEventWaiter.cs index a5e5d8280..7dc76de59 100644 --- a/DisCatSharp.Interactivity/EventHandling/Components/ComponentEventWaiter.cs +++ b/DisCatSharp.Interactivity/EventHandling/Components/ComponentEventWaiter.cs @@ -1,150 +1,150 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using ConcurrentCollections; using DisCatSharp.Entities; using DisCatSharp.EventArgs; using DisCatSharp.Interactivity.Enums; using Microsoft.Extensions.Logging; namespace DisCatSharp.Interactivity.EventHandling { /// /// A component-based version of /// internal class ComponentEventWaiter : IDisposable { private readonly DiscordClient _client; private readonly ConcurrentHashSet _matchRequests = new(); private readonly ConcurrentHashSet _collectRequests = new(); private readonly DiscordFollowupMessageBuilder _message; private readonly InteractivityConfiguration _config; /// /// Initializes a new instance of the class. /// /// The client. /// The config. public ComponentEventWaiter(DiscordClient client, InteractivityConfiguration config) { this._client = client; this._client.ComponentInteractionCreated += this.Handle; this._config = config; - this._message = new() { Content = config.ResponseMessage ?? "This message was not meant for you.", IsEphemeral = true }; + this._message = new DiscordFollowupMessageBuilder { Content = config.ResponseMessage ?? "This message was not meant for you.", IsEphemeral = true }; } /// /// Waits for a specified 's predicate to be fufilled. /// /// The request to wait for. /// The returned args, or null if it timed out. public async Task WaitForMatchAsync(ComponentMatchRequest request) { this._matchRequests.Add(request); try { return await request.Tcs.Task.ConfigureAwait(false); } catch (Exception e) { this._client.Logger.LogError(InteractivityEvents.InteractivityWaitError, e, "An exception was thrown while waiting for components."); return null; } finally { this._matchRequests.TryRemove(request); } } /// /// Collects reactions and returns the result when the 's cancellation token is canceled. /// /// The request to wait on. /// The result from request's predicate over the period of time leading up to the token's cancellation. public async Task> CollectMatchesAsync(ComponentCollectRequest request) { this._collectRequests.Add(request); try { await request.Tcs.Task.ConfigureAwait(false); } catch (Exception e) { this._client.Logger.LogError(InteractivityEvents.InteractivityCollectorError, e, "There was an error while collecting component event args."); } finally { this._collectRequests.TryRemove(request); } return request.Collected.ToArray(); } /// /// Handles the waiter. /// /// The client. /// The args. private async Task Handle(DiscordClient _, ComponentInteractionCreateEventArgs args) { foreach (var mreq in this._matchRequests.ToArray()) { if (mreq.Message == args.Message && mreq.IsMatch(args)) mreq.Tcs.TrySetResult(args); else if (this._config.ResponseBehavior is InteractionResponseBehavior.Respond) await args.Interaction.CreateFollowupMessageAsync(this._message).ConfigureAwait(false); } foreach (var creq in this._collectRequests.ToArray()) { if (creq.Message == args.Message && creq.IsMatch(args)) { await args.Interaction.CreateResponseAsync(InteractionResponseType.DeferredMessageUpdate).ConfigureAwait(false); if (creq.IsMatch(args)) creq.Collected.Add(args); else if (this._config.ResponseBehavior is InteractionResponseBehavior.Respond) await args.Interaction.CreateFollowupMessageAsync(this._message).ConfigureAwait(false); } } } /// /// Disposes the waiter. /// public void Dispose() { this._matchRequests.Clear(); this._collectRequests.Clear(); this._client.ComponentInteractionCreated -= this.Handle; } } } diff --git a/DisCatSharp.Interactivity/EventHandling/Components/ComponentPaginator.cs b/DisCatSharp.Interactivity/EventHandling/Components/ComponentPaginator.cs index 6e3aa7b06..c9f65d9e8 100644 --- a/DisCatSharp.Interactivity/EventHandling/Components/ComponentPaginator.cs +++ b/DisCatSharp.Interactivity/EventHandling/Components/ComponentPaginator.cs @@ -1,181 +1,181 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Generic; using System.Threading.Tasks; using DisCatSharp.Entities; using DisCatSharp.EventArgs; using DisCatSharp.Interactivity.Enums; using Microsoft.Extensions.Logging; namespace DisCatSharp.Interactivity.EventHandling { /// /// The component paginator. /// internal class ComponentPaginator : IPaginator { private readonly DiscordClient _client; private readonly InteractivityConfiguration _config; private readonly DiscordMessageBuilder _builder = new(); private readonly Dictionary _requests = new(); /// /// Initializes a new instance of the class. /// /// The client. /// The config. public ComponentPaginator(DiscordClient client, InteractivityConfiguration config) { this._client = client; this._client.ComponentInteractionCreated += this.Handle; this._config = config; } /// /// Does the pagination async. /// /// The request. public async Task DoPaginationAsync(IPaginationRequest request) { var id = (await request.GetMessageAsync().ConfigureAwait(false)).Id; this._requests.Add(id, request); try { var tcs = await request.GetTaskCompletionSourceAsync().ConfigureAwait(false); await tcs.Task.ConfigureAwait(false); } catch (Exception ex) { this._client.Logger.LogError(InteractivityEvents.InteractivityPaginationError, ex, "There was an exception while paginating."); } finally { this._requests.Remove(id); try { await request.DoCleanupAsync().ConfigureAwait(false); } catch (Exception ex) { this._client.Logger.LogError(InteractivityEvents.InteractivityPaginationError, ex, "There was an exception while cleaning up pagination."); } } } /// /// Disposes the paginator. /// public void Dispose() => this._client.ComponentInteractionCreated -= this.Handle; /// /// Handles the pagination event. /// /// The client. /// The event arguments. private async Task Handle(DiscordClient _, ComponentInteractionCreateEventArgs e) { if (e.Interaction.Type == InteractionType.ModalSubmit) return; if (!this._requests.TryGetValue(e.Message.Id, out var req)) return; if (this._config.AckPaginationButtons) { e.Handled = true; await e.Interaction.CreateResponseAsync(InteractionResponseType.DeferredMessageUpdate).ConfigureAwait(false); } if (await req.GetUserAsync().ConfigureAwait(false) != e.User) { if (this._config.ResponseBehavior is InteractionResponseBehavior.Respond) - await e.Interaction.CreateFollowupMessageAsync(new() { Content = this._config.ResponseMessage, IsEphemeral = true }).ConfigureAwait(false); + 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.Interactivity/EventHandling/Components/ModalEventWaiter.cs b/DisCatSharp.Interactivity/EventHandling/Components/ModalEventWaiter.cs index 02bd1c09a..16dab8bff 100644 --- a/DisCatSharp.Interactivity/EventHandling/Components/ModalEventWaiter.cs +++ b/DisCatSharp.Interactivity/EventHandling/Components/ModalEventWaiter.cs @@ -1,109 +1,109 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Linq; using System.Threading.Tasks; using ConcurrentCollections; using DisCatSharp.Entities; using DisCatSharp.EventArgs; using DisCatSharp.Interactivity.Enums; using Microsoft.Extensions.Logging; namespace DisCatSharp.Interactivity.EventHandling { /// /// A modal-based version of /// internal class ModalEventWaiter : IDisposable { private readonly DiscordClient _client; private readonly ConcurrentHashSet _modalMatchRequests = new(); private readonly DiscordFollowupMessageBuilder _message; private readonly InteractivityConfiguration _config; /// /// Initializes a new instance of the class. /// /// The client. /// The config. public ModalEventWaiter(DiscordClient client, InteractivityConfiguration config) { this._client = client; this._client.ComponentInteractionCreated += this.Handle; this._config = config; - this._message = new() { Content = config.ResponseMessage ?? "This modal was not meant for you.", IsEphemeral = true }; + this._message = new DiscordFollowupMessageBuilder { Content = config.ResponseMessage ?? "This modal was not meant for you.", IsEphemeral = true }; } /// /// Waits for a specified 's predicate to be fufilled. /// /// The request to wait for. /// The returned args, or null if it timed out. public async Task WaitForModalMatchAsync(ModalMatchRequest request) { this._modalMatchRequests.Add(request); try { return await request.Tcs.Task.ConfigureAwait(false); } catch (Exception e) { this._client.Logger.LogError(InteractivityEvents.InteractivityWaitError, e, "An exception was thrown while waiting for modals."); return null; } finally { this._modalMatchRequests.TryRemove(request); } } /// /// Handles the waiter. /// /// The client. /// The args. private async Task Handle(DiscordClient _, ComponentInteractionCreateEventArgs args) { foreach (var mreq in this._modalMatchRequests.ToArray()) { if (mreq.CustomId == args.Interaction.Data.CustomId && mreq.IsMatch(args)) mreq.Tcs.TrySetResult(args); else if (this._config.ResponseBehavior is InteractionResponseBehavior.Respond) await args.Interaction.CreateFollowupMessageAsync(this._message).ConfigureAwait(false); } } /// /// Disposes the waiter. /// public void Dispose() { this._modalMatchRequests.Clear(); this._client.ComponentInteractionCreated -= this.Handle; } } } diff --git a/DisCatSharp.Interactivity/EventHandling/Components/PaginationButtons.cs b/DisCatSharp.Interactivity/EventHandling/Components/PaginationButtons.cs index 80284bdd4..97b2cb88e 100644 --- a/DisCatSharp.Interactivity/EventHandling/Components/PaginationButtons.cs +++ b/DisCatSharp.Interactivity/EventHandling/Components/PaginationButtons.cs @@ -1,95 +1,95 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using DisCatSharp.Entities; using DisCatSharp.Enums; namespace DisCatSharp.Interactivity.EventHandling { /// /// The pagination buttons. /// public class PaginationButtons { /// /// Gets or sets the skip left button. /// public DiscordButtonComponent SkipLeft { internal get; set; } /// /// Gets or sets the left button. /// public DiscordButtonComponent Left { internal get; set; } /// /// Gets or sets the stop button. /// public DiscordButtonComponent Stop { internal get; set; } /// /// Gets or sets the right button. /// public DiscordButtonComponent Right { internal get; set; } /// /// Gets or sets the skip right button. /// public DiscordButtonComponent SkipRight { internal get; set; } /// /// Gets the button array. /// internal DiscordButtonComponent[] ButtonArray => new[] { this.SkipLeft, this.Left, this.Stop, this.Right, this.SkipRight }; /// /// Initializes a new instance of the class. /// public PaginationButtons() { - this.SkipLeft = new(ButtonStyle.Secondary, "leftskip", null, false, new(DiscordEmoji.FromUnicode("⏮"))); - this.Left = new(ButtonStyle.Secondary, "left", null, false, new(DiscordEmoji.FromUnicode("◀"))); - this.Stop = new(ButtonStyle.Secondary, "stop", null, false, new(DiscordEmoji.FromUnicode("⏹"))); - this.Right = new(ButtonStyle.Secondary, "right", null, false, new(DiscordEmoji.FromUnicode("▶"))); - this.SkipRight = new(ButtonStyle.Secondary, "rightskip", null, false, new(DiscordEmoji.FromUnicode("⏭"))); + this.SkipLeft = new DiscordButtonComponent(ButtonStyle.Secondary, "leftskip", null, false, new DiscordComponentEmoji(DiscordEmoji.FromUnicode("⏮"))); + this.Left = new DiscordButtonComponent(ButtonStyle.Secondary, "left", null, false, new DiscordComponentEmoji(DiscordEmoji.FromUnicode("◀"))); + this.Stop = new DiscordButtonComponent(ButtonStyle.Secondary, "stop", null, false, new DiscordComponentEmoji(DiscordEmoji.FromUnicode("⏹"))); + this.Right = new DiscordButtonComponent(ButtonStyle.Secondary, "right", null, false, new DiscordComponentEmoji(DiscordEmoji.FromUnicode("▶"))); + this.SkipRight = new DiscordButtonComponent(ButtonStyle.Secondary, "rightskip", null, false, new DiscordComponentEmoji(DiscordEmoji.FromUnicode("⏭"))); } /// /// Initializes a new instance of the class. /// /// The other . public PaginationButtons(PaginationButtons other) { - this.Stop = new(other.Stop); - this.Left = new(other.Left); - this.Right = new(other.Right); - this.SkipLeft = new(other.SkipLeft); - this.SkipRight = new(other.SkipRight); + this.Stop = new DiscordButtonComponent(other.Stop); + this.Left = new DiscordButtonComponent(other.Left); + this.Right = new DiscordButtonComponent(other.Right); + this.SkipLeft = new DiscordButtonComponent(other.SkipLeft); + this.SkipRight = new DiscordButtonComponent(other.SkipRight); } } } diff --git a/DisCatSharp.Interactivity/EventHandling/Components/Requests/ButtonPaginationRequest.cs b/DisCatSharp.Interactivity/EventHandling/Components/Requests/ButtonPaginationRequest.cs index 1b9207f81..3774b5ba7 100644 --- a/DisCatSharp.Interactivity/EventHandling/Components/Requests/ButtonPaginationRequest.cs +++ b/DisCatSharp.Interactivity/EventHandling/Components/Requests/ButtonPaginationRequest.cs @@ -1,244 +1,244 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using DisCatSharp.Entities; using DisCatSharp.Interactivity.Enums; namespace DisCatSharp.Interactivity.EventHandling { /// /// The button pagination request. /// internal class ButtonPaginationRequest : IPaginationRequest { private int _index; private readonly List _pages = new(); private readonly TaskCompletionSource _tcs = new(); private readonly CancellationToken _token; private readonly DiscordUser _user; private readonly DiscordMessage _message; private readonly PaginationButtons _buttons; private readonly PaginationBehaviour _wrapBehavior; private readonly ButtonPaginationBehavior _behaviorBehavior; /// /// Initializes a new instance of the class. /// /// The message. /// The user. /// The behavior. /// The button behavior. /// The buttons. /// The pages. /// The token. public ButtonPaginationRequest(DiscordMessage message, DiscordUser user, PaginationBehaviour behavior, ButtonPaginationBehavior buttonBehavior, PaginationButtons buttons, IEnumerable pages, CancellationToken token) { this._user = user; this._token = token; - this._buttons = new(buttons); + this._buttons = new PaginationButtons(buttons); this._message = message; this._wrapBehavior = behavior; this._behaviorBehavior = buttonBehavior; this._pages.AddRange(pages); this._token.Register(() => this._tcs.TrySetResult(false)); } /// /// Gets the page count. /// public int PageCount => this._pages.Count; /// /// Gets the page. /// public Task GetPageAsync() { var page = Task.FromResult(this._pages[this._index]); if (this.PageCount is 1) { this._buttons.SkipLeft.Disable(); this._buttons.Left.Disable(); this._buttons.Right.Disable(); this._buttons.SkipRight.Disable(); this._buttons.Stop.Enable(); return page; } if (this._wrapBehavior is PaginationBehaviour.WrapAround) return page; this._buttons.SkipLeft.Disabled = this._index < 2; this._buttons.Left.Disabled = this._index < 1; this._buttons.Right.Disabled = this._index >= this.PageCount - 1; this._buttons.SkipRight.Disabled = this._index >= this.PageCount - 2; return page; } /// /// Skips the left. /// public Task SkipLeftAsync() { if (this._wrapBehavior is PaginationBehaviour.WrapAround) { this._index = this._index is 0 ? this._pages.Count - 1 : 0; return Task.CompletedTask; } this._index = 0; return Task.CompletedTask; } /// /// Skips the right. /// public Task SkipRightAsync() { if (this._wrapBehavior is PaginationBehaviour.WrapAround) { this._index = this._index == this.PageCount - 1 ? 0 : this.PageCount - 1; return Task.CompletedTask; } this._index = this._pages.Count - 1; return Task.CompletedTask; } /// /// Gets the next page. /// public Task NextPageAsync() { this._index++; if (this._wrapBehavior is PaginationBehaviour.WrapAround) { if (this._index >= this.PageCount) this._index = 0; return Task.CompletedTask; } this._index = Math.Min(this._index, this.PageCount - 1); return Task.CompletedTask; } /// /// Gets the previous page. /// public Task PreviousPageAsync() { this._index--; if (this._wrapBehavior is PaginationBehaviour.WrapAround) { if (this._index is -1) this._index = this._pages.Count - 1; return Task.CompletedTask; } this._index = Math.Max(this._index, 0); return Task.CompletedTask; } /// /// Gets the emojis. /// public Task GetEmojisAsync() => Task.FromException(new NotSupportedException("Emojis aren't supported for this request.")); /// /// Gets the buttons. /// public Task> GetButtonsAsync() => Task.FromResult((IEnumerable)this._buttons.ButtonArray); /// /// Gets the message. /// public Task GetMessageAsync() => Task.FromResult(this._message); /// /// Gets the user. /// public Task GetUserAsync() => Task.FromResult(this._user); /// /// Gets the task completion source. /// public Task> GetTaskCompletionSourceAsync() => Task.FromResult(this._tcs); /// /// Does the cleanup. /// public async Task DoCleanupAsync() { switch (this._behaviorBehavior) { case ButtonPaginationBehavior.Disable: var buttons = this._buttons.ButtonArray.Select(b => b.Disable()); var builder = new DiscordMessageBuilder() .WithContent(this._pages[this._index].Content) .AddEmbed(this._pages[this._index].Embed) .AddComponents(buttons); await builder.ModifyAsync(this._message).ConfigureAwait(false); break; case ButtonPaginationBehavior.DeleteButtons: builder = new DiscordMessageBuilder() .WithContent(this._pages[this._index].Content) .AddEmbed(this._pages[this._index].Embed); await builder.ModifyAsync(this._message).ConfigureAwait(false); break; case ButtonPaginationBehavior.DeleteMessage: await this._message.DeleteAsync().ConfigureAwait(false); break; case ButtonPaginationBehavior.Ignore: break; } } } } diff --git a/DisCatSharp.Interactivity/EventHandling/Components/Requests/InteractionPaginationRequest.cs b/DisCatSharp.Interactivity/EventHandling/Components/Requests/InteractionPaginationRequest.cs index b524955ed..ab878c81c 100644 --- a/DisCatSharp.Interactivity/EventHandling/Components/Requests/InteractionPaginationRequest.cs +++ b/DisCatSharp.Interactivity/EventHandling/Components/Requests/InteractionPaginationRequest.cs @@ -1,264 +1,264 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using DisCatSharp.Entities; using DisCatSharp.Interactivity.Enums; namespace DisCatSharp.Interactivity.EventHandling { /// /// The interaction pagination request. /// internal class InteractionPaginationRequest : IPaginationRequest { private int _index; private readonly List _pages = new(); private readonly TaskCompletionSource _tcs = new(); private DiscordInteraction _lastInteraction; private CancellationTokenSource _interactionCts; private readonly CancellationToken _token; private readonly DiscordUser _user; private readonly DiscordMessage _message; private readonly PaginationButtons _buttons; private readonly PaginationBehaviour _wrapBehavior; private readonly ButtonPaginationBehavior _behaviorBehavior; /// /// Initializes a new instance of the class. /// /// The interaction. /// The message. /// The user. /// The behavior. /// The behavior behavior. /// The buttons. /// The pages. /// The token. public InteractionPaginationRequest(DiscordInteraction interaction, DiscordMessage message, DiscordUser user, PaginationBehaviour behavior, ButtonPaginationBehavior behaviorBehavior, PaginationButtons buttons, IEnumerable pages, CancellationToken token) { this._user = user; this._token = token; - this._buttons = new(buttons); + this._buttons = new PaginationButtons(buttons); this._message = message; this._wrapBehavior = behavior; this._behaviorBehavior = behaviorBehavior; this._pages.AddRange(pages); this.RegenerateCts(interaction); this._token.Register(() => this._tcs.TrySetResult(false)); } /// /// Gets the page count. /// public int PageCount => this._pages.Count; /// /// Regenerates the cts. /// /// The interaction. internal void RegenerateCts(DiscordInteraction interaction) { this._interactionCts?.Dispose(); this._lastInteraction = interaction; - this._interactionCts = new(TimeSpan.FromSeconds((60 * 15) - 5)); + this._interactionCts = new CancellationTokenSource(TimeSpan.FromSeconds((60 * 15) - 5)); this._interactionCts.Token.Register(() => this._tcs.TrySetResult(false)); } /// /// Gets the page. /// public Task GetPageAsync() { var page = Task.FromResult(this._pages[this._index]); if (this.PageCount is 1) { this._buttons.ButtonArray.Select(b => b.Disable()); this._buttons.Stop.Enable(); return page; } if (this._wrapBehavior is PaginationBehaviour.WrapAround) return page; this._buttons.SkipLeft.Disabled = this._index < 2; this._buttons.Left.Disabled = this._index < 1; this._buttons.Right.Disabled = this._index == this.PageCount - 1; this._buttons.SkipRight.Disabled = this._index >= this.PageCount - 2; return page; } /// /// Skips the left page. /// public Task SkipLeftAsync() { if (this._wrapBehavior is PaginationBehaviour.WrapAround) { this._index = this._index is 0 ? this._pages.Count - 1 : 0; return Task.CompletedTask; } this._index = 0; return Task.CompletedTask; } /// /// Skips the right page. /// public Task SkipRightAsync() { if (this._wrapBehavior is PaginationBehaviour.WrapAround) { this._index = this._index == this.PageCount - 1 ? 0 : this.PageCount - 1; return Task.CompletedTask; } this._index = this._pages.Count - 1; return Task.CompletedTask; } /// /// Gets the next page. /// /// A Task. public Task NextPageAsync() { this._index++; if (this._wrapBehavior is PaginationBehaviour.WrapAround) { if (this._index >= this.PageCount) this._index = 0; return Task.CompletedTask; } this._index = Math.Min(this._index, this.PageCount - 1); return Task.CompletedTask; } /// /// Gets the previous page. /// public Task PreviousPageAsync() { this._index--; if (this._wrapBehavior is PaginationBehaviour.WrapAround) { if (this._index is -1) this._index = this._pages.Count - 1; return Task.CompletedTask; } this._index = Math.Max(this._index, 0); return Task.CompletedTask; } /// /// Gets the emojis. /// public Task GetEmojisAsync() => Task.FromException(new NotSupportedException("Emojis aren't supported for this request.")); /// /// Gets the buttons. /// public Task> GetButtonsAsync() => Task.FromResult((IEnumerable)this._buttons.ButtonArray); /// /// Gets the message. /// public Task GetMessageAsync() => Task.FromResult(this._message); /// /// Gets the user. /// public Task GetUserAsync() => Task.FromResult(this._user); /// /// Gets the task completion source. /// public Task> GetTaskCompletionSourceAsync() => Task.FromResult(this._tcs); /// /// Cleanup. /// public async Task DoCleanupAsync() { switch (this._behaviorBehavior) { case ButtonPaginationBehavior.Disable: var buttons = this._buttons.ButtonArray .Select(b => new DiscordButtonComponent(b)) .Select(b => b.Disable()); var builder = new DiscordWebhookBuilder() .WithContent(this._pages[this._index].Content) .AddEmbed(this._pages[this._index].Embed) .AddComponents(buttons); await this._lastInteraction.EditOriginalResponseAsync(builder).ConfigureAwait(false); break; case ButtonPaginationBehavior.DeleteButtons: builder = new DiscordWebhookBuilder() .WithContent(this._pages[this._index].Content) .AddEmbed(this._pages[this._index].Embed); await this._lastInteraction.EditOriginalResponseAsync(builder).ConfigureAwait(false); break; case ButtonPaginationBehavior.DeleteMessage: await this._lastInteraction.DeleteOriginalResponseAsync().ConfigureAwait(false); break; case ButtonPaginationBehavior.Ignore: break; } } } } diff --git a/DisCatSharp.Interactivity/EventHandling/EventWaiter.cs b/DisCatSharp.Interactivity/EventHandling/EventWaiter.cs index 663f3e6a0..7cf72f30e 100644 --- a/DisCatSharp.Interactivity/EventHandling/EventWaiter.cs +++ b/DisCatSharp.Interactivity/EventHandling/EventWaiter.cs @@ -1,171 +1,171 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Reflection; using System.Threading.Tasks; using ConcurrentCollections; using DisCatSharp.Common.Utilities; using Microsoft.Extensions.Logging; namespace DisCatSharp.Interactivity.EventHandling { /// /// Eventwaiter is a class that serves as a layer between the InteractivityExtension /// and the DiscordClient to listen to an event and check for matches to a predicate. /// /// internal class EventWaiter : IDisposable where T : AsyncEventArgs { DiscordClient _client; AsyncEvent _event; AsyncEventHandler _handler; ConcurrentHashSet> _matchrequests; ConcurrentHashSet> _collectrequests; - bool _disposed = false; + bool _disposed; /// /// Creates a new Eventwaiter object. /// /// Your DiscordClient public EventWaiter(DiscordClient client) { this._client = client; var tinfo = this._client.GetType().GetTypeInfo(); var handler = tinfo.DeclaredFields.First(x => x.FieldType == typeof(AsyncEvent)); this._matchrequests = new ConcurrentHashSet>(); this._collectrequests = new ConcurrentHashSet>(); this._event = (AsyncEvent)handler.GetValue(this._client); this._handler = new AsyncEventHandler(this.HandleEvent); this._event.Register(this._handler); } /// /// Waits for a match to a specific request, else returns null. /// /// Request to match /// public async Task WaitForMatchAsync(MatchRequest request) { T result = null; this._matchrequests.Add(request); try { result = await request.Tcs.Task.ConfigureAwait(false); } catch (Exception ex) { this._client.Logger.LogError(InteractivityEvents.InteractivityWaitError, ex, "An exception occurred while waiting for {0}", typeof(T).Name); } finally { request.Dispose(); this._matchrequests.TryRemove(request); } return result; } /// /// Collects the matches async. /// /// The request. public async Task> CollectMatchesAsync(CollectRequest request) { ReadOnlyCollection result = null; this._collectrequests.Add(request); try { await request.Tcs.Task.ConfigureAwait(false); } catch (Exception ex) { this._client.Logger.LogError(InteractivityEvents.InteractivityWaitError, ex, "An exception occurred while collecting from {0}", typeof(T).Name); } finally { result = new ReadOnlyCollection(new HashSet(request.Collected).ToList()); request.Dispose(); this._collectrequests.TryRemove(request); } return result; } /// /// Handles the event. /// /// The client. /// The event args. private Task HandleEvent(DiscordClient client, T eventargs) { if (!this._disposed) { foreach (var req in this._matchrequests) { if (req.Predicate(eventargs)) { req.Tcs.TrySetResult(eventargs); } } foreach (var req in this._collectrequests) { if (req.Predicate(eventargs)) { req.Collected.Add(eventargs); } } } return Task.CompletedTask; } ~EventWaiter() { this.Dispose(); } /// /// Disposes this EventWaiter /// public void Dispose() { this._disposed = true; if (this._event != null) this._event.Unregister(this._handler); this._event = null; this._handler = null; this._client = null; if (this._matchrequests != null) this._matchrequests.Clear(); if (this._collectrequests != null) this._collectrequests.Clear(); this._matchrequests = null; this._collectrequests = null; } } } diff --git a/DisCatSharp.Interactivity/EventHandling/Requests/PaginationRequest.cs b/DisCatSharp.Interactivity/EventHandling/Requests/PaginationRequest.cs index a8abc561e..f1d3532fb 100644 --- a/DisCatSharp.Interactivity/EventHandling/Requests/PaginationRequest.cs +++ b/DisCatSharp.Interactivity/EventHandling/Requests/PaginationRequest.cs @@ -1,319 +1,319 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using DisCatSharp.Entities; using DisCatSharp.Interactivity.Enums; namespace DisCatSharp.Interactivity.EventHandling { /// /// The pagination request. /// internal class PaginationRequest : IPaginationRequest { private TaskCompletionSource _tcs; private readonly CancellationTokenSource _ct; private readonly TimeSpan _timeout; private readonly List _pages; private readonly PaginationBehaviour _behaviour; private readonly DiscordMessage _message; private readonly PaginationEmojis _emojis; private readonly DiscordUser _user; - private int _index = 0; + private int _index; /// /// Creates a new Pagination request /// /// Message to paginate /// User to allow control for /// Behaviour during pagination /// Behavior on pagination end /// Emojis for this pagination object /// Timeout time /// Pagination pages internal PaginationRequest(DiscordMessage message, DiscordUser user, PaginationBehaviour behaviour, PaginationDeletion deletion, PaginationEmojis emojis, TimeSpan timeout, params Page[] pages) { - this._tcs = new(); - this._ct = new(timeout); + this._tcs = new TaskCompletionSource(); + this._ct = new CancellationTokenSource(timeout); this._ct.Token.Register(() => this._tcs.TrySetResult(true)); this._timeout = timeout; this._message = message; this._user = user; this.PaginationDeletion = deletion; this._behaviour = behaviour; this._emojis = emojis; this._pages = new List(); foreach (var p in pages) { this._pages.Add(p); } } /// /// Gets the page count. /// public int PageCount => this._pages.Count; /// /// Gets the pagination deletion. /// public PaginationDeletion PaginationDeletion { get; } /// /// Gets the page async. /// /// A Task. public async Task GetPageAsync() { await Task.Yield(); return this._pages[this._index]; } /// /// Skips the left async. /// /// A Task. public async Task SkipLeftAsync() { await Task.Yield(); this._index = 0; } /// /// Skips the right async. /// /// A Task. public async Task SkipRightAsync() { await Task.Yield(); this._index = this._pages.Count - 1; } /// /// Nexts the page async. /// /// A Task. public async Task NextPageAsync() { await Task.Yield(); switch (this._behaviour) { case PaginationBehaviour.Ignore: if (this._index == this._pages.Count - 1) break; else this._index++; break; case PaginationBehaviour.WrapAround: if (this._index == this._pages.Count - 1) this._index = 0; else this._index++; break; } } /// /// Previous the page async. /// /// A Task. public async Task PreviousPageAsync() { await Task.Yield(); switch (this._behaviour) { case PaginationBehaviour.Ignore: if (this._index == 0) break; else this._index--; break; case PaginationBehaviour.WrapAround: if (this._index == 0) this._index = this._pages.Count - 1; else this._index--; break; } } /// /// Gets the buttons async. /// /// #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously public async Task> GetButtonsAsync() #pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously => throw new NotSupportedException("This request does not support buttons."); /// /// Gets the emojis async. /// /// A Task. public async Task GetEmojisAsync() { await Task.Yield(); return this._emojis; } /// /// Gets the message async. /// /// A Task. public async Task GetMessageAsync() { await Task.Yield(); return this._message; } /// /// Gets the user async. /// /// A Task. public async Task GetUserAsync() { await Task.Yield(); return this._user; } /// /// Dos the cleanup async. /// /// A Task. public async Task DoCleanupAsync() { switch (this.PaginationDeletion) { case PaginationDeletion.DeleteEmojis: await this._message.DeleteAllReactionsAsync().ConfigureAwait(false); break; case PaginationDeletion.DeleteMessage: await this._message.DeleteAsync().ConfigureAwait(false); break; case PaginationDeletion.KeepEmojis: break; } } /// /// Gets the task completion source async. /// /// A Task. public async Task> GetTaskCompletionSourceAsync() { await Task.Yield(); return this._tcs; } ~PaginationRequest() { this.Dispose(); } /// /// Disposes this PaginationRequest. /// public void Dispose() { this._ct.Dispose(); this._tcs = null; } } } namespace DisCatSharp.Interactivity { /// /// The pagination emojis. /// public class PaginationEmojis { public DiscordEmoji SkipLeft; public DiscordEmoji SkipRight; public DiscordEmoji Left; public DiscordEmoji Right; public DiscordEmoji Stop; /// /// Initializes a new instance of the class. /// public PaginationEmojis() { this.Left = DiscordEmoji.FromUnicode("◀"); this.Right = DiscordEmoji.FromUnicode("▶"); this.SkipLeft = DiscordEmoji.FromUnicode("⏮"); this.SkipRight = DiscordEmoji.FromUnicode("⏭"); this.Stop = DiscordEmoji.FromUnicode("⏹"); } } /// /// The page. /// public class Page { /// /// Gets or sets the content. /// public string Content { get; set; } /// /// Gets or sets the embed. /// public DiscordEmbed Embed { get; set; } /// /// Initializes a new instance of the class. /// /// The content. /// The embed. public Page(string content = "", DiscordEmbedBuilder embed = null) { this.Content = content; this.Embed = embed?.Build(); } } } diff --git a/DisCatSharp.Interactivity/InteractivityConfiguration.cs b/DisCatSharp.Interactivity/InteractivityConfiguration.cs index 67c3f7324..58d67df39 100644 --- a/DisCatSharp.Interactivity/InteractivityConfiguration.cs +++ b/DisCatSharp.Interactivity/InteractivityConfiguration.cs @@ -1,115 +1,115 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using DisCatSharp.Interactivity.Enums; using DisCatSharp.Interactivity.EventHandling; using Microsoft.Extensions.DependencyInjection; namespace DisCatSharp.Interactivity { /// /// Configuration class for your Interactivity extension /// public sealed class InteractivityConfiguration { /// /// Sets the default interactivity action timeout. /// Defaults to 1 minute. /// public TimeSpan Timeout { internal get; set; } = TimeSpan.FromMinutes(1); /// /// What to do after the poll ends /// public PollBehaviour PollBehaviour { internal get; set; } = PollBehaviour.DeleteEmojis; /// /// Emojis to use for pagination /// public PaginationEmojis PaginationEmojis { internal get; set; } = new(); /// /// Buttons to use for pagination. /// public PaginationButtons PaginationButtons { internal get; set; } = new(); /// /// Whether interactivity should ACK buttons that are pushed. Setting this to will also prevent subsequent event handlers from running. /// public bool AckPaginationButtons { internal get; set; } /// /// How to handle buttons after pagination ends. /// - public ButtonPaginationBehavior ButtonBehavior { internal get; set; } = new(); + public ButtonPaginationBehavior ButtonBehavior { internal get; set; } /// /// How to handle pagination. Defaults to WrapAround. /// public PaginationBehaviour PaginationBehaviour { internal get; set; } = PaginationBehaviour.WrapAround; /// /// How to handle pagination deletion. Defaults to DeleteEmojis. /// public PaginationDeletion PaginationDeletion { internal get; set; } = PaginationDeletion.DeleteEmojis; /// /// How to handle invalid interactions. Defaults to Ignore. /// public InteractionResponseBehavior ResponseBehavior { internal get; set; } = InteractionResponseBehavior.Ignore; /// /// The message to send to the user when processing invalid interactions. Ignored if is not set to . /// public string ResponseMessage { internal get; set; } /// /// Creates a new instance of . /// [ActivatorUtilitiesConstructor] public InteractivityConfiguration() { } /// /// Creates a new instance of , copying the properties of another configuration. /// /// Configuration the properties of which are to be copied. public InteractivityConfiguration(InteractivityConfiguration other) { this.AckPaginationButtons = other.AckPaginationButtons; this.PaginationButtons = other.PaginationButtons; this.ButtonBehavior = other.ButtonBehavior; this.PaginationBehaviour = other.PaginationBehaviour; this.PaginationDeletion = other.PaginationDeletion; this.ResponseBehavior = other.ResponseBehavior; this.PaginationEmojis = other.PaginationEmojis; this.ResponseMessage = other.ResponseMessage; this.PollBehaviour = other.PollBehaviour; this.Timeout = other.Timeout; if (this.ResponseBehavior is InteractionResponseBehavior.Respond && string.IsNullOrWhiteSpace(this.ResponseMessage)) throw new ArgumentException($"{nameof(this.ResponseMessage)} cannot be null, empty, or whitespace when {nameof(this.ResponseBehavior)} is set to respond."); } } } diff --git a/DisCatSharp.Interactivity/InteractivityEvents.cs b/DisCatSharp.Interactivity/InteractivityEvents.cs index de5365b96..d20c87cef 100644 --- a/DisCatSharp.Interactivity/InteractivityEvents.cs +++ b/DisCatSharp.Interactivity/InteractivityEvents.cs @@ -1,57 +1,57 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using Microsoft.Extensions.Logging; namespace DisCatSharp.Interactivity { /// /// Contains well-defined event IDs used by the Interactivity extension. /// public static class InteractivityEvents { /// /// Miscellaneous events, that do not fit in any other category. /// - public static EventId Misc { get; } = new EventId(500, "Interactivity"); + public static EventId Misc { get; } = new(500, "Interactivity"); /// /// Events pertaining to errors that happen during waiting for events. /// - public static EventId InteractivityWaitError { get; } = new EventId(501, nameof(InteractivityWaitError)); + public static EventId InteractivityWaitError { get; } = new(501, nameof(InteractivityWaitError)); /// /// Events pertaining to pagination. /// - public static EventId InteractivityPaginationError { get; } = new EventId(502, nameof(InteractivityPaginationError)); + public static EventId InteractivityPaginationError { get; } = new(502, nameof(InteractivityPaginationError)); /// /// Events pertaining to polling. /// - public static EventId InteractivityPollError { get; } = new EventId(503, nameof(InteractivityPollError)); + public static EventId InteractivityPollError { get; } = new(503, nameof(InteractivityPollError)); /// /// Events pertaining to event collection. /// - public static EventId InteractivityCollectorError { get; } = new EventId(504, nameof(InteractivityCollectorError)); + public static EventId InteractivityCollectorError { get; } = new(504, nameof(InteractivityCollectorError)); } } diff --git a/DisCatSharp.Interactivity/InteractivityExtension.cs b/DisCatSharp.Interactivity/InteractivityExtension.cs index bff60ea85..79c129736 100644 --- a/DisCatSharp.Interactivity/InteractivityExtension.cs +++ b/DisCatSharp.Interactivity/InteractivityExtension.cs @@ -1,958 +1,958 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Threading; using System.Threading.Tasks; using DisCatSharp.Common.Utilities; using DisCatSharp.Entities; using DisCatSharp.Enums; using DisCatSharp.EventArgs; using DisCatSharp.Interactivity.Enums; using DisCatSharp.Interactivity.EventHandling; namespace DisCatSharp.Interactivity { /// /// Extension class for DisCatSharp.Interactivity /// public class InteractivityExtension : BaseExtension { /// /// Gets the config. /// internal InteractivityConfiguration Config { get; } private EventWaiter _messageCreatedWaiter; private EventWaiter _messageReactionAddWaiter; private EventWaiter _typingStartWaiter; private EventWaiter _modalInteractionWaiter; private EventWaiter _componentInteractionWaiter; private ComponentEventWaiter _componentEventWaiter; private ModalEventWaiter _modalEventWaiter; private ReactionCollector _reactionCollector; private Poller _poller; private Paginator _paginator; private ComponentPaginator _compPaginator; /// /// Initializes a new instance of the class. /// /// The configuration. internal InteractivityExtension(InteractivityConfiguration cfg) { this.Config = new InteractivityConfiguration(cfg); } /// /// Setups the Interactivity Extension. /// /// Discord client. protected internal override void Setup(DiscordClient client) { this.Client = client; this._messageCreatedWaiter = new EventWaiter(this.Client); this._messageReactionAddWaiter = new EventWaiter(this.Client); this._componentInteractionWaiter = new EventWaiter(this.Client); this._modalInteractionWaiter = new EventWaiter(this.Client); this._typingStartWaiter = new EventWaiter(this.Client); this._poller = new Poller(this.Client); this._reactionCollector = new ReactionCollector(this.Client); this._paginator = new Paginator(this.Client); - this._compPaginator = new(this.Client, this.Config); - this._componentEventWaiter = new(this.Client, this.Config); - this._modalEventWaiter = new(this.Client, this.Config); + this._compPaginator = new ComponentPaginator(this.Client, this.Config); + this._componentEventWaiter = new ComponentEventWaiter(this.Client, this.Config); + this._modalEventWaiter = new ModalEventWaiter(this.Client, this.Config); } /// /// Makes a poll and returns poll results. /// /// Message to create poll on. /// Emojis to use for this poll. /// What to do when the poll ends. /// override timeout period. /// public async Task> DoPollAsync(DiscordMessage m, IEnumerable emojis, PollBehaviour? behaviour = default, TimeSpan? timeout = null) { if (!Utilities.HasReactionIntents(this.Client.Configuration.Intents)) throw new InvalidOperationException("No reaction intents are enabled."); if (!emojis.Any()) throw new ArgumentException("You need to provide at least one emoji for a poll!"); foreach (var em in emojis) await m.CreateReactionAsync(em).ConfigureAwait(false); var res = await this._poller.DoPollAsync(new PollRequest(m, timeout ?? this.Config.Timeout, emojis)).ConfigureAwait(false); var pollbehaviour = behaviour ?? this.Config.PollBehaviour; var thismember = await m.Channel.Guild.GetMemberAsync(this.Client.CurrentUser.Id).ConfigureAwait(false); if (pollbehaviour == PollBehaviour.DeleteEmojis && m.Channel.PermissionsFor(thismember).HasPermission(Permissions.ManageMessages)) await m.DeleteAllReactionsAsync().ConfigureAwait(false); return new ReadOnlyCollection(res.ToList()); } /// /// Waits for any button in the specified collection to be pressed. /// /// The message to wait on. /// A collection of buttons to listen for. /// Override the timeout period in . /// A with the result of button that was pressed, if any. /// Thrown when attempting to wait for a message that is not authored by the current user. /// Thrown when the message does not contain a button with the specified Id, or any buttons at all. public Task> WaitForButtonAsync(DiscordMessage message, IEnumerable buttons, TimeSpan? timeoutOverride = null) => this.WaitForButtonAsync(message, buttons, this.GetCancellationToken(timeoutOverride)); /// /// Waits for any button in the specified collection to be pressed. /// /// The message to wait on. /// A collection of buttons to listen for. /// A custom cancellation token that can be cancelled at any point. /// A with the result of button that was pressed, if any. /// Thrown when attempting to wait for a message that is not authored by the current user. /// Thrown when the message does not contain a button with the specified Id, or any buttons at all. public async Task> WaitForButtonAsync(DiscordMessage message, IEnumerable buttons, CancellationToken token) { if (message.Author != this.Client.CurrentUser) throw new InvalidOperationException("Interaction events are only sent to the application that created them."); if (!buttons.Any()) throw new ArgumentException("You must specify at least one button to listen for."); if (!message.Components.Any()) throw new ArgumentException("Provided message does not contain any components."); if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type is ComponentType.Button)) throw new ArgumentException("Provided Message does not contain any button components."); var res = await this._componentEventWaiter - .WaitForMatchAsync(new(message, + .WaitForMatchAsync(new ComponentMatchRequest(message, c => c.Interaction.Data.ComponentType == ComponentType.Button && buttons.Any(b => b.CustomId == c.Id), token)).ConfigureAwait(false); - return new(res is null, res); + return new InteractivityResult(res is null, res); } /// /// Waits for a user modal submit. /// /// The custom id of the modal to wait for. /// Override the timeout period specified in . /// A with the result of the modal. public Task> WaitForModalAsync(string customId, TimeSpan? timeoutOverride = null) => this.WaitForModalAsync(customId, this.GetCancellationToken(timeoutOverride)); /// /// Waits for a user modal submit. /// /// The custom id of the modal to wait for. /// A custom cancellation token that can be cancelled at any point. /// A with the result of the modal. public async Task> WaitForModalAsync(string customId, CancellationToken token) { var result = await this ._modalEventWaiter - .WaitForModalMatchAsync(new(customId, c => c.Interaction.Type == InteractionType.ModalSubmit, token)) + .WaitForModalMatchAsync(new ModalMatchRequest(customId, c => c.Interaction.Type == InteractionType.ModalSubmit, token)) .ConfigureAwait(false); - return new(result is null, result); + return new InteractivityResult(result is null, result); } /// /// Waits for any button on the specified message to be pressed. /// /// The message to wait for the button on. /// Override the timeout period specified in . /// A with the result of button that was pressed, if any. /// Thrown when attempting to wait for a message that is not authored by the current user. /// Thrown when the message does not contain a button with the specified Id, or any buttons at all. public Task> WaitForButtonAsync(DiscordMessage message, TimeSpan? timeoutOverride = null) => this.WaitForButtonAsync(message, this.GetCancellationToken(timeoutOverride)); /// /// Waits for any button on the specified message to be pressed. /// /// The message to wait for the button on. /// A custom cancellation token that can be cancelled at any point. /// A with the result of button that was pressed, if any. /// Thrown when attempting to wait for a message that is not authored by the current user. /// Thrown when the message does not contain a button with the specified Id, or any buttons at all. public async Task> WaitForButtonAsync(DiscordMessage message, CancellationToken token) { if (message.Author != this.Client.CurrentUser) throw new InvalidOperationException("Interaction events are only sent to the application that created them."); if (!message.Components.Any()) throw new ArgumentException("Provided message does not contain any components."); if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type is ComponentType.Button)) throw new ArgumentException("Message does not contain any button components."); var ids = message.Components.SelectMany(m => m.Components).Select(c => c.CustomId); var result = await this ._componentEventWaiter - .WaitForMatchAsync(new(message, c => c.Interaction.Data.ComponentType == ComponentType.Button && ids.Contains(c.Id), token)) + .WaitForMatchAsync(new ComponentMatchRequest(message, c => c.Interaction.Data.ComponentType == ComponentType.Button && ids.Contains(c.Id), token)) .ConfigureAwait(false); - return new(result is null, result); + return new InteractivityResult(result is null, result); } /// /// Waits for any button on the specified message to be pressed by the specified user. /// /// The message to wait for the button on. /// The user to wait for the button press from. /// Override the timeout period specified in . /// A with the result of button that was pressed, if any. /// Thrown when attempting to wait for a message that is not authored by the current user. /// Thrown when the message does not contain a button with the specified Id, or any buttons at all. public Task> WaitForButtonAsync(DiscordMessage message, DiscordUser user, TimeSpan? timeoutOverride = null) => this.WaitForButtonAsync(message, user, this.GetCancellationToken(timeoutOverride)); /// /// Waits for any button on the specified message to be pressed by the specified user. /// /// The message to wait for the button on. /// The user to wait for the button press from. /// A custom cancellation token that can be cancelled at any point. /// A with the result of button that was pressed, if any. /// Thrown when attempting to wait for a message that is not authored by the current user. /// Thrown when the message does not contain a button with the specified Id, or any buttons at all. public async Task> WaitForButtonAsync(DiscordMessage message, DiscordUser user, CancellationToken token) { if (message.Author != this.Client.CurrentUser) throw new InvalidOperationException("Interaction events are only sent to the application that created them."); if (!message.Components.Any()) throw new ArgumentException("Provided message does not contain any components."); if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type is ComponentType.Button)) throw new ArgumentException("Message does not contain any button components."); var result = await this ._componentEventWaiter - .WaitForMatchAsync(new(message, (c) => c.Interaction.Data.ComponentType is ComponentType.Button && c.User == user, token)) + .WaitForMatchAsync(new ComponentMatchRequest(message, (c) => c.Interaction.Data.ComponentType is ComponentType.Button && c.User == user, token)) .ConfigureAwait(false); - return new(result is null, result); + return new InteractivityResult(result is null, result); } /// /// Waits for a button with the specified Id to be pressed. /// /// The message to wait for the button on. /// The Id of the button to wait for. /// Override the timeout period specified in . /// A with the result of the operation. /// Thrown when attempting to wait for a message that is not authored by the current user. /// Thrown when the message does not contain a button with the specified Id, or any buttons at all. public Task> WaitForButtonAsync(DiscordMessage message, string id, TimeSpan? timeoutOverride = null) => this.WaitForButtonAsync(message, id, this.GetCancellationToken(timeoutOverride)); /// /// Waits for a button with the specified Id to be pressed. /// /// The message to wait for the button on. /// The Id of the button to wait for. /// Override the timeout period specified in . /// A with the result of the operation. /// Thrown when attempting to wait for a message that is not authored by the current user. /// Thrown when the message does not contain a button with the specified Id, or any buttons at all. public async Task> WaitForButtonAsync(DiscordMessage message, string id, CancellationToken token) { if (message.Author != this.Client.CurrentUser) throw new InvalidOperationException("Interaction events are only sent to the application that created them."); if (!message.Components.Any()) throw new ArgumentException("Provided message does not contain any components."); if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type is ComponentType.Button)) throw new ArgumentException("Message does not contain any button components."); if (!message.Components.SelectMany(c => c.Components).OfType().Any(c => c.CustomId == id)) throw new ArgumentException($"Message does not contain button with Id of '{id}'."); var result = await this ._componentEventWaiter - .WaitForMatchAsync(new(message, (c) => c.Interaction.Data.ComponentType is ComponentType.Button && c.Id == id, token)) + .WaitForMatchAsync(new ComponentMatchRequest(message, (c) => c.Interaction.Data.ComponentType is ComponentType.Button && c.Id == id, token)) .ConfigureAwait(false); - return new(result is null, result); + return new InteractivityResult(result is null, result); } /// /// Waits for any button to be interacted with. /// /// The message to wait on. /// The predicate to filter interactions by. /// Override the timeout specified in public Task> WaitForButtonAsync(DiscordMessage message, Func predicate, TimeSpan? timeoutOverride = null) => this.WaitForButtonAsync(message, predicate, this.GetCancellationToken(timeoutOverride)); /// /// Waits for any button to be interacted with. /// /// The message to wait on. /// The predicate to filter interactions by. /// A token to cancel interactivity with at any time. Pass to wait indefinitely. public async Task> WaitForButtonAsync(DiscordMessage message, Func predicate, CancellationToken token) { if (message.Author != this.Client.CurrentUser) throw new InvalidOperationException("Interaction events are only sent to the application that created them."); if (!message.Components.Any()) throw new ArgumentException("Provided message does not contain any components."); if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type is ComponentType.Button)) throw new ArgumentException("Message does not contain any button components."); var result = await this ._componentEventWaiter - .WaitForMatchAsync(new(message, c => c.Interaction.Data.ComponentType is ComponentType.Button && predicate(c), token)) + .WaitForMatchAsync(new ComponentMatchRequest(message, c => c.Interaction.Data.ComponentType is ComponentType.Button && predicate(c), token)) .ConfigureAwait(false); - return new(result is null, result); + return new InteractivityResult(result is null, result); } /// /// Waits for any dropdown to be interacted with. /// /// The message to wait for. /// A filter predicate. /// Override the timeout period specified in . /// Thrown when the Provided message does not contain any dropdowns public Task> WaitForSelectAsync(DiscordMessage message, Func predicate, TimeSpan? timeoutOverride = null) => this.WaitForSelectAsync(message, predicate, this.GetCancellationToken(timeoutOverride)); /// /// Waits for any dropdown to be interacted with. /// /// The message to wait for. /// A filter predicate. /// A token that can be used to cancel interactivity. Pass to wait indefinitely. /// Thrown when the Provided message does not contain any dropdowns public async Task> WaitForSelectAsync(DiscordMessage message, Func predicate, CancellationToken token) { if (message.Author != this.Client.CurrentUser) throw new InvalidOperationException("Interaction events are only sent to the application that created them."); if (!message.Components.Any()) throw new ArgumentException("Provided message does not contain any components."); if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type is ComponentType.Select)) throw new ArgumentException("Message does not contain any select components."); var result = await this ._componentEventWaiter - .WaitForMatchAsync(new(message, c => c.Interaction.Data.ComponentType is ComponentType.Select && predicate(c), token)) + .WaitForMatchAsync(new ComponentMatchRequest(message, c => c.Interaction.Data.ComponentType is ComponentType.Select && predicate(c), token)) .ConfigureAwait(false); - return new(result is null, result); + return new InteractivityResult(result is null, result); } /// /// Waits for a dropdown to be interacted with. /// /// This is here for backwards-compatibility and will internally create a cancellation token. /// The message to wait on. /// The Id of the dropdown to wait on. /// Override the timeout period specified in . /// Thrown when the message does not have any dropdowns or any dropdown with the specified Id. public Task> WaitForSelectAsync(DiscordMessage message, string id, TimeSpan? timeoutOverride = null) => this.WaitForSelectAsync(message, id, this.GetCancellationToken(timeoutOverride)); /// /// Waits for a dropdown to be interacted with. /// /// The message to wait on. /// The Id of the dropdown to wait on. /// A custom cancellation token that can be cancelled at any point. /// Thrown when the message does not have any dropdowns or any dropdown with the specified Id. public async Task> WaitForSelectAsync(DiscordMessage message, string id, CancellationToken token) { if (message.Author != this.Client.CurrentUser) throw new InvalidOperationException("Interaction events are only sent to the application that created them."); if (!message.Components.Any()) throw new ArgumentException("Provided message does not contain any components."); if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type is ComponentType.Select)) throw new ArgumentException("Message does not contain any select components."); if (message.Components.SelectMany(c => c.Components).OfType().All(c => c.CustomId != id)) throw new ArgumentException($"Message does not contain select component with Id of '{id}'."); var result = await this ._componentEventWaiter - .WaitForMatchAsync(new(message, (c) => c.Interaction.Data.ComponentType is ComponentType.Select && c.Id == id, token)) + .WaitForMatchAsync(new ComponentMatchRequest(message, (c) => c.Interaction.Data.ComponentType is ComponentType.Select && c.Id == id, token)) .ConfigureAwait(false); - return new(result is null, result); + return new InteractivityResult(result is null, result); } /// /// Waits for a dropdown to be interacted with by a specific user. /// /// The message to wait on. /// The user to wait on. /// The Id of the dropdown to wait on. /// Override the timeout period specified in . /// Thrown when the message does not have any dropdowns or any dropdown with the specified Id. public Task> WaitForSelectAsync(DiscordMessage message, DiscordUser user, string id, TimeSpan? timeoutOverride = null) => this.WaitForSelectAsync(message, user, id, this.GetCancellationToken(timeoutOverride)); /// /// Waits for a dropdown to be interacted with by a specific user. /// /// The message to wait on. /// The user to wait on. /// The Id of the dropdown to wait on. /// A custom cancellation token that can be cancelled at any point. /// Thrown when the message does not have any dropdowns or any dropdown with the specified Id. public async Task> WaitForSelectAsync(DiscordMessage message, DiscordUser user, string id, CancellationToken token) { if (message.Author != this.Client.CurrentUser) throw new InvalidOperationException("Interaction events are only sent to the application that created them."); if (!message.Components.Any()) throw new ArgumentException("Provided message does not contain any components."); if (!message.Components.SelectMany(c => c.Components).Any(c => c.Type is ComponentType.Select)) throw new ArgumentException("Message does not contain any select components."); if (message.Components.SelectMany(c => c.Components).OfType().All(c => c.CustomId != id)) throw new ArgumentException($"Message does not contain select with Id of '{id}'."); var result = await this ._componentEventWaiter - .WaitForMatchAsync(new(message, (c) => c.Id == id && c.User == user, token)).ConfigureAwait(false); + .WaitForMatchAsync(new ComponentMatchRequest(message, (c) => c.Id == id && c.User == user, token)).ConfigureAwait(false); - return new(result is null, result); + return new InteractivityResult(result is null, result); } /// /// Waits for a specific message. /// /// Predicate to match. /// override timeout period. public async Task> WaitForMessageAsync(Func predicate, TimeSpan? timeoutoverride = null) { if (!Utilities.HasMessageIntents(this.Client.Configuration.Intents)) throw new InvalidOperationException("No message intents are enabled."); var timeout = timeoutoverride ?? this.Config.Timeout; var returns = await this._messageCreatedWaiter.WaitForMatchAsync(new MatchRequest(x => predicate(x.Message), timeout)).ConfigureAwait(false); return new InteractivityResult(returns == null, returns?.Message); } /// /// Wait for a specific reaction. /// /// Predicate to match. /// override timeout period. public async Task> WaitForReactionAsync(Func predicate, TimeSpan? timeoutoverride = null) { if (!Utilities.HasReactionIntents(this.Client.Configuration.Intents)) throw new InvalidOperationException("No reaction intents are enabled."); var timeout = timeoutoverride ?? this.Config.Timeout; var returns = await this._messageReactionAddWaiter.WaitForMatchAsync(new MatchRequest(predicate, timeout)).ConfigureAwait(false); return new InteractivityResult(returns == null, returns); } /// /// Wait for a specific reaction. /// For this Event you need the intent specified in /// /// Message reaction was added to. /// User that made the reaction. /// override timeout period. public async Task> WaitForReactionAsync(DiscordMessage message, DiscordUser user, TimeSpan? timeoutoverride = null) => await this.WaitForReactionAsync(x => x.User.Id == user.Id && x.Message.Id == message.Id, timeoutoverride).ConfigureAwait(false); /// /// Waits for a specific reaction. /// For this Event you need the intent specified in /// /// Predicate to match. /// Message reaction was added to. /// User that made the reaction. /// override timeout period. public async Task> WaitForReactionAsync(Func predicate, DiscordMessage message, DiscordUser user, TimeSpan? timeoutoverride = null) => await this.WaitForReactionAsync(x => predicate(x) && x.User.Id == user.Id && x.Message.Id == message.Id, timeoutoverride).ConfigureAwait(false); /// /// Waits for a specific reaction. /// For this Event you need the intent specified in /// /// predicate to match. /// User that made the reaction. /// Override timeout period. public async Task> WaitForReactionAsync(Func predicate, DiscordUser user, TimeSpan? timeoutoverride = null) => await this.WaitForReactionAsync(x => predicate(x) && x.User.Id == user.Id, timeoutoverride).ConfigureAwait(false); /// /// Waits for a user to start typing. /// /// User that starts typing. /// Channel the user is typing in. /// Override timeout period. public async Task> WaitForUserTypingAsync(DiscordUser user, DiscordChannel channel, TimeSpan? timeoutoverride = null) { if (!Utilities.HasTypingIntents(this.Client.Configuration.Intents)) throw new InvalidOperationException("No typing intents are enabled."); var timeout = timeoutoverride ?? this.Config.Timeout; var returns = await this._typingStartWaiter.WaitForMatchAsync( new MatchRequest(x => x.User.Id == user.Id && x.Channel.Id == channel.Id, timeout)) .ConfigureAwait(false); return new InteractivityResult(returns == null, returns); } /// /// Waits for a user to start typing. /// /// User that starts typing. /// Override timeout period. public async Task> WaitForUserTypingAsync(DiscordUser user, TimeSpan? timeoutoverride = null) { if (!Utilities.HasTypingIntents(this.Client.Configuration.Intents)) throw new InvalidOperationException("No typing intents are enabled."); var timeout = timeoutoverride ?? this.Config.Timeout; var returns = await this._typingStartWaiter.WaitForMatchAsync( new MatchRequest(x => x.User.Id == user.Id, timeout)) .ConfigureAwait(false); return new InteractivityResult(returns == null, returns); } /// /// Waits for any user to start typing. /// /// Channel to type in. /// Override timeout period. public async Task> WaitForTypingAsync(DiscordChannel channel, TimeSpan? timeoutoverride = null) { if (!Utilities.HasTypingIntents(this.Client.Configuration.Intents)) throw new InvalidOperationException("No typing intents are enabled."); var timeout = timeoutoverride ?? this.Config.Timeout; var returns = await this._typingStartWaiter.WaitForMatchAsync( new MatchRequest(x => x.Channel.Id == channel.Id, timeout)) .ConfigureAwait(false); return new InteractivityResult(returns == null, returns); } /// /// Collects reactions on a specific message. /// /// Message to collect reactions on. /// Override timeout period. public async Task> CollectReactionsAsync(DiscordMessage m, TimeSpan? timeoutoverride = null) { if (!Utilities.HasReactionIntents(this.Client.Configuration.Intents)) throw new InvalidOperationException("No reaction intents are enabled."); var timeout = timeoutoverride ?? this.Config.Timeout; var collection = await this._reactionCollector.CollectAsync(new ReactionCollectRequest(m, timeout)).ConfigureAwait(false); return collection; } /// /// Waits for specific event args to be received. Make sure the appropriate are registered, if needed. /// /// /// The predicate. /// Override timeout period. public async Task> WaitForEventArgsAsync(Func predicate, TimeSpan? timeoutoverride = null) where T : AsyncEventArgs { var timeout = timeoutoverride ?? this.Config.Timeout; using var waiter = new EventWaiter(this.Client); var res = await waiter.WaitForMatchAsync(new MatchRequest(predicate, timeout)).ConfigureAwait(false); return new InteractivityResult(res == null, res); } /// /// Collects the event arguments. /// /// The predicate. /// Override timeout period. public async Task> CollectEventArgsAsync(Func predicate, TimeSpan? timeoutoverride = null) where T : AsyncEventArgs { var timeout = timeoutoverride ?? this.Config.Timeout; using var waiter = new EventWaiter(this.Client); var res = await waiter.CollectMatchesAsync(new CollectRequest(predicate, timeout)).ConfigureAwait(false); return res; } /// /// Sends a paginated message with buttons. /// /// The channel to send it on. /// User to give control. /// The pages. /// Pagination buttons (pass null to use buttons defined in ). /// Pagination behaviour. /// Deletion behaviour /// A custom cancellation token that can be cancelled at any point. public async Task SendPaginatedMessageAsync( DiscordChannel channel, DiscordUser user, IEnumerable pages, PaginationButtons buttons, PaginationBehaviour? behaviour = default, ButtonPaginationBehavior? deletion = default, CancellationToken token = default) { var bhv = behaviour ?? this.Config.PaginationBehaviour; var del = deletion ?? this.Config.ButtonBehavior; var bts = buttons ?? this.Config.PaginationButtons; - bts = new(bts); + bts = new PaginationButtons(bts); if (bhv is PaginationBehaviour.Ignore) { bts.SkipLeft.Disable(); bts.Left.Disable(); } var builder = new DiscordMessageBuilder() .WithContent(pages.First().Content) .WithEmbed(pages.First().Embed) .AddComponents(bts.ButtonArray); var message = await builder.SendAsync(channel).ConfigureAwait(false); var req = new ButtonPaginationRequest(message, user, bhv, del, bts, pages.ToArray(), token == default ? this.GetCancellationToken() : token); await this._compPaginator.DoPaginationAsync(req).ConfigureAwait(false); } /// /// Sends a paginated message with buttons. /// /// The channel to send it on. /// User to give control. /// The pages. /// Pagination buttons (pass null to use buttons defined in ). /// Pagination behaviour. /// Deletion behaviour /// Override timeout period. public Task SendPaginatedMessageAsync( DiscordChannel channel, DiscordUser user, IEnumerable pages, PaginationButtons buttons, TimeSpan? timeoutoverride, PaginationBehaviour? behaviour = default, ButtonPaginationBehavior? deletion = default) => this.SendPaginatedMessageAsync(channel, user, pages, buttons, behaviour, deletion, this.GetCancellationToken(timeoutoverride)); /// /// Sends the paginated message. /// /// The channel. /// The user. /// The pages. /// The behaviour. /// The deletion. /// The token. /// A Task. public Task SendPaginatedMessageAsync(DiscordChannel channel, DiscordUser user, IEnumerable pages, PaginationBehaviour? behaviour = default, ButtonPaginationBehavior? deletion = default, CancellationToken token = default) => this.SendPaginatedMessageAsync(channel, user, pages, default, behaviour, deletion, token); /// /// Sends the paginated message. /// /// The channel. /// The user. /// The pages. /// The timeoutoverride. /// The behaviour. /// The deletion. /// A Task. public Task SendPaginatedMessageAsync(DiscordChannel channel, DiscordUser user, IEnumerable pages, TimeSpan? timeoutoverride, PaginationBehaviour? behaviour = default, ButtonPaginationBehavior? deletion = default) => this.SendPaginatedMessageAsync(channel, user, pages, timeoutoverride, behaviour, deletion); /// /// Sends a paginated message. /// For this Event you need the intent specified in /// /// Channel to send paginated message in. /// User to give control. /// Pages. /// Pagination emojis. /// Pagination behaviour (when hitting max and min indices). /// Deletion behaviour. /// Override timeout period. public async Task SendPaginatedMessageAsync(DiscordChannel channel, DiscordUser user, IEnumerable pages, PaginationEmojis emojis, PaginationBehaviour? behaviour = default, PaginationDeletion? deletion = default, TimeSpan? timeoutoverride = null) { var builder = new DiscordMessageBuilder() .WithContent(pages.First().Content) .WithEmbed(pages.First().Embed); var m = await builder.SendAsync(channel).ConfigureAwait(false); var timeout = timeoutoverride ?? this.Config.Timeout; var bhv = behaviour ?? this.Config.PaginationBehaviour; var del = deletion ?? this.Config.PaginationDeletion; var ems = emojis ?? this.Config.PaginationEmojis; var prequest = new PaginationRequest(m, user, bhv, del, ems, timeout, pages.ToArray()); await this._paginator.DoPaginationAsync(prequest).ConfigureAwait(false); } /// /// Sends a paginated message in response to an interaction. /// /// Pass the interaction directly. Interactivity will ACK it. /// /// /// The interaction to create a response to. /// Whether the response should be ephemeral. /// The user to listen for button presses from. /// The pages to paginate. /// Optional: custom buttons /// Pagination behaviour. /// Deletion behaviour /// A custom cancellation token that can be cancelled at any point. public async Task SendPaginatedResponseAsync(DiscordInteraction interaction, bool ephemeral, DiscordUser user, IEnumerable pages, PaginationButtons buttons = null, PaginationBehaviour? behaviour = default, ButtonPaginationBehavior? deletion = default, CancellationToken token = default) { var bhv = behaviour ?? this.Config.PaginationBehaviour; var del = deletion ?? this.Config.ButtonBehavior; var bts = buttons ?? this.Config.PaginationButtons; - bts = new(bts); + bts = new PaginationButtons(bts); if (bhv is PaginationBehaviour.Ignore) { bts.SkipLeft.Disable(); bts.Left.Disable(); } var builder = new DiscordInteractionResponseBuilder() .WithContent(pages.First().Content) .AddEmbed(pages.First().Embed) .AsEphemeral(ephemeral) .AddComponents(bts.ButtonArray); await interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, builder).ConfigureAwait(false); var message = await interaction.GetOriginalResponseAsync().ConfigureAwait(false); var req = new InteractionPaginationRequest(interaction, message, user, bhv, del, bts, pages, token); await this._compPaginator.DoPaginationAsync(req).ConfigureAwait(false); } /// /// Waits for a custom pagination request to finish. /// This does NOT handle removing emojis after finishing for you. /// /// /// public async Task WaitForCustomPaginationAsync(IPaginationRequest request) => await this._paginator.DoPaginationAsync(request).ConfigureAwait(false); /// /// Waits for custom button-based pagination request to finish. ///
/// This does not invoke . ///
/// The request to wait for. public async Task WaitForCustomComponentPaginationAsync(IPaginationRequest request) => await this._compPaginator.DoPaginationAsync(request).ConfigureAwait(false); /// /// Generates pages from a string, and puts them in message content. /// /// Input string. /// How to split input string. /// public IEnumerable GeneratePagesInContent(string input, SplitType splittype = SplitType.Character) { if (string.IsNullOrEmpty(input)) throw new ArgumentException("You must provide a string that is not null or empty!"); var result = new List(); List split; switch (splittype) { default: case SplitType.Character: split = this.SplitString(input, 500).ToList(); break; case SplitType.Line: var subsplit = input.Split('\n'); split = new List(); var s = ""; for (var i = 0; i < subsplit.Length; i++) { s += subsplit[i]; if (i >= 15 && i % 15 == 0) { split.Add(s); s = ""; } } if (split.All(x => x != s)) split.Add(s); break; } var page = 1; foreach (var s in split) { result.Add(new Page($"Page {page}:\n{s}")); page++; } return result; } /// /// Generates pages from a string, and puts them in message embeds. /// /// Input string. /// How to split input string. /// Base embed for output embeds. /// public IEnumerable GeneratePagesInEmbed(string input, SplitType splittype = SplitType.Character, DiscordEmbedBuilder embedbase = null) { if (string.IsNullOrEmpty(input)) throw new ArgumentException("You must provide a string that is not null or empty!"); var embed = embedbase ?? new DiscordEmbedBuilder(); var result = new List(); List split; switch (splittype) { default: case SplitType.Character: split = this.SplitString(input, 500).ToList(); break; case SplitType.Line: var subsplit = input.Split('\n'); split = new List(); var s = ""; for (var i = 0; i < subsplit.Length; i++) { s += $"{subsplit[i]}\n"; if (i % 15 == 0 && i != 0) { split.Add(s); s = ""; } } if (!split.Any(x => x == s)) split.Add(s); break; } var page = 1; foreach (var s in split) { result.Add(new Page("", new DiscordEmbedBuilder(embed).WithDescription(s).WithFooter($"Page {page}/{split.Count}"))); page++; } return result; } /// /// Splits the string. /// /// The string. /// The chunk size. private List SplitString(string str, int chunkSize) { var res = new List(); var len = str.Length; var i = 0; while (i < len) { var size = Math.Min(len - i, chunkSize); res.Add(str.Substring(i, size)); i += size; } return res; } /// /// Gets the cancellation token. /// /// The timeout. private CancellationToken GetCancellationToken(TimeSpan? timeout = null) => new CancellationTokenSource(timeout ?? this.Config.Timeout).Token; /// /// Handles an invalid interaction. /// /// The interaction. private async Task HandleInvalidInteraction(DiscordInteraction interaction) { var at = this.Config.ResponseBehavior switch { InteractionResponseBehavior.Ack => interaction.CreateResponseAsync(InteractionResponseType.DeferredMessageUpdate), - InteractionResponseBehavior.Respond => interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new() { Content = this.Config.ResponseMessage, IsEphemeral = true}), + InteractionResponseBehavior.Respond => interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, new DiscordInteractionResponseBuilder { Content = this.Config.ResponseMessage, IsEphemeral = true}), InteractionResponseBehavior.Ignore => Task.CompletedTask, _ => throw new ArgumentException("Unknown enum value.") }; await at; } } } diff --git a/DisCatSharp.Lavalink/Entities/LavalinkRouteStatus.cs b/DisCatSharp.Lavalink/Entities/LavalinkRouteStatus.cs index 4a21121dd..4a58b3399 100644 --- a/DisCatSharp.Lavalink/Entities/LavalinkRouteStatus.cs +++ b/DisCatSharp.Lavalink/Entities/LavalinkRouteStatus.cs @@ -1,157 +1,155 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System.Collections.Generic; using Newtonsoft.Json; namespace DisCatSharp.Lavalink.Entities { /// /// The lavalink route status. /// public class LavalinkRouteStatus { /// /// Gets the route planner type. /// [JsonIgnore] public LavalinkRoutePlannerType? Class => this.GetLavalinkRoutePlannerType(this.PlannerTypeClass); /// /// Gets the details of the route planner. /// [JsonProperty("details", NullValueHandling = NullValueHandling.Ignore)] public LavalinkRouteStatusDetails Details { get; internal set; } /// /// Gets or sets the class. /// [JsonProperty("class", NullValueHandling = NullValueHandling.Ignore)] internal string PlannerTypeClass { get; set; } /// /// Gets the lavalink route planner type. /// /// The type. - private LavalinkRoutePlannerType? GetLavalinkRoutePlannerType(string type) - { - return type switch + private LavalinkRoutePlannerType? GetLavalinkRoutePlannerType(string type) => + type switch { "RotatingIpRoutePlanner" => LavalinkRoutePlannerType.RotatingIpRoutePlanner, "BalancingIpRoutePlanner" => LavalinkRoutePlannerType.BalancingIpRoutePlanner, "NanoIpRoutePlanner" => LavalinkRoutePlannerType.NanoIpRoutePlanner, "RotatingNanoIpRoutePlanner" => LavalinkRoutePlannerType.RotatingNanoIpRoutePlanner, _ => null, }; - } } /// /// The lavalink route status details. /// public class LavalinkRouteStatusDetails { /// /// Gets the details for the current IP block. /// [JsonProperty("ipBlock", NullValueHandling = NullValueHandling.Ignore)] public LavalinkIpBlock IpBlock { get; internal set; } /// /// Gets the collection of failed addresses. /// [JsonProperty("failingAddresses", NullValueHandling = NullValueHandling.Ignore)] public IEnumerable FailedAddresses { get; internal set; } /// /// Gets the number of rotations since the restart of Lavalink. /// Only present in the . /// [JsonProperty("rotateIndex", NullValueHandling = NullValueHandling.Ignore)] public string RotateIndex { get; internal set; } /// /// Gets the current offset of the IP block. /// Only present in the . /// [JsonProperty("ipIndex", NullValueHandling = NullValueHandling.Ignore)] public string IpIndex { get; internal set; } /// /// Gets the current IP Address used by the planner. /// Only present in the . /// [JsonProperty("currentAddress", NullValueHandling = NullValueHandling.Ignore)] public string CurrentAddress { get; internal set; } /// /// Gets the current offset of the IP block. /// Only present in the and the . /// [JsonProperty("currentAddressIndex", NullValueHandling = NullValueHandling.Ignore)] public long CurrentAddressIndex { get; internal set; } /// /// Gets the information in which /64 block ips are chosen. This number increases on each ban. /// Only present in the . /// [JsonProperty("blockIndex", NullValueHandling = NullValueHandling.Ignore)] public string BlockIndex { get; internal set; } } public struct LavalinkIpBlock { /// /// Gets the type of the IP block. /// [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] public string Type { get; internal set; } /// /// Gets the size of the IP block. /// [JsonProperty("size", NullValueHandling = NullValueHandling.Ignore)] public string Size { get; internal set; } } public struct LavalinkFailedAddress { /// /// Gets the failed address IP. /// [JsonProperty("address", NullValueHandling = NullValueHandling.Ignore)] public string Address { get; internal set; } /// /// Gets the failing timestamp in miliseconds. /// [JsonProperty("failingTimestamp", NullValueHandling = NullValueHandling.Ignore)] public ulong FailingTimestamp { get; internal set; } /// /// Gets the DateTime format of the failing address. /// [JsonProperty("failingTime", NullValueHandling = NullValueHandling.Ignore)] public string FailingTime { get; internal set; } } } diff --git a/DisCatSharp.Lavalink/LavalinkConfiguration.cs b/DisCatSharp.Lavalink/LavalinkConfiguration.cs index 48c701965..aa4e2fc6b 100644 --- a/DisCatSharp.Lavalink/LavalinkConfiguration.cs +++ b/DisCatSharp.Lavalink/LavalinkConfiguration.cs @@ -1,115 +1,115 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using DisCatSharp.Entities; using DisCatSharp.Net; using Microsoft.Extensions.DependencyInjection; namespace DisCatSharp.Lavalink { /// /// Lavalink connection configuration. /// public sealed class LavalinkConfiguration { /// /// Sets the endpoint for Lavalink REST. /// Defaults to 127.0.0.1 on port 2333. /// - public ConnectionEndpoint RestEndpoint { internal get; set; } = new ConnectionEndpoint("127.0.0.1", 2333); + public ConnectionEndpoint RestEndpoint { internal get; set; } = new("127.0.0.1", 2333); /// /// Sets the endpoint for the Lavalink Websocket connection. /// Defaults to 127.0.0.1 on port 2333. /// - public ConnectionEndpoint SocketEndpoint { internal get; set; } = new ConnectionEndpoint("127.0.0.1", 2333); + public ConnectionEndpoint SocketEndpoint { internal get; set; } = new("127.0.0.1", 2333); /// /// Sets whether the connection wrapper should attempt automatic reconnects should the connection drop. /// Defaults to true. /// public bool SocketAutoReconnect { internal get; set; } = true; /// /// Sets the password for the Lavalink connection. /// Defaults to "youshallnotpass". /// public string Password { internal get; set; } = "youshallnotpass"; /// /// Sets the resume key for the Lavalink connection. /// This will allow existing voice sessions to continue for a certain time after the client is disconnected. /// public string ResumeKey { internal get; set; } /// /// Sets the time in seconds when all voice sessions are closed after the client disconnects. /// Defaults to 60 seconds. /// public int ResumeTimeout { internal get; set; } = 60; /// /// Sets the time in miliseconds to wait for Lavalink's voice WebSocket to close after leaving a voice channel. /// This will be the delay before the guild connection is removed. /// Defaults to 3000 miliseconds. /// public int WebSocketCloseTimeout { internal get; set; } = 3000; /// /// Sets the voice region ID for the Lavalink connection. /// This should be used if nodes should be filtered by region with . /// public DiscordVoiceRegion Region { internal get; set; } /// /// Creates a new instance of . /// [ActivatorUtilitiesConstructor] public LavalinkConfiguration() { } /// /// Creates a new instance of , copying the properties of another configuration. /// /// Configuration the properties of which are to be copied. public LavalinkConfiguration(LavalinkConfiguration other) { this.RestEndpoint = new ConnectionEndpoint { Hostname = other.RestEndpoint.Hostname, Port = other.RestEndpoint.Port, Secured = other.RestEndpoint.Secured }; this.SocketEndpoint = new ConnectionEndpoint { Hostname = other.SocketEndpoint.Hostname, Port = other.SocketEndpoint.Port, Secured = other.SocketEndpoint.Secured }; this.Password = other.Password; this.ResumeKey = other.ResumeKey; this.ResumeTimeout = other.ResumeTimeout; this.SocketAutoReconnect = other.SocketAutoReconnect; this.Region = other.Region; this.WebSocketCloseTimeout = other.WebSocketCloseTimeout; } } } diff --git a/DisCatSharp.Lavalink/LavalinkEvents.cs b/DisCatSharp.Lavalink/LavalinkEvents.cs index d34165d7a..b107377da 100644 --- a/DisCatSharp.Lavalink/LavalinkEvents.cs +++ b/DisCatSharp.Lavalink/LavalinkEvents.cs @@ -1,77 +1,77 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using Microsoft.Extensions.Logging; namespace DisCatSharp.Lavalink { /// /// Contains well-defined event IDs used by the Lavalink extension. /// public static class LavalinkEvents { /// /// Miscellaneous events, that do not fit in any other category. /// - public static EventId Misc { get; } = new EventId(400, "Lavalink"); + public static EventId Misc { get; } = new(400, "Lavalink"); /// /// Events pertaining to Lavalink node connection errors. /// - public static EventId LavalinkConnectionError { get; } = new EventId(401, nameof(LavalinkConnectionError)); + public static EventId LavalinkConnectionError { get; } = new(401, nameof(LavalinkConnectionError)); /// /// Events emitted for clean disconnects from Lavalink. /// - public static EventId LavalinkConnectionClosed { get; } = new EventId(402, nameof(LavalinkConnectionClosed)); + public static EventId LavalinkConnectionClosed { get; } = new(402, nameof(LavalinkConnectionClosed)); /// /// Events emitted for successful connections made to Lavalink. /// - public static EventId LavalinkConnected { get; } = new EventId(403, nameof(LavalinkConnected)); + public static EventId LavalinkConnected { get; } = new(403, nameof(LavalinkConnected)); /// /// Events pertaining to errors that occur when decoding payloads received from Lavalink nodes. /// - public static EventId LavalinkDecodeError { get; } = new EventId(404, nameof(LavalinkDecodeError)); + public static EventId LavalinkDecodeError { get; } = new(404, nameof(LavalinkDecodeError)); /// /// Events emitted when Lavalink's REST API responds with an error. /// - public static EventId LavalinkRestError { get; } = new EventId(405, nameof(LavalinkRestError)); + public static EventId LavalinkRestError { get; } = new(405, nameof(LavalinkRestError)); /// /// Events containing raw payloads, received from Lavalink nodes. /// - public static EventId LavalinkWsRx { get; } = new EventId(406, "Lavalink ↓"); + public static EventId LavalinkWsRx { get; } = new(406, "Lavalink ↓"); /// /// Events containing raw payloads, as they're being sent to Lavalink nodes. /// - public static EventId LavalinkWsTx { get; } = new EventId(407, "Lavalink ↑"); + public static EventId LavalinkWsTx { get; } = new(407, "Lavalink ↑"); /// /// Events pertaining to Gateway Intents. Typically diagnostic information. /// - public static EventId Intents { get; } = new EventId(408, nameof(Intents)); + public static EventId Intents { get; } = new(408, nameof(Intents)); } } diff --git a/DisCatSharp.Lavalink/LavalinkExtension.cs b/DisCatSharp.Lavalink/LavalinkExtension.cs index 658ffd766..993613ed4 100644 --- a/DisCatSharp.Lavalink/LavalinkExtension.cs +++ b/DisCatSharp.Lavalink/LavalinkExtension.cs @@ -1,215 +1,215 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using DisCatSharp.Common.Utilities; using DisCatSharp.Entities; using DisCatSharp.Lavalink.EventArgs; using DisCatSharp.Net; namespace DisCatSharp.Lavalink { /// /// The lavalink extension. /// public sealed class LavalinkExtension : BaseExtension { /// /// Triggered whenever a node disconnects. /// public event AsyncEventHandler NodeDisconnected { - add { this._nodeDisconnected.Register(value); } - remove { this._nodeDisconnected.Unregister(value); } + add => this._nodeDisconnected.Register(value); + remove => this._nodeDisconnected.Unregister(value); } private AsyncEvent _nodeDisconnected; /// /// Gets a dictionary of connected Lavalink nodes for the extension. /// public IReadOnlyDictionary ConnectedNodes { get; } private readonly ConcurrentDictionary _connectedNodes = new(); /// /// Creates a new instance of this Lavalink extension. /// internal LavalinkExtension() { this.ConnectedNodes = new ReadOnlyConcurrentDictionary(this._connectedNodes); } /// /// DO NOT USE THIS MANUALLY. /// /// DO NOT USE THIS MANUALLY. /// protected internal override void Setup(DiscordClient client) { if (this.Client != null) throw new InvalidOperationException("What did I tell you?"); this.Client = client; this._nodeDisconnected = new AsyncEvent("LAVALINK_NODE_DISCONNECTED", TimeSpan.Zero, this.Client.EventErrorHandler); } /// /// Connect to a Lavalink node. /// /// Lavalink client configuration. /// The established Lavalink connection. public async Task ConnectAsync(LavalinkConfiguration config) { if (this._connectedNodes.ContainsKey(config.SocketEndpoint)) return this._connectedNodes[config.SocketEndpoint]; var con = new LavalinkNodeConnection(this.Client, this, config); con.NodeDisconnected += this.Con_NodeDisconnected; con.Disconnected += this.Con_Disconnected; this._connectedNodes[con.NodeEndpoint] = con; try { await con.StartAsync().ConfigureAwait(false); } catch { this.Con_NodeDisconnected(con); throw; } return con; } /// /// Gets the Lavalink node connection for the specified endpoint. /// /// Endpoint at which the node resides. /// Lavalink node connection. public LavalinkNodeConnection GetNodeConnection(ConnectionEndpoint endpoint) => this._connectedNodes.ContainsKey(endpoint) ? this._connectedNodes[endpoint] : null; /// /// Gets a Lavalink node connection based on load balancing and an optional voice region. /// /// The region to compare with the node's , if any. /// The least load affected node connection, or null if no nodes are present. public LavalinkNodeConnection GetIdealNodeConnection(DiscordVoiceRegion region = null) { if (this._connectedNodes.Count <= 1) return this._connectedNodes.Values.FirstOrDefault(); var nodes = this._connectedNodes.Values.ToArray(); if (region != null) { var regionPredicate = new Func(x => x.Region == region); if (nodes.Any(regionPredicate)) nodes = nodes.Where(regionPredicate).ToArray(); if (nodes.Count() <= 1) return nodes.FirstOrDefault(); } return this.FilterByLoad(nodes); } /// /// Gets a Lavalink guild connection from a . /// /// The guild the connection is on. /// The found guild connection, or null if one could not be found. public LavalinkGuildConnection GetGuildConnection(DiscordGuild guild) { var nodes = this._connectedNodes.Values; var node = nodes.FirstOrDefault(x => x.ConnectedGuildsInternal.ContainsKey(guild.Id)); return node?.GetGuildConnection(guild); } /// /// Filters the by load. /// /// The nodes. private LavalinkNodeConnection FilterByLoad(LavalinkNodeConnection[] nodes) { Array.Sort(nodes, (a, b) => { if (!a.Statistics.Updated || !b.Statistics.Updated) return 0; //https://github.com/FredBoat/Lavalink-Client/blob/48bc27784f57be5b95d2ff2eff6665451b9366f5/src/main/java/lavalink/client/io/LavalinkLoadBalancer.java#L122 //https://github.com/briantanner/eris-lavalink/blob/master/src/PlayerManager.js#L329 //player count var aPenaltyCount = a.Statistics.ActivePlayers; var bPenaltyCount = b.Statistics.ActivePlayers; //cpu load aPenaltyCount += (int)Math.Pow(1.05d, (100 * (a.Statistics.CpuSystemLoad / a.Statistics.CpuCoreCount) * 10) - 10); bPenaltyCount += (int)Math.Pow(1.05d, (100 * (b.Statistics.CpuSystemLoad / a.Statistics.CpuCoreCount) * 10) - 10); //frame load if (a.Statistics.AverageDeficitFramesPerMinute > 0) { //deficit frame load aPenaltyCount += (int)((Math.Pow(1.03d, 500f * (a.Statistics.AverageDeficitFramesPerMinute / 3000f)) * 600) - 600); //null frame load aPenaltyCount += (int)((Math.Pow(1.03d, 500f * (a.Statistics.AverageNulledFramesPerMinute / 3000f)) * 300) - 300); } //frame load if (b.Statistics.AverageDeficitFramesPerMinute > 0) { //deficit frame load bPenaltyCount += (int)((Math.Pow(1.03d, 500f * (b.Statistics.AverageDeficitFramesPerMinute / 3000f)) * 600) - 600); //null frame load bPenaltyCount += (int)((Math.Pow(1.03d, 500f * (b.Statistics.AverageNulledFramesPerMinute / 3000f)) * 300) - 300); } return aPenaltyCount - bPenaltyCount; }); return nodes[0]; } /// /// Removes a node. /// /// The node to be removed. private void Con_NodeDisconnected(LavalinkNodeConnection node) => this._connectedNodes.TryRemove(node.NodeEndpoint, out _); /// /// Disconnects a node. /// /// The affected node. /// The node disconnected event args. private Task Con_Disconnected(LavalinkNodeConnection node, NodeDisconnectedEventArgs e) => this._nodeDisconnected.InvokeAsync(node, e); } } diff --git a/DisCatSharp.Lavalink/LavalinkGuildConnection.cs b/DisCatSharp.Lavalink/LavalinkGuildConnection.cs index 8066e3921..e38a72e48 100644 --- a/DisCatSharp.Lavalink/LavalinkGuildConnection.cs +++ b/DisCatSharp.Lavalink/LavalinkGuildConnection.cs @@ -1,439 +1,439 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Globalization; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using DisCatSharp.Common.Utilities; using DisCatSharp.Entities; using DisCatSharp.EventArgs; using DisCatSharp.Lavalink.Entities; using DisCatSharp.Lavalink.EventArgs; using Newtonsoft.Json; namespace DisCatSharp.Lavalink { internal delegate void ChannelDisconnectedEventHandler(LavalinkGuildConnection node); /// /// Represents a Lavalink connection to a channel. /// public sealed class LavalinkGuildConnection { /// /// Triggered whenever Lavalink updates player status. /// public event AsyncEventHandler PlayerUpdated { - add { this._playerUpdated.Register(value); } - remove { this._playerUpdated.Unregister(value); } + add => this._playerUpdated.Register(value); + remove => this._playerUpdated.Unregister(value); } private readonly AsyncEvent _playerUpdated; /// /// Triggered whenever playback of a track starts. /// This is only available for version 3.3.1 and greater. /// public event AsyncEventHandler PlaybackStarted { - add { this._playbackStarted.Register(value); } - remove { this._playbackStarted.Unregister(value); } + add => this._playbackStarted.Register(value); + remove => this._playbackStarted.Unregister(value); } private readonly AsyncEvent _playbackStarted; /// /// Triggered whenever playback of a track finishes. /// public event AsyncEventHandler PlaybackFinished { - add { this._playbackFinished.Register(value); } - remove { this._playbackFinished.Unregister(value); } + add => this._playbackFinished.Register(value); + remove => this._playbackFinished.Unregister(value); } private readonly AsyncEvent _playbackFinished; /// /// Triggered whenever playback of a track gets stuck. /// public event AsyncEventHandler TrackStuck { - add { this._trackStuck.Register(value); } - remove { this._trackStuck.Unregister(value); } + add => this._trackStuck.Register(value); + remove => this._trackStuck.Unregister(value); } private readonly AsyncEvent _trackStuck; /// /// Triggered whenever playback of a track encounters an error. /// public event AsyncEventHandler TrackException { - add { this._trackException.Register(value); } - remove { this._trackException.Unregister(value); } + add => this._trackException.Register(value); + remove => this._trackException.Unregister(value); } private readonly AsyncEvent _trackException; /// /// Triggered whenever Discord Voice WebSocket connection is terminated. /// public event AsyncEventHandler DiscordWebSocketClosed { - add { this._webSocketClosed.Register(value); } - remove { this._webSocketClosed.Unregister(value); } + add => this._webSocketClosed.Register(value); + remove => this._webSocketClosed.Unregister(value); } private readonly AsyncEvent _webSocketClosed; /// /// Gets whether this channel is still connected. /// public bool IsConnected => !Volatile.Read(ref this._isDisposed) && this.Channel != null; - private bool _isDisposed = false; + private bool _isDisposed; /// /// Gets the current player state. /// public LavalinkPlayerState CurrentState { get; } /// /// Gets the voice channel associated with this connection. /// public DiscordChannel Channel => this.VoiceStateUpdate.Channel; /// /// Gets the guild associated with this connection. /// public DiscordGuild Guild => this.Channel.Guild; /// /// Gets the Lavalink node associated with this connection. /// public LavalinkNodeConnection Node { get; } /// /// Gets the guild id string. /// internal string GuildIdString => this.GuildId.ToString(CultureInfo.InvariantCulture); /// /// Gets the guild id. /// internal ulong GuildId => this.Channel.Guild.Id; /// /// Gets or sets the voice state update. /// internal VoiceStateUpdateEventArgs VoiceStateUpdate { get; set; } /// /// Gets or sets the voice ws disconnect tcs. /// internal TaskCompletionSource VoiceWsDisconnectTcs { get; set; } /// /// Initializes a new instance of the class. /// /// The node. /// The channel. /// The vstu. internal LavalinkGuildConnection(LavalinkNodeConnection node, DiscordChannel channel, VoiceStateUpdateEventArgs vstu) { this.Node = node; this.VoiceStateUpdate = vstu; this.CurrentState = new LavalinkPlayerState(); this.VoiceWsDisconnectTcs = new TaskCompletionSource(); Volatile.Write(ref this._isDisposed, false); this._playerUpdated = new AsyncEvent("LAVALINK_PLAYER_UPDATE", TimeSpan.Zero, this.Node.Discord.EventErrorHandler); this._playbackStarted = new AsyncEvent("LAVALINK_PLAYBACK_STARTED", TimeSpan.Zero, this.Node.Discord.EventErrorHandler); this._playbackFinished = new AsyncEvent("LAVALINK_PLAYBACK_FINISHED", TimeSpan.Zero, this.Node.Discord.EventErrorHandler); this._trackStuck = new AsyncEvent("LAVALINK_TRACK_STUCK", TimeSpan.Zero, this.Node.Discord.EventErrorHandler); this._trackException = new AsyncEvent("LAVALINK_TRACK_EXCEPTION", TimeSpan.Zero, this.Node.Discord.EventErrorHandler); this._webSocketClosed = new AsyncEvent("LAVALINK_DISCORD_WEBSOCKET_CLOSED", TimeSpan.Zero, this.Node.Discord.EventErrorHandler); } /// /// Disconnects the connection from the voice channel. /// /// Whether the connection should be destroyed on the Lavalink server when leaving. public Task DisconnectAsync(bool shouldDestroy = true) => this.DisconnectInternalAsync(shouldDestroy); /// /// Disconnects the internal async. /// /// If true, should destroy. /// If true, is manual disconnection. internal async Task DisconnectInternalAsync(bool shouldDestroy, bool isManualDisconnection = false) { if (!this.IsConnected && !isManualDisconnection) throw new InvalidOperationException("This connection is not valid."); Volatile.Write(ref this._isDisposed, true); if (shouldDestroy) await this.Node.SendPayloadAsync(new LavalinkDestroy(this)).ConfigureAwait(false); if (!isManualDisconnection) { await this.SendVoiceUpdateAsync().ConfigureAwait(false); this.ChannelDisconnected?.Invoke(this); } } /// /// Sends the voice update async. /// internal async Task SendVoiceUpdateAsync() { var vsd = new VoiceDispatch { OpCode = 4, Payload = new VoiceStateUpdatePayload { GuildId = this.GuildId, ChannelId = null, Deafened = false, Muted = false } }; var vsj = JsonConvert.SerializeObject(vsd, Formatting.None); await (this.Channel.Discord as DiscordClient).WsSendAsync(vsj).ConfigureAwait(false); } /// /// Searches for specified terms. /// /// What to search for. /// What platform will search for. /// A collection of tracks matching the criteria. public Task GetTracksAsync(string searchQuery, LavalinkSearchType type = LavalinkSearchType.Youtube) => this.Node.Rest.GetTracksAsync(searchQuery, type); /// /// Loads tracks from specified URL. /// /// URL to load tracks from. /// A collection of tracks from the URL. public Task GetTracksAsync(Uri uri) => this.Node.Rest.GetTracksAsync(uri); /// /// Loads tracks from a local file. /// /// File to load tracks from. /// A collection of tracks from the file. public Task GetTracksAsync(FileInfo file) => this.Node.Rest.GetTracksAsync(file); /// /// Queues the specified track for playback. /// /// Track to play. public async Task PlayAsync(LavalinkTrack track) { if (!this.IsConnected) throw new InvalidOperationException("This connection is not valid."); this.CurrentState.CurrentTrack = track; await this.Node.SendPayloadAsync(new LavalinkPlay(this, track)).ConfigureAwait(false); } /// /// Queues the specified track for playback. The track will be played from specified start timestamp to specified end timestamp. /// /// Track to play. /// Timestamp to start playback at. /// Timestamp to stop playback at. public async Task PlayPartialAsync(LavalinkTrack track, TimeSpan start, TimeSpan end) { if (!this.IsConnected) throw new InvalidOperationException("This connection is not valid."); if (start.TotalMilliseconds < 0 || end <= start) throw new ArgumentException("Both start and end timestamps need to be greater or equal to zero, and the end timestamp needs to be greater than start timestamp."); this.CurrentState.CurrentTrack = track; await this.Node.SendPayloadAsync(new LavalinkPlayPartial(this, track, start, end)).ConfigureAwait(false); } /// /// Stops the player completely. /// public async Task StopAsync() { if (!this.IsConnected) throw new InvalidOperationException("This connection is not valid."); await this.Node.SendPayloadAsync(new LavalinkStop(this)).ConfigureAwait(false); } /// /// Pauses the player. /// public async Task PauseAsync() { if (!this.IsConnected) throw new InvalidOperationException("This connection is not valid."); await this.Node.SendPayloadAsync(new LavalinkPause(this, true)).ConfigureAwait(false); } /// /// Resumes playback. /// public async Task ResumeAsync() { if (!this.IsConnected) throw new InvalidOperationException("This connection is not valid."); await this.Node.SendPayloadAsync(new LavalinkPause(this, false)).ConfigureAwait(false); } /// /// Seeks the current track to specified position. /// /// Position to seek to. public async Task SeekAsync(TimeSpan position) { if (!this.IsConnected) throw new InvalidOperationException("This connection is not valid."); await this.Node.SendPayloadAsync(new LavalinkSeek(this, position)).ConfigureAwait(false); } /// /// Sets the playback volume. This might incur a lot of CPU usage. /// /// Volume to set. Needs to be greater or equal to 0, and less than or equal to 100. 100 means 100% and is the default value. public async Task SetVolumeAsync(int volume) { if (!this.IsConnected) throw new InvalidOperationException("This connection is not valid."); if (volume < 0 || volume > 1000) throw new ArgumentOutOfRangeException(nameof(volume), "Volume needs to range from 0 to 1000."); await this.Node.SendPayloadAsync(new LavalinkVolume(this, volume)).ConfigureAwait(false); } /// /// Adjusts the specified bands in the audio equalizer. This will alter the sound output, and might incur a lot of CPU usage. /// /// Bands adjustments to make. You must specify one adjustment per band at most. public async Task AdjustEqualizerAsync(params LavalinkBandAdjustment[] bands) { if (!this.IsConnected) throw new InvalidOperationException("This connection is not valid."); if (bands?.Any() != true) return; if (bands.Distinct(new LavalinkBandAdjustmentComparer()).Count() != bands.Count()) throw new InvalidOperationException("You cannot specify multiple modifiers for the same band."); await this.Node.SendPayloadAsync(new LavalinkEqualizer(this, bands)).ConfigureAwait(false); } /// /// Resets the audio equalizer to default values. /// public async Task ResetEqualizerAsync() { if (!this.IsConnected) throw new InvalidOperationException("This connection is not valid."); await this.Node.SendPayloadAsync(new LavalinkEqualizer(this, Enumerable.Range(0, 15).Select(x => new LavalinkBandAdjustment(x, 0)))).ConfigureAwait(false); } /// /// Internals the update player state async. /// /// The new state. internal Task InternalUpdatePlayerStateAsync(LavalinkState newState) { this.CurrentState.LastUpdate = newState.Time; this.CurrentState.PlaybackPosition = newState.Position; return this._playerUpdated.InvokeAsync(this, new PlayerUpdateEventArgs(this, newState.Time, newState.Position)); } /// /// Internals the playback started async. /// /// The track. internal Task InternalPlaybackStartedAsync(string track) { var ea = new TrackStartEventArgs(this, LavalinkUtilities.DecodeTrack(track)); return this._playbackStarted.InvokeAsync(this, ea); } /// /// Internals the playback finished async. /// /// The e. internal Task InternalPlaybackFinishedAsync(TrackFinishData e) { if (e.Reason != TrackEndReason.Replaced) this.CurrentState.CurrentTrack = default; var ea = new TrackFinishEventArgs(this, LavalinkUtilities.DecodeTrack(e.Track), e.Reason); return this._playbackFinished.InvokeAsync(this, ea); } /// /// Internals the track stuck async. /// /// The e. internal Task InternalTrackStuckAsync(TrackStuckData e) { var ea = new TrackStuckEventArgs(this, e.Threshold, LavalinkUtilities.DecodeTrack(e.Track)); return this._trackStuck.InvokeAsync(this, ea); } /// /// Internals the track exception async. /// /// The e. internal Task InternalTrackExceptionAsync(TrackExceptionData e) { var ea = new TrackExceptionEventArgs(this, e.Error, LavalinkUtilities.DecodeTrack(e.Track)); return this._trackException.InvokeAsync(this, ea); } /// /// Internals the web socket closed async. /// /// The e. internal Task InternalWebSocketClosedAsync(WebSocketCloseEventArgs e) => this._webSocketClosed.InvokeAsync(this, e); internal event ChannelDisconnectedEventHandler ChannelDisconnected; } } diff --git a/DisCatSharp.Lavalink/LavalinkNodeConnection.cs b/DisCatSharp.Lavalink/LavalinkNodeConnection.cs index fd18e688d..f704d7db5 100644 --- a/DisCatSharp.Lavalink/LavalinkNodeConnection.cs +++ b/DisCatSharp.Lavalink/LavalinkNodeConnection.cs @@ -1,606 +1,606 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; using DisCatSharp.Common.Utilities; using DisCatSharp.Entities; using DisCatSharp.EventArgs; using DisCatSharp.Lavalink.Entities; using DisCatSharp.Lavalink.EventArgs; using DisCatSharp.Net; using DisCatSharp.Net.WebSocket; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace DisCatSharp.Lavalink { internal delegate void NodeDisconnectedEventHandler(LavalinkNodeConnection node); /// /// Represents a connection to a Lavalink node. /// public sealed class LavalinkNodeConnection { /// /// Triggered whenever Lavalink WebSocket throws an exception. /// public event AsyncEventHandler LavalinkSocketErrored { - add { this._lavalinkSocketError.Register(value); } - remove { this._lavalinkSocketError.Unregister(value); } + add => this._lavalinkSocketError.Register(value); + remove => this._lavalinkSocketError.Unregister(value); } private readonly AsyncEvent _lavalinkSocketError; /// /// Triggered when this node disconnects. /// public event AsyncEventHandler Disconnected { - add { this._disconnected.Register(value); } - remove { this._disconnected.Unregister(value); } + add => this._disconnected.Register(value); + remove => this._disconnected.Unregister(value); } private readonly AsyncEvent _disconnected; /// /// Triggered when this node receives a statistics update. /// public event AsyncEventHandler StatisticsReceived { - add { this._statsReceived.Register(value); } - remove { this._statsReceived.Unregister(value); } + add => this._statsReceived.Register(value); + remove => this._statsReceived.Unregister(value); } private readonly AsyncEvent _statsReceived; /// /// Triggered whenever any of the players on this node is updated. /// public event AsyncEventHandler PlayerUpdated { - add { this._playerUpdated.Register(value); } - remove { this._playerUpdated.Unregister(value); } + add => this._playerUpdated.Register(value); + remove => this._playerUpdated.Unregister(value); } private readonly AsyncEvent _playerUpdated; /// /// Triggered whenever playback of a track starts. /// This is only available for version 3.3.1 and greater. /// public event AsyncEventHandler PlaybackStarted { - add { this._playbackStarted.Register(value); } - remove { this._playbackStarted.Unregister(value); } + add => this._playbackStarted.Register(value); + remove => this._playbackStarted.Unregister(value); } private readonly AsyncEvent _playbackStarted; /// /// Triggered whenever playback of a track finishes. /// public event AsyncEventHandler PlaybackFinished { - add { this._playbackFinished.Register(value); } - remove { this._playbackFinished.Unregister(value); } + add => this._playbackFinished.Register(value); + remove => this._playbackFinished.Unregister(value); } private readonly AsyncEvent _playbackFinished; /// /// Triggered whenever playback of a track gets stuck. /// public event AsyncEventHandler TrackStuck { - add { this._trackStuck.Register(value); } - remove { this._trackStuck.Unregister(value); } + add => this._trackStuck.Register(value); + remove => this._trackStuck.Unregister(value); } private readonly AsyncEvent _trackStuck; /// /// Triggered whenever playback of a track encounters an error. /// public event AsyncEventHandler TrackException { - add { this._trackException.Register(value); } - remove { this._trackException.Unregister(value); } + add => this._trackException.Register(value); + remove => this._trackException.Unregister(value); } private readonly AsyncEvent _trackException; /// /// Gets the remote endpoint of this Lavalink node connection. /// public ConnectionEndpoint NodeEndpoint => this.Configuration.SocketEndpoint; /// /// Gets whether the client is connected to Lavalink. /// public bool IsConnected => !Volatile.Read(ref this._isDisposed); - private bool _isDisposed = false; - private int _backoff = 0; + private bool _isDisposed; + private int _backoff; /// /// The minimum backoff. /// private const int MINIMUM_BACKOFF = 7500; /// /// The maximum backoff. /// private const int MAXIMUM_BACKOFF = 120000; /// /// Gets the current resource usage statistics. /// public LavalinkStatistics Statistics { get; } /// /// Gets a dictionary of Lavalink guild connections for this node. /// public IReadOnlyDictionary ConnectedGuilds { get; } internal ConcurrentDictionary ConnectedGuildsInternal = new(); /// /// Gets the REST client for this Lavalink connection. /// public LavalinkRestClient Rest { get; } /// /// Gets the parent extension which this node connection belongs to. /// public LavalinkExtension Parent { get; } /// /// Gets the Discord client this node connection belongs to. /// public DiscordClient Discord { get; } /// /// Gets the configuration. /// internal LavalinkConfiguration Configuration { get; } /// /// Gets the region. /// internal DiscordVoiceRegion Region { get; } /// /// Gets or sets the web socket. /// private IWebSocketClient _webSocket; /// /// Gets the voice state updates. /// private readonly ConcurrentDictionary> _voiceStateUpdates; /// /// Gets the voice server updates. /// private readonly ConcurrentDictionary> _voiceServerUpdates; /// /// Initializes a new instance of the class. /// /// The client. /// the event.tension. /// The config. internal LavalinkNodeConnection(DiscordClient client, LavalinkExtension extension, LavalinkConfiguration config) { this.Discord = client; this.Parent = extension; this.Configuration = new LavalinkConfiguration(config); if (config.Region != null && this.Discord.VoiceRegions.Values.Contains(config.Region)) this.Region = config.Region; this.ConnectedGuilds = new ReadOnlyConcurrentDictionary(this.ConnectedGuildsInternal); this.Statistics = new LavalinkStatistics(); this._lavalinkSocketError = new AsyncEvent("LAVALINK_SOCKET_ERROR", TimeSpan.Zero, this.Discord.EventErrorHandler); this._disconnected = new AsyncEvent("LAVALINK_NODE_DISCONNECTED", TimeSpan.Zero, this.Discord.EventErrorHandler); this._statsReceived = new AsyncEvent("LAVALINK_STATS_RECEIVED", TimeSpan.Zero, this.Discord.EventErrorHandler); this._playerUpdated = new AsyncEvent("LAVALINK_PLAYER_UPDATED", TimeSpan.Zero, this.Discord.EventErrorHandler); this._playbackStarted = new AsyncEvent("LAVALINK_PLAYBACK_STARTED", TimeSpan.Zero, this.Discord.EventErrorHandler); this._playbackFinished = new AsyncEvent("LAVALINK_PLAYBACK_FINISHED", TimeSpan.Zero, this.Discord.EventErrorHandler); this._trackStuck = new AsyncEvent("LAVALINK_TRACK_STUCK", TimeSpan.Zero, this.Discord.EventErrorHandler); this._trackException = new AsyncEvent("LAVALINK_TRACK_EXCEPTION", TimeSpan.Zero, this.Discord.EventErrorHandler); this._voiceServerUpdates = new ConcurrentDictionary>(); this._voiceStateUpdates = new ConcurrentDictionary>(); this.Discord.VoiceStateUpdated += this.Discord_VoiceStateUpdated; this.Discord.VoiceServerUpdated += this.Discord_VoiceServerUpdated; this.Rest = new LavalinkRestClient(this.Configuration, this.Discord); Volatile.Write(ref this._isDisposed, false); } /// /// Establishes a connection to the Lavalink node. /// /// internal async Task StartAsync() { if (this.Discord?.CurrentUser?.Id == null || this.Discord?.ShardCount == null) throw new InvalidOperationException("This operation requires the Discord client to be fully initialized."); this._webSocket = this.Discord.Configuration.WebSocketClientFactory(this.Discord.Configuration.Proxy, this.Discord.ServiceProvider); this._webSocket.Connected += this.WebSocket_OnConnect; this._webSocket.Disconnected += this.WebSocket_OnDisconnect; this._webSocket.ExceptionThrown += this.WebSocket_OnException; this._webSocket.MessageReceived += this.WebSocket_OnMessage; this._webSocket.AddDefaultHeader("Authorization", this.Configuration.Password); this._webSocket.AddDefaultHeader("Num-Shards", this.Discord.ShardCount.ToString(CultureInfo.InvariantCulture)); this._webSocket.AddDefaultHeader("User-Id", this.Discord.CurrentUser.Id.ToString(CultureInfo.InvariantCulture)); if (this.Configuration.ResumeKey != null) this._webSocket.AddDefaultHeader("Resume-Key", this.Configuration.ResumeKey); do { try { if (this._backoff != 0) { await Task.Delay(this._backoff).ConfigureAwait(false); this._backoff = Math.Min(this._backoff * 2, MAXIMUM_BACKOFF); } else { this._backoff = MINIMUM_BACKOFF; } await this._webSocket.ConnectAsync(new Uri(this.Configuration.SocketEndpoint.ToWebSocketString())).ConfigureAwait(false); break; } catch (PlatformNotSupportedException) { throw; } catch (NotImplementedException) { throw; } catch (Exception ex) { if (!this.Configuration.SocketAutoReconnect || this._backoff == MAXIMUM_BACKOFF) { this.Discord.Logger.LogCritical(LavalinkEvents.LavalinkConnectionError, ex, "Failed to connect to Lavalink."); throw ex; } else { this.Discord.Logger.LogCritical(LavalinkEvents.LavalinkConnectionError, ex, $"Failed to connect to Lavalink, retrying in {this._backoff} ms."); } } } while (this.Configuration.SocketAutoReconnect); Volatile.Write(ref this._isDisposed, false); } /// /// Stops this Lavalink node connection and frees resources. /// /// public async Task StopAsync() { foreach (var kvp in this.ConnectedGuildsInternal) await kvp.Value.DisconnectAsync().ConfigureAwait(false); this.NodeDisconnected?.Invoke(this); Volatile.Write(ref this._isDisposed, true); await this._webSocket.DisconnectAsync().ConfigureAwait(false); // this should not be here, no? //await this._disconnected.InvokeAsync(this, new NodeDisconnectedEventArgs(this)).ConfigureAwait(false); } /// /// Connects this Lavalink node to specified Discord channel. /// /// Voice channel to connect to. /// Channel connection, which allows for playback control. public async Task ConnectAsync(DiscordChannel channel) { if (this.ConnectedGuildsInternal.ContainsKey(channel.Guild.Id)) return this.ConnectedGuildsInternal[channel.Guild.Id]; if (channel.Guild == null || (channel.Type != ChannelType.Voice && channel.Type != ChannelType.Stage)) throw new ArgumentException("Invalid channel specified.", nameof(channel)); var vstut = new TaskCompletionSource(); var vsrut = new TaskCompletionSource(); this._voiceStateUpdates[channel.Guild.Id] = vstut; this._voiceServerUpdates[channel.Guild.Id] = vsrut; var vsd = new VoiceDispatch { OpCode = 4, Payload = new VoiceStateUpdatePayload { GuildId = channel.Guild.Id, ChannelId = channel.Id, Deafened = false, Muted = false } }; var vsj = JsonConvert.SerializeObject(vsd, Formatting.None); await (channel.Discord as DiscordClient).WsSendAsync(vsj).ConfigureAwait(false); var vstu = await vstut.Task.ConfigureAwait(false); var vsru = await vsrut.Task.ConfigureAwait(false); await this.SendPayloadAsync(new LavalinkVoiceUpdate(vstu, vsru)).ConfigureAwait(false); var con = new LavalinkGuildConnection(this, channel, vstu); con.ChannelDisconnected += this.Con_ChannelDisconnected; con.PlayerUpdated += (s, e) => this._playerUpdated.InvokeAsync(s, e); con.PlaybackStarted += (s, e) => this._playbackStarted.InvokeAsync(s, e); con.PlaybackFinished += (s, e) => this._playbackFinished.InvokeAsync(s, e); con.TrackStuck += (s, e) => this._trackStuck.InvokeAsync(s, e); con.TrackException += (s, e) => this._trackException.InvokeAsync(s, e); this.ConnectedGuildsInternal[channel.Guild.Id] = con; return con; } /// /// Gets a Lavalink connection to specified Discord channel. /// /// Guild to get connection for. /// Channel connection, which allows for playback control. public LavalinkGuildConnection GetGuildConnection(DiscordGuild guild) => this.ConnectedGuildsInternal.TryGetValue(guild.Id, out var lgc) && lgc.IsConnected ? lgc : null; /// /// Sends the payload async. /// /// The payload. internal async Task SendPayloadAsync(LavalinkPayload payload) => await this.WsSendAsync(JsonConvert.SerializeObject(payload, Formatting.None)).ConfigureAwait(false); /// /// Webs the socket_ on message. /// /// The client. /// the event.ent. private async Task WebSocket_OnMessage(IWebSocketClient client, SocketMessageEventArgs e) { if (e is not SocketTextMessageEventArgs et) { this.Discord.Logger.LogCritical(LavalinkEvents.LavalinkConnectionError, "Lavalink sent binary data - unable to process"); return; } this.Discord.Logger.LogTrace(LavalinkEvents.LavalinkWsRx, et.Message); var json = et.Message; var jsonData = JObject.Parse(json); switch (jsonData["op"].ToString()) { case "playerUpdate": var gid = (ulong)jsonData["guildId"]; var state = jsonData["state"].ToObject(); if (this.ConnectedGuildsInternal.TryGetValue(gid, out var lvl)) await lvl.InternalUpdatePlayerStateAsync(state).ConfigureAwait(false); break; case "stats": var statsRaw = jsonData.ToObject(); this.Statistics.Update(statsRaw); await this._statsReceived.InvokeAsync(this, new StatisticsReceivedEventArgs(this.Discord.ServiceProvider, this.Statistics)).ConfigureAwait(false); break; case "event": var evtype = jsonData["type"].ToObject(); var guildId = (ulong)jsonData["guildId"]; switch (evtype) { case EventType.TrackStartEvent: if (this.ConnectedGuildsInternal.TryGetValue(guildId, out var lvlEvtst)) await lvlEvtst.InternalPlaybackStartedAsync(jsonData["track"].ToString()).ConfigureAwait(false); break; case EventType.TrackEndEvent: var reason = TrackEndReason.Cleanup; switch (jsonData["reason"].ToString()) { case "FINISHED": reason = TrackEndReason.Finished; break; case "LOAD_FAILED": reason = TrackEndReason.LoadFailed; break; case "STOPPED": reason = TrackEndReason.Stopped; break; case "REPLACED": reason = TrackEndReason.Replaced; break; case "CLEANUP": reason = TrackEndReason.Cleanup; break; } if (this.ConnectedGuildsInternal.TryGetValue(guildId, out var lvlEvtf)) await lvlEvtf.InternalPlaybackFinishedAsync(new TrackFinishData { Track = jsonData["track"].ToString(), Reason = reason }).ConfigureAwait(false); break; case EventType.TrackStuckEvent: if (this.ConnectedGuildsInternal.TryGetValue(guildId, out var lvlEvts)) await lvlEvts.InternalTrackStuckAsync(new TrackStuckData { Track = jsonData["track"].ToString(), Threshold = (long)jsonData["thresholdMs"] }).ConfigureAwait(false); break; case EventType.TrackExceptionEvent: if (this.ConnectedGuildsInternal.TryGetValue(guildId, out var lvlEvte)) await lvlEvte.InternalTrackExceptionAsync(new TrackExceptionData { Track = jsonData["track"].ToString(), Error = jsonData["error"].ToString() }).ConfigureAwait(false); break; case EventType.WebSocketClosedEvent: if (this.ConnectedGuildsInternal.TryGetValue(guildId, out var lvlEwsce)) { lvlEwsce.VoiceWsDisconnectTcs.SetResult(true); await lvlEwsce.InternalWebSocketClosedAsync(new WebSocketCloseEventArgs(jsonData["code"].ToObject(), jsonData["reason"].ToString(), jsonData["byRemote"].ToObject(), this.Discord.ServiceProvider)).ConfigureAwait(false); } break; } break; } } /// /// Webs the socket_ on exception. /// /// The client. /// the event. private Task WebSocket_OnException(IWebSocketClient client, SocketErrorEventArgs e) => this._lavalinkSocketError.InvokeAsync(this, new SocketErrorEventArgs(client.ServiceProvider) { Exception = e.Exception }); /// /// Webs the socket_ on disconnect. /// /// The client. /// the event. private async Task WebSocket_OnDisconnect(IWebSocketClient client, SocketCloseEventArgs e) { if (this.IsConnected && e.CloseCode != 1001 && e.CloseCode != -1) { this.Discord.Logger.LogWarning(LavalinkEvents.LavalinkConnectionClosed, "Connection broken ({0}, '{1}'), reconnecting", e.CloseCode, e.CloseMessage); await this._disconnected.InvokeAsync(this, new NodeDisconnectedEventArgs(this, false)).ConfigureAwait(false); if (this.Configuration.SocketAutoReconnect) await this.StartAsync().ConfigureAwait(false); } else if (e.CloseCode != 1001 && e.CloseCode != -1) { this.Discord.Logger.LogInformation(LavalinkEvents.LavalinkConnectionClosed, "Connection closed ({0}, '{1}')", e.CloseCode, e.CloseMessage); this.NodeDisconnected?.Invoke(this); await this._disconnected.InvokeAsync(this, new NodeDisconnectedEventArgs(this, true)).ConfigureAwait(false); } else { Volatile.Write(ref this._isDisposed, true); this.Discord.Logger.LogWarning(LavalinkEvents.LavalinkConnectionClosed, "Lavalink died"); foreach (var kvp in this.ConnectedGuildsInternal) { await kvp.Value.SendVoiceUpdateAsync().ConfigureAwait(false); _ = this.ConnectedGuildsInternal.TryRemove(kvp.Key, out _); } this.NodeDisconnected?.Invoke(this); await this._disconnected.InvokeAsync(this, new NodeDisconnectedEventArgs(this, false)).ConfigureAwait(false); if (this.Configuration.SocketAutoReconnect) await this.StartAsync().ConfigureAwait(false); } } /// /// Webs the socket_ on connect. /// /// The client. /// the event.. private async Task WebSocket_OnConnect(IWebSocketClient client, SocketEventArgs ea) { this.Discord.Logger.LogDebug(LavalinkEvents.LavalinkConnected, "Connection to Lavalink node established"); this._backoff = 0; if (this.Configuration.ResumeKey != null) await this.SendPayloadAsync(new LavalinkConfigureResume(this.Configuration.ResumeKey, this.Configuration.ResumeTimeout)).ConfigureAwait(false); } /// /// Con_S the channel disconnected. /// /// The con. private void Con_ChannelDisconnected(LavalinkGuildConnection con) => this.ConnectedGuildsInternal.TryRemove(con.GuildId, out _); /// /// Discord voice state updated. /// /// The client. /// the event. private Task Discord_VoiceStateUpdated(DiscordClient client, VoiceStateUpdateEventArgs e) { var gld = e.Guild; if (gld == null) return Task.CompletedTask; if (e.User == null) return Task.CompletedTask; if (e.User.Id == this.Discord.CurrentUser.Id) { if (this.ConnectedGuildsInternal.TryGetValue(e.Guild.Id, out var lvlgc)) lvlgc.VoiceStateUpdate = e; if (e.After.Channel == null && this.IsConnected && this.ConnectedGuildsInternal.ContainsKey(gld.Id)) { _ = Task.Run(async () => { var delayTask = Task.Delay(this.Configuration.WebSocketCloseTimeout); var tcs = lvlgc.VoiceWsDisconnectTcs.Task; _ = await Task.WhenAny(delayTask, tcs).ConfigureAwait(false); await lvlgc.DisconnectInternalAsync(false, true).ConfigureAwait(false); _ = this.ConnectedGuildsInternal.TryRemove(gld.Id, out _); }); } if (!string.IsNullOrWhiteSpace(e.SessionId) && e.Channel != null && this._voiceStateUpdates.TryRemove(gld.Id, out var xe)) xe.SetResult(e); } return Task.CompletedTask; } /// /// Discord voice server updated. /// /// The client. /// the event. private Task Discord_VoiceServerUpdated(DiscordClient client, VoiceServerUpdateEventArgs e) { var gld = e.Guild; if (gld == null) return Task.CompletedTask; if (this.ConnectedGuildsInternal.TryGetValue(e.Guild.Id, out var lvlgc)) { var lvlp = new LavalinkVoiceUpdate(lvlgc.VoiceStateUpdate, e); _ = Task.Run(() => this.WsSendAsync(JsonConvert.SerializeObject(lvlp))); } if (this._voiceServerUpdates.TryRemove(gld.Id, out var xe)) xe.SetResult(e); return Task.CompletedTask; } /// /// Ws the send async. /// /// The payload. private async Task WsSendAsync(string payload) { this.Discord.Logger.LogTrace(LavalinkEvents.LavalinkWsTx, payload); await this._webSocket.SendMessageAsync(payload).ConfigureAwait(false); } internal event NodeDisconnectedEventHandler NodeDisconnected; } } diff --git a/DisCatSharp.Lavalink/LavalinkUtil.cs b/DisCatSharp.Lavalink/LavalinkUtil.cs index 86aae4fc5..633619545 100644 --- a/DisCatSharp.Lavalink/LavalinkUtil.cs +++ b/DisCatSharp.Lavalink/LavalinkUtil.cs @@ -1,285 +1,285 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.IO; using System.Text; using DisCatSharp.Lavalink.EventArgs; namespace DisCatSharp.Lavalink { /// /// Various utilities for Lavalink. /// public static class LavalinkUtilities { /// /// Indicates whether a new track should be started after reciving this TrackEndReason. If this is false, either this event is /// already triggered because another track started (REPLACED) or because the player is stopped (STOPPED, CLEANUP). /// public static bool MayStartNext(this TrackEndReason reason) => reason == TrackEndReason.Finished || reason == TrackEndReason.LoadFailed; /// /// Decodes a Lavalink track string. /// /// Track string to decode. /// Decoded Lavalink track. public static LavalinkTrack DecodeTrack(string track) { // https://github.com/sedmelluq/lavaplayer/blob/804cd1038229230052d9b1dee5e6d1741e30e284/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/DefaultAudioPlayerManager.java#L63-L64 const int TRACK_INFO_VERSIONED = 1; //const int TRACK_INFO_VERSION = 2; var raw = Convert.FromBase64String(track); var decoded = new LavalinkTrack { TrackString = track }; using (var ms = new MemoryStream(raw)) using (var br = new JavaBinaryReader(ms)) { // https://github.com/sedmelluq/lavaplayer/blob/b0c536098c4f92e6d03b00f19221021f8f50b19b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/io/MessageInput.java#L37-L39 var messageHeader = br.ReadInt32(); var messageFlags = (int) ((messageHeader & 0xC0000000L) >> 30); var messageSize = messageHeader & 0x3FFFFFFF; //if (messageSize != raw.Length) // Warn($"Size conflict: {messageSize} but was {raw.Length}"); // https://github.com/sedmelluq/lavaplayer/blob/804cd1038229230052d9b1dee5e6d1741e30e284/main/src/main/java/com/sedmelluq/discord/lavaplayer/player/DefaultAudioPlayerManager.java#L268 // java bytes are signed // https://docs.oracle.com/javase/7/docs/api/java/io/DataInput.html#readByte() - var version = (messageFlags & TRACK_INFO_VERSIONED) != 0 ? (br.ReadSByte() & 0xFF) : 1; + var version = (messageFlags & TRACK_INFO_VERSIONED) != 0 ? br.ReadSByte() & 0xFF : 1; //if (version != TRACK_INFO_VERSION) // Warn($"Version conflict: Expected {TRACK_INFO_VERSION} but got {version}"); decoded.Title = br.ReadJavaUtf8(); decoded.Author = br.ReadJavaUtf8(); decoded.LengthInternal = br.ReadInt64(); decoded.Identifier = br.ReadJavaUtf8(); decoded.IsStream = br.ReadBoolean(); var uri = br.ReadNullableString(); decoded.Uri = uri != null && version >= 2 ? new Uri(uri) : null; } return decoded; } } /// /// /// Java's DataOutputStream always uses big-endian, while BinaryReader always uses little-endian. /// This class converts a big-endian stream to little-endian, and includes some helper methods /// for interacting with Lavaplayer/Lavalink. /// internal class JavaBinaryReader : BinaryReader { private static readonly Encoding s_utf8NoBom = new UTF8Encoding(); /// /// Initializes a new instance of the class. /// /// The ms. public JavaBinaryReader(Stream ms) : base(ms, s_utf8NoBom) { } // https://docs.oracle.com/javase/7/docs/api/java/io/DataInput.html#readUTF() /// /// Reads the java utf8. /// /// A string. public string ReadJavaUtf8() { var length = this.ReadUInt16(); // string size in bytes var bytes = new byte[length]; var amountRead = this.Read(bytes, 0, length); if (amountRead < length) throw new InvalidDataException("EOS unexpected"); var output = new char[length]; var strlen = 0; // i'm gonna blindly assume here that the javadocs had the correct endianness for (var i = 0; i < length; i++) { var value1 = bytes[i]; if ((value1 & 0b10000000) == 0) // highest bit 1 is false { output[strlen++] = (char)value1; continue; } // remember to skip one byte for every extra byte var value2 = bytes[++i]; if ((value1 & 0b00100000) == 0 && // highest bit 3 is false (value1 & 0b11000000) != 0 && // highest bit 1 and 2 are true (value2 & 0b01000000) == 0 && // highest bit 2 is false (value2 & 0b10000000) != 0) // highest bit 1 is true { var value1Chop = (value1 & 0b00011111) << 6; var value2Chop = value2 & 0b00111111; output[strlen++] = (char)(value1Chop | value2Chop); continue; } var value3 = bytes[++i]; if ((value1 & 0b00010000) == 0 && // highest bit 4 is false (value1 & 0b11100000) != 0 && // highest bit 1,2,3 are true (value2 & 0b01000000) == 0 && // highest bit 2 is false (value2 & 0b10000000) != 0 && // highest bit 1 is true (value3 & 0b01000000) == 0 && // highest bit 2 is false (value3 & 0b10000000) != 0) // highest bit 1 is true { var value1Chop = (value1 & 0b00001111) << 12; var value2Chop = (value2 & 0b00111111) << 6; var value3Chop = value3 & 0b00111111; output[strlen++] = (char)(value1Chop | value2Chop | value3Chop); continue; } } return new string(output, 0, strlen); } // https://github.com/sedmelluq/lavaplayer/blob/b0c536098c4f92e6d03b00f19221021f8f50b19b/main/src/main/java/com/sedmelluq/discord/lavaplayer/tools/DataFormatTools.java#L114-L125 /// /// Reads the nullable string. /// /// A string. public string ReadNullableString() => this.ReadBoolean() ? this.ReadJavaUtf8() : null; // swap endianness /// /// Reads the decimal. /// /// A decimal. public override decimal ReadDecimal() => throw new MissingMethodException("This method does not have a Java equivalent"); // from https://github.com/Zoltu/Zoltu.EndianAwareBinaryReaderWriter under CC0 /// /// Reads the single. /// /// A float. public override float ReadSingle() => this.Read(4, BitConverter.ToSingle); /// /// Reads the double. /// /// A double. public override double ReadDouble() => this.Read(8, BitConverter.ToDouble); /// /// Reads the int16. /// /// A short. public override short ReadInt16() => this.Read(2, BitConverter.ToInt16); /// /// Reads the int32. /// /// An int. public override int ReadInt32() => this.Read(4, BitConverter.ToInt32); /// /// Reads the int64. /// /// A long. public override long ReadInt64() => this.Read(8, BitConverter.ToInt64); /// /// Reads the u int16. /// /// An ushort. public override ushort ReadUInt16() => this.Read(2, BitConverter.ToUInt16); /// /// Reads the u int32. /// /// An uint. public override uint ReadUInt32() => this.Read(4, BitConverter.ToUInt32); /// /// Reads the u int64. /// /// An ulong. public override ulong ReadUInt64() => this.Read(8, BitConverter.ToUInt64); /// /// Reads the. /// /// The size. /// The converter. /// A T. private T Read(int size, Func converter) where T : struct { //Contract.Requires(size >= 0); //Contract.Requires(converter != null); var bytes = this.GetNextBytesNativeEndian(size); return converter(bytes, 0); } /// /// Gets the next bytes native endian. /// /// The count. /// An array of byte. private byte[] GetNextBytesNativeEndian(int count) { //Contract.Requires(count >= 0); //Contract.Ensures(Contract.Result() != null); //Contract.Ensures(Contract.Result().Length == count); var bytes = this.GetNextBytes(count); if (BitConverter.IsLittleEndian) Array.Reverse(bytes); return bytes; } /// /// Gets the next bytes. /// /// The count. /// An array of byte. private byte[] GetNextBytes(int count) { //Contract.Requires(count >= 0); //Contract.Ensures(Contract.Result() != null); //Contract.Ensures(Contract.Result().Length == count); var buffer = new byte[count]; var bytesRead = this.BaseStream.Read(buffer, 0, count); return bytesRead != count ? throw new EndOfStreamException() : buffer; } } } diff --git a/DisCatSharp.VoiceNext/AudioFormat.cs b/DisCatSharp.VoiceNext/AudioFormat.cs index d3f4589bc..8a16677fd 100644 --- a/DisCatSharp.VoiceNext/AudioFormat.cs +++ b/DisCatSharp.VoiceNext/AudioFormat.cs @@ -1,164 +1,164 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Runtime.CompilerServices; namespace DisCatSharp.VoiceNext { /// /// Defines the format of PCM data consumed or produced by Opus. /// public struct AudioFormat { /// /// Gets the collection of sampling rates (in Hz) the Opus encoder can use. /// public static IReadOnlyCollection AllowedSampleRates { get; } = new ReadOnlyCollection(new[] { 8000, 12000, 16000, 24000, 48000 }); /// /// Gets the collection of channel counts the Opus encoder can use. /// public static IReadOnlyCollection AllowedChannelCounts { get; } = new ReadOnlyCollection(new[] { 1, 2 }); /// /// Gets the collection of sample durations (in ms) the Opus encoder can use. /// public static IReadOnlyCollection AllowedSampleDurations { get; } = new ReadOnlyCollection(new[] { 5, 10, 20, 40, 60 }); /// /// Gets the default audio format. This is a formt configured for 48kHz sampling rate, 2 channels, with music quality preset. /// - public static AudioFormat Default { get; } = new AudioFormat(48000, 2, VoiceApplication.Music); + public static AudioFormat Default { get; } = new(48000, 2, VoiceApplication.Music); /// /// Gets the audio sampling rate in Hz. /// public int SampleRate { get; } /// /// Gets the audio channel count. /// public int ChannelCount { get; } /// /// Gets the voice application, which dictates the quality preset. /// public VoiceApplication VoiceApplication { get; } /// /// Creates a new audio format for use with Opus encoder. /// /// Audio sampling rate in Hz. /// Number of audio channels in the data. /// Encoder preset to use. public AudioFormat(int sampleRate = 48000, int channelCount = 2, VoiceApplication voiceApplication = VoiceApplication.Music) { if (!AllowedSampleRates.Contains(sampleRate)) throw new ArgumentOutOfRangeException(nameof(sampleRate), "Invalid sample rate specified."); if (!AllowedChannelCounts.Contains(channelCount)) throw new ArgumentOutOfRangeException(nameof(channelCount), "Invalid channel count specified."); if (voiceApplication != VoiceApplication.Music && voiceApplication != VoiceApplication.Voice && voiceApplication != VoiceApplication.LowLatency) throw new ArgumentOutOfRangeException(nameof(voiceApplication), "Invalid voice application specified."); this.SampleRate = sampleRate; this.ChannelCount = channelCount; this.VoiceApplication = voiceApplication; } /// /// Calculates a sample size in bytes. /// /// Millsecond duration of a sample. /// Calculated sample size in bytes. [MethodImpl(MethodImplOptions.AggressiveInlining)] public int CalculateSampleSize(int sampleDuration) { if (!AllowedSampleDurations.Contains(sampleDuration)) throw new ArgumentOutOfRangeException(nameof(sampleDuration), "Invalid sample duration specified."); // Sample size in bytes is a product of the following: // - duration in milliseconds // - number of channels // - sample rate in kHz // - size of data (in this case, sizeof(int16_t)) // which comes down to below: return sampleDuration * this.ChannelCount * (this.SampleRate / 1000) * 2; } /// /// Gets the maximum buffer size for decoding. This method should be called when decoding Opus data to PCM, to ensure sufficient buffer size. /// /// Buffer size required to decode data. [MethodImpl(MethodImplOptions.AggressiveInlining)] public int GetMaximumBufferSize() => this.CalculateMaximumFrameSize(); /// /// Calculates the sample duration. /// /// The sample size. /// An int. [MethodImpl(MethodImplOptions.AggressiveInlining)] internal int CalculateSampleDuration(int sampleSize) => sampleSize / (this.SampleRate / 1000) / this.ChannelCount / 2 /* sizeof(int16_t) */; /// /// Calculates the frame size. /// /// The sample duration. /// An int. [MethodImpl(MethodImplOptions.AggressiveInlining)] internal int CalculateFrameSize(int sampleDuration) => sampleDuration * (this.SampleRate / 1000); /// /// Calculates the maximum frame size. /// /// An int. [MethodImpl(MethodImplOptions.AggressiveInlining)] internal int CalculateMaximumFrameSize() => 120 * (this.SampleRate / 1000); /// /// Samples the count to sample size. /// /// The sample count. /// An int. [MethodImpl(MethodImplOptions.AggressiveInlining)] internal int SampleCountToSampleSize(int sampleCount) => sampleCount * this.ChannelCount * 2 /* sizeof(int16_t) */; /// /// Are the valid. /// /// A bool. internal bool IsValid() => AllowedSampleRates.Contains(this.SampleRate) && AllowedChannelCounts.Contains(this.ChannelCount) && (this.VoiceApplication == VoiceApplication.Music || this.VoiceApplication == VoiceApplication.Voice || this.VoiceApplication == VoiceApplication.LowLatency); } } diff --git a/DisCatSharp.VoiceNext/Codec/Opus.cs b/DisCatSharp.VoiceNext/Codec/Opus.cs index 1d26a4394..621d4665f 100644 --- a/DisCatSharp.VoiceNext/Codec/Opus.cs +++ b/DisCatSharp.VoiceNext/Codec/Opus.cs @@ -1,288 +1,288 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Generic; namespace DisCatSharp.VoiceNext.Codec { /// /// The opus. /// internal sealed class Opus : IDisposable { /// /// Gets the audio format. /// public AudioFormat AudioFormat { get; } /// /// Gets the encoder. /// private readonly IntPtr _encoder; /// /// Gets the managed decoders. /// private readonly List _managedDecoders; /// /// Initializes a new instance of the class. /// /// The audio format. public Opus(AudioFormat audioFormat) { if (!audioFormat.IsValid()) throw new ArgumentException("Invalid audio format specified.", nameof(audioFormat)); this.AudioFormat = audioFormat; this._encoder = Interop.OpusCreateEncoder(this.AudioFormat); // Set appropriate encoder options var sig = OpusSignal.Auto; switch (this.AudioFormat.VoiceApplication) { case VoiceApplication.Music: sig = OpusSignal.Music; break; case VoiceApplication.Voice: sig = OpusSignal.Voice; break; } Interop.OpusSetEncoderOption(this._encoder, OpusControl.SetSignal, (int)sig); Interop.OpusSetEncoderOption(this._encoder, OpusControl.SetPacketLossPercent, 15); Interop.OpusSetEncoderOption(this._encoder, OpusControl.SetInBandFec, 1); Interop.OpusSetEncoderOption(this._encoder, OpusControl.SetBitrate, 131072); this._managedDecoders = new List(); } /// /// Encodes the Opus. /// /// The pcm. /// The target. public void Encode(ReadOnlySpan pcm, ref Span target) { if (pcm.Length != target.Length) throw new ArgumentException("PCM and Opus buffer lengths need to be equal.", nameof(target)); var duration = this.AudioFormat.CalculateSampleDuration(pcm.Length); var frameSize = this.AudioFormat.CalculateFrameSize(duration); var sampleSize = this.AudioFormat.CalculateSampleSize(duration); if (pcm.Length != sampleSize) throw new ArgumentException("Invalid PCM sample size.", nameof(target)); Interop.OpusEncode(this._encoder, pcm, frameSize, ref target); } /// /// Decodes the Opus. /// /// The decoder. /// The opus. /// The target. /// If true, use fec. /// The output format. public void Decode(OpusDecoder decoder, ReadOnlySpan opus, ref Span target, bool useFec, out AudioFormat outputFormat) { //if (target.Length != this.AudioFormat.CalculateMaximumFrameSize()) // throw new ArgumentException("PCM target buffer size needs to be equal to maximum buffer size for specified audio format.", nameof(target)); Interop.OpusGetPacketMetrics(opus, this.AudioFormat.SampleRate, out var channels, out var frames, out var samplesPerFrame, out var frameSize); outputFormat = this.AudioFormat.ChannelCount != channels ? new AudioFormat(this.AudioFormat.SampleRate, channels, this.AudioFormat.VoiceApplication) : this.AudioFormat; if (decoder.AudioFormat.ChannelCount != channels) decoder.Initialize(outputFormat); var sampleCount = Interop.OpusDecode(decoder.Decoder, opus, frameSize, target, useFec); var sampleSize = outputFormat.SampleCountToSampleSize(sampleCount); target = target[..sampleSize]; } /// /// Processes the packet loss. /// /// The decoder. /// The frame size. /// The target. public void ProcessPacketLoss(OpusDecoder decoder, int frameSize, ref Span target) => Interop.OpusDecode(decoder.Decoder, frameSize, target); /// /// Gets the last packet sample count. /// /// The decoder. /// An int. public int GetLastPacketSampleCount(OpusDecoder decoder) { Interop.OpusGetLastPacketDuration(decoder.Decoder, out var sampleCount); return sampleCount; } /// /// Creates the decoder. /// /// An OpusDecoder. public OpusDecoder CreateDecoder() { lock (this._managedDecoders) { var managedDecoder = new OpusDecoder(this); this._managedDecoders.Add(managedDecoder); return managedDecoder; } } /// /// Destroys the decoder. /// /// The decoder. public void DestroyDecoder(OpusDecoder decoder) { lock (this._managedDecoders) { if (!this._managedDecoders.Contains(decoder)) return; this._managedDecoders.Remove(decoder); decoder.Dispose(); } } /// /// Disposes the Opus. /// public void Dispose() { Interop.OpusDestroyEncoder(this._encoder); lock (this._managedDecoders) { foreach (var decoder in this._managedDecoders) decoder.Dispose(); } } } /// /// Represents an Opus decoder. /// public class OpusDecoder : IDisposable { /// /// Gets the audio format produced by this decoder. /// public AudioFormat AudioFormat { get; private set; } /// /// Gets the opus. /// internal Opus Opus { get; } /// /// Gets the decoder. /// internal IntPtr Decoder { get; private set; } - private volatile bool _isDisposed = false; + private volatile bool _isDisposed; /// /// Initializes a new instance of the class. /// /// The managed opus. internal OpusDecoder(Opus managedOpus) { this.Opus = managedOpus; } /// /// Used to lazily initialize the decoder to make sure we're /// using the correct output format, this way we don't end up /// creating more decoders than we need. /// /// internal void Initialize(AudioFormat outputFormat) { if (this.Decoder != IntPtr.Zero) Interop.OpusDestroyDecoder(this.Decoder); this.AudioFormat = outputFormat; this.Decoder = Interop.OpusCreateDecoder(outputFormat); } /// /// Disposes of this Opus decoder. /// public void Dispose() { if (this._isDisposed) return; this._isDisposed = true; if (this.Decoder != IntPtr.Zero) Interop.OpusDestroyDecoder(this.Decoder); } } /// /// The opus error. /// [Flags] internal enum OpusError { Ok = 0, BadArgument = -1, BufferTooSmall = -2, InternalError = -3, InvalidPacket = -4, Unimplemented = -5, InvalidState = -6, AllocationFailure = -7 } /// /// The opus control. /// internal enum OpusControl : int { SetBitrate = 4002, SetBandwidth = 4008, SetInBandFec = 4012, SetPacketLossPercent = 4014, SetSignal = 4024, ResetState = 4028, GetLastPacketDuration = 4039 } /// /// The opus signal. /// internal enum OpusSignal : int { Auto = -1000, Voice = 3001, Music = 3002, } } diff --git a/DisCatSharp.VoiceNext/Codec/Rtp.cs b/DisCatSharp.VoiceNext/Codec/Rtp.cs index 4f2f4c638..bf0b2a3be 100644 --- a/DisCatSharp.VoiceNext/Codec/Rtp.cs +++ b/DisCatSharp.VoiceNext/Codec/Rtp.cs @@ -1,161 +1,159 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Buffers.Binary; namespace DisCatSharp.VoiceNext.Codec { /// /// The rtp. /// internal sealed class Rtp : IDisposable { /// /// The header size. /// public const int HEADER_SIZE = 12; /// /// The rtp no extension. /// private const byte RTP_NO_EXTENSION = 0x80; /// /// The rtp extension. /// private const byte RTP_EXTENSION = 0x90; /// /// The rtp version. /// private const byte RTP_VERSION = 0x78; /// /// Initializes a new instance of the class. /// public Rtp() { } /// /// Encodes the header. /// /// The sequence. /// The timestamp. /// The ssrc. /// The target. public void EncodeHeader(ushort sequence, uint timestamp, uint ssrc, Span target) { if (target.Length < HEADER_SIZE) throw new ArgumentException("Header buffer is too short.", nameof(target)); target[0] = RTP_NO_EXTENSION; target[1] = RTP_VERSION; // Write data big endian BinaryPrimitives.WriteUInt16BigEndian(target[2..], sequence); // header + magic BinaryPrimitives.WriteUInt32BigEndian(target[4..], timestamp); // header + magic + sizeof(sequence) BinaryPrimitives.WriteUInt32BigEndian(target[8..], ssrc); // header + magic + sizeof(sequence) + sizeof(timestamp) } /// /// Are the rtp header. /// /// The source. /// A bool. public bool IsRtpHeader(ReadOnlySpan source) => source.Length >= HEADER_SIZE && (source[0] == RTP_NO_EXTENSION || source[0] == RTP_EXTENSION) && source[1] == RTP_VERSION; /// /// Decodes the header. /// /// The source. /// The sequence. /// The timestamp. /// The ssrc. /// If true, has extension. public void DecodeHeader(ReadOnlySpan source, out ushort sequence, out uint timestamp, out uint ssrc, out bool hasExtension) { if (source.Length < HEADER_SIZE) throw new ArgumentException("Header buffer is too short.", nameof(source)); if ((source[0] != RTP_NO_EXTENSION && source[0] != RTP_EXTENSION) || source[1] != RTP_VERSION) throw new ArgumentException("Invalid RTP header.", nameof(source)); hasExtension = source[0] == RTP_EXTENSION; // Read data big endian sequence = BinaryPrimitives.ReadUInt16BigEndian(source[2..]); timestamp = BinaryPrimitives.ReadUInt32BigEndian(source[4..]); ssrc = BinaryPrimitives.ReadUInt32BigEndian(source[8..]); } /// /// Calculates the packet size. /// /// The encrypted length. /// The encryption mode. /// An int. - public int CalculatePacketSize(int encryptedLength, EncryptionMode encryptionMode) - { - return encryptionMode switch + public int CalculatePacketSize(int encryptedLength, EncryptionMode encryptionMode) => + encryptionMode switch { EncryptionMode.XSalsa20Poly1305 => HEADER_SIZE + encryptedLength, EncryptionMode.XSalsa20Poly1305Suffix => HEADER_SIZE + encryptedLength + Interop.SodiumNonceSize, EncryptionMode.XSalsa20Poly1305Lite => HEADER_SIZE + encryptedLength + 4, _ => throw new ArgumentException("Unsupported encryption mode.", nameof(encryptionMode)), }; - } /// /// Gets the data from packet. /// /// The packet. /// The data. /// The encryption mode. public void GetDataFromPacket(ReadOnlySpan packet, out ReadOnlySpan data, EncryptionMode encryptionMode) { switch (encryptionMode) { case EncryptionMode.XSalsa20Poly1305: data = packet[HEADER_SIZE..]; return; case EncryptionMode.XSalsa20Poly1305Suffix: data = packet.Slice(HEADER_SIZE, packet.Length - HEADER_SIZE - Interop.SodiumNonceSize); return; case EncryptionMode.XSalsa20Poly1305Lite: data = packet.Slice(HEADER_SIZE, packet.Length - HEADER_SIZE - 4); break; default: throw new ArgumentException("Unsupported encryption mode.", nameof(encryptionMode)); } } /// /// Disposes the Rtp. /// public void Dispose() { } } } diff --git a/DisCatSharp.VoiceNext/VoiceNextConfiguration.cs b/DisCatSharp.VoiceNext/VoiceNextConfiguration.cs index 540143420..1324c9354 100644 --- a/DisCatSharp.VoiceNext/VoiceNextConfiguration.cs +++ b/DisCatSharp.VoiceNext/VoiceNextConfiguration.cs @@ -1,66 +1,66 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using Microsoft.Extensions.DependencyInjection; namespace DisCatSharp.VoiceNext { /// /// VoiceNext client configuration. /// public sealed class VoiceNextConfiguration { /// /// Sets the audio format for Opus. This will determine the quality of the audio output. /// Defaults to . /// public AudioFormat AudioFormat { internal get; set; } = AudioFormat.Default; /// /// Sets whether incoming voice receiver should be enabled. /// Defaults to false. /// - public bool EnableIncoming { internal get; set; } = false; + public bool EnableIncoming { internal get; set; } /// /// Sets the size of the packet queue. /// Defaults to 25 or ~500ms. /// public int PacketQueueSize { internal get; set; } = 25; /// /// Creates a new instance of . /// [ActivatorUtilitiesConstructor] public VoiceNextConfiguration() { } /// /// Creates a new instance of , copying the properties of another configuration. /// /// Configuration the properties of which are to be copied. public VoiceNextConfiguration(VoiceNextConfiguration other) { this.AudioFormat = new AudioFormat(other.AudioFormat.SampleRate, other.AudioFormat.ChannelCount, other.AudioFormat.VoiceApplication); this.EnableIncoming = other.EnableIncoming; } } } diff --git a/DisCatSharp.VoiceNext/VoiceNextConnection.cs b/DisCatSharp.VoiceNext/VoiceNextConnection.cs index 74d7dc82f..eb8141c70 100644 --- a/DisCatSharp.VoiceNext/VoiceNextConnection.cs +++ b/DisCatSharp.VoiceNext/VoiceNextConnection.cs @@ -1,1352 +1,1352 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Buffers; using System.Buffers.Binary; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; using DisCatSharp.Common.Utilities; using DisCatSharp.Entities; using DisCatSharp.EventArgs; using DisCatSharp.Net; using DisCatSharp.Net.Udp; using DisCatSharp.Net.WebSocket; using DisCatSharp.VoiceNext.Codec; using DisCatSharp.VoiceNext.Entities; using DisCatSharp.VoiceNext.EventArgs; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace DisCatSharp.VoiceNext { internal delegate Task VoiceDisconnectedEventHandler(DiscordGuild guild); /// /// VoiceNext connection to a voice channel. /// public sealed class VoiceNextConnection : IDisposable { /// /// Triggered whenever a user speaks in the connected voice channel. /// public event AsyncEventHandler UserSpeaking { - add { this._userSpeaking.Register(value); } - remove { this._userSpeaking.Unregister(value); } + add => this._userSpeaking.Register(value); + remove => this._userSpeaking.Unregister(value); } private readonly AsyncEvent _userSpeaking; /// /// Triggered whenever a user joins voice in the connected guild. /// public event AsyncEventHandler UserJoined { - add { this._userJoined.Register(value); } - remove { this._userJoined.Unregister(value); } + add => this._userJoined.Register(value); + remove => this._userJoined.Unregister(value); } private readonly AsyncEvent _userJoined; /// /// Triggered whenever a user leaves voice in the connected guild. /// public event AsyncEventHandler UserLeft { - add { this._userLeft.Register(value); } - remove { this._userLeft.Unregister(value); } + add => this._userLeft.Register(value); + remove => this._userLeft.Unregister(value); } private readonly AsyncEvent _userLeft; /// /// Triggered whenever voice data is received from the connected voice channel. /// public event AsyncEventHandler VoiceReceived { - add { this._voiceReceived.Register(value); } - remove { this._voiceReceived.Unregister(value); } + add => this._voiceReceived.Register(value); + remove => this._voiceReceived.Unregister(value); } private readonly AsyncEvent _voiceReceived; /// /// Triggered whenever voice WebSocket throws an exception. /// public event AsyncEventHandler VoiceSocketErrored { - add { this._voiceSocketError.Register(value); } - remove { this._voiceSocketError.Unregister(value); } + add => this._voiceSocketError.Register(value); + remove => this._voiceSocketError.Unregister(value); } private readonly AsyncEvent _voiceSocketError; internal event VoiceDisconnectedEventHandler VoiceDisconnected; /// /// Gets the unix epoch. /// - private static DateTimeOffset s_unixEpoch { get; } = new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero); + private static DateTimeOffset s_unixEpoch { get; } = new(1970, 1, 1, 0, 0, 0, TimeSpan.Zero); /// /// Gets the discord. /// private readonly DiscordClient _discord; /// /// Gets the guild. /// private readonly DiscordGuild _guild; /// /// Gets the transmitting s s r cs. /// private readonly ConcurrentDictionary _transmittingSsrCs; /// /// Gets the udp client. /// private readonly BaseUdpClient _udpClient; /// /// Gets or sets the voice ws. /// private IWebSocketClient _voiceWs; /// /// Gets or sets the heartbeat task. /// private Task _heartbeatTask; /// /// Gets or sets the heartbeat interval. /// private int _heartbeatInterval; /// /// Gets or sets the last heartbeat. /// private DateTimeOffset _lastHeartbeat; /// /// Gets or sets the token source. /// private CancellationTokenSource _tokenSource; /// /// Gets the token. /// private CancellationToken TOKEN => this._tokenSource.Token; /// /// Gets or sets the server data. /// internal VoiceServerUpdatePayload ServerData { get; set; } /// /// Gets or sets the state data. /// internal VoiceStateUpdatePayload StateData { get; set; } /// /// Gets or sets a value indicating whether resume. /// internal bool Resume { get; set; } /// /// Gets the configuration. /// private readonly VoiceNextConfiguration _configuration; /// /// Gets or sets the opus. /// private Opus _opus; /// /// Gets or sets the sodium. /// private Sodium _sodium; /// /// Gets or sets the rtp. /// private Rtp _rtp; /// /// Gets or sets the selected encryption mode. /// private EncryptionMode _selectedEncryptionMode; /// /// Gets or sets the nonce. /// - private uint _nonce = 0; + private uint _nonce; /// /// Gets or sets the sequence. /// private ushort _sequence; /// /// Gets or sets the timestamp. /// private uint _timestamp; /// /// Gets or sets the s s r c. /// private uint _ssrc; /// /// Gets or sets the key. /// private byte[] _key; /// /// Gets or sets the discovered endpoint. /// private IpEndpoint _discoveredEndpoint; /// /// Gets or sets the web socket endpoint. /// internal ConnectionEndpoint WebSocketEndpoint { get; set; } /// /// Gets or sets the udp endpoint. /// internal ConnectionEndpoint UdpEndpoint { get; set; } /// /// Gets or sets the ready wait. /// private readonly TaskCompletionSource _readyWait; /// /// Gets or sets a value indicating whether is initialized. /// private bool _isInitialized; /// /// Gets or sets a value indicating whether is disposed. /// private bool _isDisposed; /// /// Gets or sets the playing wait. /// private TaskCompletionSource _playingWait; /// /// Gets the pause event. /// private readonly AsyncManualResetEvent _pauseEvent; /// /// Gets or sets the transmit stream. /// private VoiceTransmitSink _transmitStream; /// /// Gets the transmit channel. /// private readonly Channel _transmitChannel; /// /// Gets the keepalive timestamps. /// private readonly ConcurrentDictionary _keepaliveTimestamps; - private ulong _lastKeepalive = 0; + private ulong _lastKeepalive; /// /// Gets or sets the sender task. /// private Task _senderTask; /// /// Gets or sets the sender token source. /// private CancellationTokenSource _senderTokenSource; /// /// Gets the sender token. /// private CancellationToken SENDER_TOKEN => this._senderTokenSource.Token; /// /// Gets or sets the receiver task. /// private Task _receiverTask; /// /// Gets or sets the receiver token source. /// private CancellationTokenSource _receiverTokenSource; /// /// Gets the receiver token. /// private CancellationToken RECEIVER_TOKEN => this._receiverTokenSource.Token; /// /// Gets or sets the keepalive task. /// private Task _keepaliveTask; /// /// Gets or sets the keepalive token source. /// private CancellationTokenSource _keepaliveTokenSource; /// /// Gets the keepalive token. /// private CancellationToken KEEPALIVE_TOKEN => this._keepaliveTokenSource.Token; - private volatile bool _isSpeaking = false; + private volatile bool _isSpeaking; /// /// Gets the audio format used by the Opus encoder. /// public AudioFormat AudioFormat => this._configuration.AudioFormat; /// /// Gets whether this connection is still playing audio. /// public bool IsPlaying => this._playingWait != null && !this._playingWait.Task.IsCompleted; /// /// Gets the websocket round-trip time in ms. /// public int WebSocketPing => Volatile.Read(ref this._wsPing); - private int _wsPing = 0; + private int _wsPing; /// /// Gets the UDP round-trip time in ms. /// public int UdpPing => Volatile.Read(ref this._udpPing); - private int _udpPing = 0; + private int _udpPing; private int _queueCount; /// /// Gets the channel this voice client is connected to. /// public DiscordChannel TargetChannel { get; internal set; } /// /// Initializes a new instance of the class. /// /// The client. /// The guild. /// The channel. /// The config. /// The server. /// The state. internal VoiceNextConnection(DiscordClient client, DiscordGuild guild, DiscordChannel channel, VoiceNextConfiguration config, VoiceServerUpdatePayload server, VoiceStateUpdatePayload state) { this._discord = client; this._guild = guild; this.TargetChannel = channel; this._transmittingSsrCs = new ConcurrentDictionary(); this._userSpeaking = new AsyncEvent("VNEXT_USER_SPEAKING", TimeSpan.Zero, this._discord.EventErrorHandler); this._userJoined = new AsyncEvent("VNEXT_USER_JOINED", TimeSpan.Zero, this._discord.EventErrorHandler); this._userLeft = new AsyncEvent("VNEXT_USER_LEFT", TimeSpan.Zero, this._discord.EventErrorHandler); this._voiceReceived = new AsyncEvent("VNEXT_VOICE_RECEIVED", TimeSpan.Zero, this._discord.EventErrorHandler); this._voiceSocketError = new AsyncEvent("VNEXT_WS_ERROR", TimeSpan.Zero, this._discord.EventErrorHandler); this._tokenSource = new CancellationTokenSource(); this._configuration = config; this._isInitialized = false; this._isDisposed = false; this._opus = new Opus(this.AudioFormat); //this.Sodium = new Sodium(); this._rtp = new Rtp(); this.ServerData = server; this.StateData = state; var eps = this.ServerData.Endpoint; var epi = eps.LastIndexOf(':'); var eph = string.Empty; var epp = 443; if (epi != -1) { eph = eps[..epi]; epp = int.Parse(eps[(epi + 1)..]); } else { eph = eps; } this.WebSocketEndpoint = new ConnectionEndpoint { Hostname = eph, Port = epp }; this._readyWait = new TaskCompletionSource(); this._playingWait = null; this._transmitChannel = Channel.CreateBounded(new BoundedChannelOptions(this._configuration.PacketQueueSize)); this._keepaliveTimestamps = new ConcurrentDictionary(); this._pauseEvent = new AsyncManualResetEvent(true); this._udpClient = this._discord.Configuration.UdpClientFactory(); this._voiceWs = this._discord.Configuration.WebSocketClientFactory(this._discord.Configuration.Proxy, this._discord.ServiceProvider); this._voiceWs.Disconnected += this.VoiceWS_SocketClosed; this._voiceWs.MessageReceived += this.VoiceWS_SocketMessage; this._voiceWs.Connected += this.VoiceWS_SocketOpened; this._voiceWs.ExceptionThrown += this.VoiceWs_SocketException; } ~VoiceNextConnection() { this.Dispose(); } /// /// Connects to the specified voice channel. /// /// A task representing the connection operation. internal Task ConnectAsync() { var gwuri = new UriBuilder { Scheme = "wss", Host = this.WebSocketEndpoint.Hostname, Query = "encoding=json&v=4" }; return this._voiceWs.ConnectAsync(gwuri.Uri); } /// /// Reconnects . /// /// A Task. internal Task ReconnectAsync() => this._voiceWs.DisconnectAsync(); /// /// Starts . /// /// A Task. internal async Task StartAsync() { // Let's announce our intentions to the server var vdp = new VoiceDispatch(); if (!this.Resume) { vdp.OpCode = 0; vdp.Payload = new VoiceIdentifyPayload { ServerId = this.ServerData.GuildId, UserId = this.StateData.UserId.Value, SessionId = this.StateData.SessionId, Token = this.ServerData.Token }; this.Resume = true; } else { vdp.OpCode = 7; vdp.Payload = new VoiceIdentifyPayload { ServerId = this.ServerData.GuildId, SessionId = this.StateData.SessionId, Token = this.ServerData.Token }; } var vdj = JsonConvert.SerializeObject(vdp, Formatting.None); await this.WsSendAsync(vdj).ConfigureAwait(false); } /// /// Waits the for ready async. /// /// A Task. internal Task WaitForReadyAsync() => this._readyWait.Task; /// /// Enqueues the packet async. /// /// The packet. /// The token. /// A Task. internal async Task EnqueuePacketAsync(RawVoicePacket packet, CancellationToken token = default) { await this._transmitChannel.Writer.WriteAsync(packet, token).ConfigureAwait(false); this._queueCount++; } /// /// Prepares the packet. /// /// The pcm. /// The target. /// The length. /// A bool. internal bool PreparePacket(ReadOnlySpan pcm, out byte[] target, out int length) { target = null; length = 0; if (this._isDisposed) return false; var audioFormat = this.AudioFormat; var packetArray = ArrayPool.Shared.Rent(this._rtp.CalculatePacketSize(audioFormat.SampleCountToSampleSize(audioFormat.CalculateMaximumFrameSize()), this._selectedEncryptionMode)); var packet = packetArray.AsSpan(); this._rtp.EncodeHeader(this._sequence, this._timestamp, this._ssrc, packet); var opus = packet.Slice(Rtp.HEADER_SIZE, pcm.Length); this._opus.Encode(pcm, ref opus); this._sequence++; this._timestamp += (uint)audioFormat.CalculateFrameSize(audioFormat.CalculateSampleDuration(pcm.Length)); Span nonce = stackalloc byte[Sodium.NonceSize]; switch (this._selectedEncryptionMode) { case EncryptionMode.XSalsa20Poly1305: this._sodium.GenerateNonce(packet[..Rtp.HEADER_SIZE], nonce); break; case EncryptionMode.XSalsa20Poly1305Suffix: this._sodium.GenerateNonce(nonce); break; case EncryptionMode.XSalsa20Poly1305Lite: this._sodium.GenerateNonce(this._nonce++, nonce); break; default: ArrayPool.Shared.Return(packetArray); throw new Exception("Unsupported encryption mode."); } Span encrypted = stackalloc byte[Sodium.CalculateTargetSize(opus)]; this._sodium.Encrypt(opus, encrypted, nonce); encrypted.CopyTo(packet[Rtp.HEADER_SIZE..]); packet = packet[..this._rtp.CalculatePacketSize(encrypted.Length, this._selectedEncryptionMode)]; this._sodium.AppendNonce(nonce, packet, this._selectedEncryptionMode); target = packetArray; length = packet.Length; return true; } /// /// Voices the sender task. /// /// A Task. private async Task VoiceSenderTask() { var token = this.SENDER_TOKEN; var client = this._udpClient; var reader = this._transmitChannel.Reader; byte[] data = null; var length = 0; var synchronizerTicks = (double)Stopwatch.GetTimestamp(); var synchronizerResolution = Stopwatch.Frequency * 0.005; var tickResolution = 10_000_000.0 / Stopwatch.Frequency; this._discord.Logger.LogDebug(VoiceNextEvents.Misc, "Timer accuracy: {0}/{1} (high resolution? {2})", Stopwatch.Frequency, synchronizerResolution, Stopwatch.IsHighResolution); while (!token.IsCancellationRequested) { await this._pauseEvent.WaitAsync().ConfigureAwait(false); var hasPacket = reader.TryRead(out var rawPacket); if (hasPacket) { this._queueCount--; if (this._playingWait == null || this._playingWait.Task.IsCompleted) this._playingWait = new TaskCompletionSource(); } // Provided by Laura#0090 (214796473689178133); this is Python, but adaptable: // // delay = max(0, self.delay + ((start_time + self.delay * loops) + - time.time())) // // self.delay // sample size // start_time // time since streaming started // loops // number of samples sent // time.time() // DateTime.Now if (hasPacket) { hasPacket = this.PreparePacket(rawPacket.Bytes.Span, out data, out length); if (rawPacket.RentedBuffer != null) ArrayPool.Shared.Return(rawPacket.RentedBuffer); } var durationModifier = hasPacket ? rawPacket.Duration / 5 : 4; var cts = Math.Max(Stopwatch.GetTimestamp() - synchronizerTicks, 0); if (cts < synchronizerResolution * durationModifier) await Task.Delay(TimeSpan.FromTicks((long)(((synchronizerResolution * durationModifier) - cts) * tickResolution))).ConfigureAwait(false); synchronizerTicks += synchronizerResolution * durationModifier; if (!hasPacket) continue; await this.SendSpeakingAsync(true).ConfigureAwait(false); await client.SendAsync(data, length).ConfigureAwait(false); ArrayPool.Shared.Return(data); if (!rawPacket.Silence && this._queueCount == 0) { var nullpcm = new byte[this.AudioFormat.CalculateSampleSize(20)]; for (var i = 0; i < 3; i++) { var nullpacket = new byte[nullpcm.Length]; var nullpacketmem = nullpacket.AsMemory(); await this.EnqueuePacketAsync(new RawVoicePacket(nullpacketmem, 20, true)).ConfigureAwait(false); } } else if (this._queueCount == 0) { await this.SendSpeakingAsync(false).ConfigureAwait(false); this._playingWait?.SetResult(true); } } } /// /// Processes the packet. /// /// The data. /// The opus. /// The pcm. /// The pcm packets. /// The voice sender. /// The output format. /// A bool. private bool ProcessPacket(ReadOnlySpan data, ref Memory opus, ref Memory pcm, IList> pcmPackets, out AudioSender voiceSender, out AudioFormat outputFormat) { voiceSender = null; outputFormat = default; if (!this._rtp.IsRtpHeader(data)) return false; this._rtp.DecodeHeader(data, out var sequence, out var timestamp, out var ssrc, out var hasExtension); if (!this._transmittingSsrCs.TryGetValue(ssrc, out var vtx)) { var decoder = this._opus.CreateDecoder(); vtx = new AudioSender(ssrc, decoder) { // user isn't present as we haven't received a speaking event yet. User = null }; } voiceSender = vtx; if (sequence <= vtx.LastSequence) // out-of-order packet; discard return false; var gap = vtx.LastSequence != 0 ? sequence - 1 - vtx.LastSequence : 0; if (gap >= 5) this._discord.Logger.LogWarning(VoiceNextEvents.VoiceReceiveFailure, "5 or more voice packets were dropped when receiving"); Span nonce = stackalloc byte[Sodium.NonceSize]; this._sodium.GetNonce(data, nonce, this._selectedEncryptionMode); this._rtp.GetDataFromPacket(data, out var encryptedOpus, this._selectedEncryptionMode); var opusSize = Sodium.CalculateSourceSize(encryptedOpus); opus = opus[..opusSize]; var opusSpan = opus.Span; try { this._sodium.Decrypt(encryptedOpus, opusSpan, nonce); // Strip extensions, if any if (hasExtension) { // RFC 5285, 4.2 One-Byte header // http://www.rfcreader.com/#rfc5285_line186 if (opusSpan[0] == 0xBE && opusSpan[1] == 0xDE) { var headerLen = (opusSpan[2] << 8) | opusSpan[3]; var i = 4; for (; i < headerLen + 4; i++) { var @byte = opusSpan[i]; // ID is currently unused since we skip it anyway //var id = (byte)(@byte >> 4); var length = (byte)(@byte & 0x0F) + 1; i += length; } // Strip extension padding too while (opusSpan[i] == 0) i++; opusSpan = opusSpan[i..]; } // TODO: consider implementing RFC 5285, 4.3. Two-Byte Header } if (opusSpan[0] == 0x90) { // I'm not 100% sure what this header is/does, however removing the data causes no // real issues, and has the added benefit of removing a lot of noise. opusSpan = opusSpan[2..]; } if (gap == 1) { var lastSampleCount = this._opus.GetLastPacketSampleCount(vtx.Decoder); var fecpcm = new byte[this.AudioFormat.SampleCountToSampleSize(lastSampleCount)]; var fecpcmMem = fecpcm.AsSpan(); this._opus.Decode(vtx.Decoder, opusSpan, ref fecpcmMem, true, out _); pcmPackets.Add(fecpcm.AsMemory(0, fecpcmMem.Length)); } else if (gap > 1) { var lastSampleCount = this._opus.GetLastPacketSampleCount(vtx.Decoder); for (var i = 0; i < gap; i++) { var fecpcm = new byte[this.AudioFormat.SampleCountToSampleSize(lastSampleCount)]; var fecpcmMem = fecpcm.AsSpan(); this._opus.ProcessPacketLoss(vtx.Decoder, lastSampleCount, ref fecpcmMem); pcmPackets.Add(fecpcm.AsMemory(0, fecpcmMem.Length)); } } var pcmSpan = pcm.Span; this._opus.Decode(vtx.Decoder, opusSpan, ref pcmSpan, false, out outputFormat); pcm = pcm[..pcmSpan.Length]; } finally { vtx.LastSequence = sequence; } return true; } /// /// Processes the voice packet. /// /// The data. /// A Task. private async Task ProcessVoicePacket(byte[] data) { if (data.Length < 13) // minimum packet length return; try { var pcm = new byte[this.AudioFormat.CalculateMaximumFrameSize()]; var pcmMem = pcm.AsMemory(); var opus = new byte[pcm.Length]; var opusMem = opus.AsMemory(); var pcmFillers = new List>(); if (!this.ProcessPacket(data, ref opusMem, ref pcmMem, pcmFillers, out var vtx, out var audioFormat)) return; foreach (var pcmFiller in pcmFillers) await this._voiceReceived.InvokeAsync(this, new VoiceReceiveEventArgs(this._discord.ServiceProvider) { Ssrc = vtx.Ssrc, User = vtx.User, PcmData = pcmFiller, OpusData = new byte[0].AsMemory(), AudioFormat = audioFormat, AudioDuration = audioFormat.CalculateSampleDuration(pcmFiller.Length) }).ConfigureAwait(false); await this._voiceReceived.InvokeAsync(this, new VoiceReceiveEventArgs(this._discord.ServiceProvider) { Ssrc = vtx.Ssrc, User = vtx.User, PcmData = pcmMem, OpusData = opusMem, AudioFormat = audioFormat, AudioDuration = audioFormat.CalculateSampleDuration(pcmMem.Length) }).ConfigureAwait(false); } catch (Exception ex) { this._discord.Logger.LogError(VoiceNextEvents.VoiceReceiveFailure, ex, "Exception occurred when decoding incoming audio data"); } } /// /// Processes the keepalive. /// /// The data. private void ProcessKeepalive(byte[] data) { try { var keepalive = BinaryPrimitives.ReadUInt64LittleEndian(data); if (!this._keepaliveTimestamps.TryRemove(keepalive, out var timestamp)) return; var tdelta = (int)((Stopwatch.GetTimestamp() - timestamp) / (double)Stopwatch.Frequency * 1000); this._discord.Logger.LogDebug(VoiceNextEvents.VoiceKeepalive, "Received UDP keepalive {0} (ping {1}ms)", keepalive, tdelta); Volatile.Write(ref this._udpPing, tdelta); } catch (Exception ex) { this._discord.Logger.LogError(VoiceNextEvents.VoiceKeepalive, ex, "Exception occurred when handling keepalive"); } } /// /// Udps the receiver task. /// /// A Task. private async Task UdpReceiverTask() { var token = this.RECEIVER_TOKEN; var client = this._udpClient; while (!token.IsCancellationRequested) { var data = await client.ReceiveAsync().ConfigureAwait(false); if (data.Length == 8) this.ProcessKeepalive(data); else if (this._configuration.EnableIncoming) await this.ProcessVoicePacket(data).ConfigureAwait(false); } } /// /// Sends a speaking status to the connected voice channel. /// /// Whether the current user is speaking or not. /// A task representing the sending operation. public async Task SendSpeakingAsync(bool speaking = true) { if (!this._isInitialized) throw new InvalidOperationException("The connection is not initialized"); if (this._isSpeaking != speaking) { this._isSpeaking = speaking; var pld = new VoiceDispatch { OpCode = 5, Payload = new VoiceSpeakingPayload { Speaking = speaking, Delay = 0 } }; var plj = JsonConvert.SerializeObject(pld, Formatting.None); await this.WsSendAsync(plj).ConfigureAwait(false); } } /// /// Gets a transmit stream for this connection, optionally specifying a packet size to use with the stream. If a stream is already configured, it will return the existing one. /// /// Duration, in ms, to use for audio packets. /// Transmit stream. public VoiceTransmitSink GetTransmitSink(int sampleDuration = 20) { if (!AudioFormat.AllowedSampleDurations.Contains(sampleDuration)) throw new ArgumentOutOfRangeException(nameof(sampleDuration), "Invalid PCM sample duration specified."); if (this._transmitStream == null) this._transmitStream = new VoiceTransmitSink(this, sampleDuration); return this._transmitStream; } /// /// Asynchronously waits for playback to be finished. Playback is finished when speaking = false is signalled. /// /// A task representing the waiting operation. public async Task WaitForPlaybackFinishAsync() { if (this._playingWait != null) await this._playingWait.Task.ConfigureAwait(false); } /// /// Pauses playback. /// public void Pause() => this._pauseEvent.Reset(); /// /// Asynchronously resumes playback. /// /// public async Task ResumeAsync() => await this._pauseEvent.SetAsync().ConfigureAwait(false); /// /// Disconnects and disposes this voice connection. /// public void Disconnect() => this.Dispose(); /// /// Disconnects and disposes this voice connection. /// public void Dispose() { if (this._isDisposed) return; try { this._isDisposed = true; this._isInitialized = false; this._tokenSource?.Cancel(); this._senderTokenSource?.Cancel(); this._receiverTokenSource?.Cancel(); } catch (Exception ex) { this._discord.Logger.LogError(ex, ex.Message); } try { this._voiceWs.DisconnectAsync().ConfigureAwait(false).GetAwaiter().GetResult(); this._udpClient.Close(); } catch { } try { this._keepaliveTokenSource?.Cancel(); this._tokenSource?.Dispose(); this._senderTokenSource?.Dispose(); this._receiverTokenSource?.Dispose(); this._keepaliveTokenSource?.Dispose(); this._opus?.Dispose(); this._opus = null; this._sodium?.Dispose(); this._sodium = null; this._rtp?.Dispose(); this._rtp = null; } catch (Exception ex) { this._discord.Logger.LogError(ex, ex.Message); } this.VoiceDisconnected?.Invoke(this._guild); } /// /// Heartbeats . /// /// A Task. private async Task HeartbeatAsync() { await Task.Yield(); var token = this.TOKEN; while (true) { try { token.ThrowIfCancellationRequested(); var dt = DateTime.Now; this._discord.Logger.LogTrace(VoiceNextEvents.VoiceHeartbeat, "Sent heartbeat"); var hbd = new VoiceDispatch { OpCode = 3, Payload = UnixTimestamp(dt) }; var hbj = JsonConvert.SerializeObject(hbd); await this.WsSendAsync(hbj).ConfigureAwait(false); this._lastHeartbeat = dt; await Task.Delay(this._heartbeatInterval).ConfigureAwait(false); } catch (OperationCanceledException) { return; } } } /// /// Keepalives . /// /// A Task. private async Task KeepaliveAsync() { await Task.Yield(); var token = this.KEEPALIVE_TOKEN; var client = this._udpClient; while (!token.IsCancellationRequested) { var timestamp = Stopwatch.GetTimestamp(); var keepalive = Volatile.Read(ref this._lastKeepalive); Volatile.Write(ref this._lastKeepalive, keepalive + 1); this._keepaliveTimestamps.TryAdd(keepalive, timestamp); var packet = new byte[8]; BinaryPrimitives.WriteUInt64LittleEndian(packet, keepalive); await client.SendAsync(packet, packet.Length).ConfigureAwait(false); await Task.Delay(5000, token).ConfigureAwait(false); } } /// /// Stage1S . /// /// The voice ready. /// A Task. private async Task Stage1(VoiceReadyPayload voiceReady) { // IP Discovery this._udpClient.Setup(this.UdpEndpoint); var pck = new byte[70]; PreparePacket(pck); await this._udpClient.SendAsync(pck, pck.Length).ConfigureAwait(false); var ipd = await this._udpClient.ReceiveAsync().ConfigureAwait(false); ReadPacket(ipd, out var ip, out var port); this._discoveredEndpoint = new IpEndpoint { Address = ip, Port = port }; this._discord.Logger.LogTrace(VoiceNextEvents.VoiceHandshake, "Endpoint dicovery finished - discovered endpoint is {0}:{1}", ip, port); void PreparePacket(byte[] packet) { var ssrc = this._ssrc; var packetSpan = packet.AsSpan(); MemoryMarshal.Write(packetSpan, ref ssrc); Helpers.ZeroFill(packetSpan); } void ReadPacket(byte[] packet, out System.Net.IPAddress decodedIp, out ushort decodedPort) { var packetSpan = packet.AsSpan(); var ipString = Utilities.UTF8.GetString(packet, 4, 64 /* 70 - 6 */).TrimEnd('\0'); decodedIp = System.Net.IPAddress.Parse(ipString); decodedPort = BinaryPrimitives.ReadUInt16LittleEndian(packetSpan[68 /* 70 - 2 */..]); } // Select voice encryption mode var selectedEncryptionMode = Sodium.SelectMode(voiceReady.Modes); this._selectedEncryptionMode = selectedEncryptionMode.Value; // Ready this._discord.Logger.LogTrace(VoiceNextEvents.VoiceHandshake, "Selected encryption mode is {0}", selectedEncryptionMode.Key); var vsp = new VoiceDispatch { OpCode = 1, Payload = new VoiceSelectProtocolPayload { Protocol = "udp", Data = new VoiceSelectProtocolPayloadData { Address = this._discoveredEndpoint.Address.ToString(), Port = (ushort)this._discoveredEndpoint.Port, Mode = selectedEncryptionMode.Key } } }; var vsj = JsonConvert.SerializeObject(vsp, Formatting.None); await this.WsSendAsync(vsj).ConfigureAwait(false); this._senderTokenSource = new CancellationTokenSource(); this._senderTask = Task.Run(this.VoiceSenderTask, this.SENDER_TOKEN); this._receiverTokenSource = new CancellationTokenSource(); this._receiverTask = Task.Run(this.UdpReceiverTask, this.RECEIVER_TOKEN); } /// /// Stage2S . /// /// The voice session description. /// A Task. private async Task Stage2(VoiceSessionDescriptionPayload voiceSessionDescription) { this._selectedEncryptionMode = Sodium.SupportedModes[voiceSessionDescription.Mode.ToLowerInvariant()]; this._discord.Logger.LogTrace(VoiceNextEvents.VoiceHandshake, "Discord updated encryption mode - new mode is {0}", this._selectedEncryptionMode); // start keepalive this._keepaliveTokenSource = new CancellationTokenSource(); this._keepaliveTask = this.KeepaliveAsync(); // send 3 packets of silence to get things going var nullpcm = new byte[this.AudioFormat.CalculateSampleSize(20)]; for (var i = 0; i < 3; i++) { var nullPcm = new byte[nullpcm.Length]; var nullpacketmem = nullPcm.AsMemory(); await this.EnqueuePacketAsync(new RawVoicePacket(nullpacketmem, 20, true)).ConfigureAwait(false); } this._isInitialized = true; this._readyWait.SetResult(true); } /// /// Handles the dispatch. /// /// The jo. /// A Task. private async Task HandleDispatch(JObject jo) { var opc = (int)jo["op"]; var opp = jo["d"] as JObject; switch (opc) { case 2: // READY this._discord.Logger.LogTrace(VoiceNextEvents.VoiceDispatch, "Received READY (OP2)"); var vrp = opp.ToObject(); this._ssrc = vrp.Ssrc; this.UdpEndpoint = new ConnectionEndpoint(vrp.Address, vrp.Port); // this is not the valid interval // oh, discord //this.HeartbeatInterval = vrp.HeartbeatInterval; this._heartbeatTask = Task.Run(this.HeartbeatAsync); await this.Stage1(vrp).ConfigureAwait(false); break; case 4: // SESSION_DESCRIPTION this._discord.Logger.LogTrace(VoiceNextEvents.VoiceDispatch, "Received SESSION_DESCRIPTION (OP4)"); var vsd = opp.ToObject(); this._key = vsd.SecretKey; this._sodium = new Sodium(this._key.AsMemory()); await this.Stage2(vsd).ConfigureAwait(false); break; case 5: // SPEAKING // Don't spam OP5 // No longer spam, Discord supposedly doesn't send many of these this._discord.Logger.LogTrace(VoiceNextEvents.VoiceDispatch, "Received SPEAKING (OP5)"); var spd = opp.ToObject(); var foundUserInCache = this._discord.TryGetCachedUserInternal(spd.UserId.Value, out var resolvedUser); var spk = new UserSpeakingEventArgs(this._discord.ServiceProvider) { Speaking = spd.Speaking, Ssrc = spd.Ssrc.Value, User = resolvedUser, }; if (foundUserInCache && this._transmittingSsrCs.TryGetValue(spk.Ssrc, out var txssrc5) && txssrc5.Id == 0) { txssrc5.User = spk.User; } else { var opus = this._opus.CreateDecoder(); var vtx = new AudioSender(spk.Ssrc, opus) { User = await this._discord.GetUserAsync(spd.UserId.Value).ConfigureAwait(false) }; if (!this._transmittingSsrCs.TryAdd(spk.Ssrc, vtx)) this._opus.DestroyDecoder(opus); } await this._userSpeaking.InvokeAsync(this, spk).ConfigureAwait(false); break; case 6: // HEARTBEAT ACK var dt = DateTime.Now; var ping = (int)(dt - this._lastHeartbeat).TotalMilliseconds; Volatile.Write(ref this._wsPing, ping); this._discord.Logger.LogTrace(VoiceNextEvents.VoiceDispatch, "Received HEARTBEAT_ACK (OP6, {0}ms)", ping); this._lastHeartbeat = dt; break; case 8: // HELLO // this sends a heartbeat interval that we need to use for heartbeating this._discord.Logger.LogTrace(VoiceNextEvents.VoiceDispatch, "Received HELLO (OP8)"); this._heartbeatInterval = opp["heartbeat_interval"].ToObject(); break; case 9: // RESUMED this._discord.Logger.LogTrace(VoiceNextEvents.VoiceDispatch, "Received RESUMED (OP9)"); this._heartbeatTask = Task.Run(this.HeartbeatAsync); break; case 12: // CLIENT_CONNECTED this._discord.Logger.LogTrace(VoiceNextEvents.VoiceDispatch, "Received CLIENT_CONNECTED (OP12)"); var ujpd = opp.ToObject(); var usrj = await this._discord.GetUserAsync(ujpd.UserId).ConfigureAwait(false); { var opus = this._opus.CreateDecoder(); var vtx = new AudioSender(ujpd.Ssrc, opus) { User = usrj }; if (!this._transmittingSsrCs.TryAdd(vtx.Ssrc, vtx)) this._opus.DestroyDecoder(opus); } await this._userJoined.InvokeAsync(this, new VoiceUserJoinEventArgs(this._discord.ServiceProvider) { User = usrj, Ssrc = ujpd.Ssrc }).ConfigureAwait(false); break; case 13: // CLIENT_DISCONNECTED this._discord.Logger.LogTrace(VoiceNextEvents.VoiceDispatch, "Received CLIENT_DISCONNECTED (OP13)"); var ulpd = opp.ToObject(); var txssrc = this._transmittingSsrCs.FirstOrDefault(x => x.Value.Id == ulpd.UserId); if (this._transmittingSsrCs.ContainsKey(txssrc.Key)) { this._transmittingSsrCs.TryRemove(txssrc.Key, out var txssrc13); this._opus.DestroyDecoder(txssrc13.Decoder); } var usrl = await this._discord.GetUserAsync(ulpd.UserId).ConfigureAwait(false); await this._userLeft.InvokeAsync(this, new VoiceUserLeaveEventArgs(this._discord.ServiceProvider) { User = usrl, Ssrc = txssrc.Key }).ConfigureAwait(false); break; default: this._discord.Logger.LogTrace(VoiceNextEvents.VoiceDispatch, "Received unknown voice opcode (OP{0})", opc); break; } } /// /// Voices the w s_ socket closed. /// /// The client. /// The e. /// A Task. private async Task VoiceWS_SocketClosed(IWebSocketClient client, SocketCloseEventArgs e) { this._discord.Logger.LogDebug(VoiceNextEvents.VoiceConnectionClose, "Voice WebSocket closed ({0}, '{1}')", e.CloseCode, e.CloseMessage); // generally this should not be disposed on all disconnects, only on requested ones // or something // otherwise problems happen //this.Dispose(); if (e.CloseCode == 4006 || e.CloseCode == 4009) this.Resume = false; if (!this._isDisposed) { this._tokenSource.Cancel(); this._tokenSource = new CancellationTokenSource(); this._voiceWs = this._discord.Configuration.WebSocketClientFactory(this._discord.Configuration.Proxy, this._discord.ServiceProvider); this._voiceWs.Disconnected += this.VoiceWS_SocketClosed; this._voiceWs.MessageReceived += this.VoiceWS_SocketMessage; this._voiceWs.Connected += this.VoiceWS_SocketOpened; if (this.Resume) // emzi you dipshit await this.ConnectAsync().ConfigureAwait(false); } } /// /// Voices the w s_ socket message. /// /// The client. /// The e. /// A Task. private Task VoiceWS_SocketMessage(IWebSocketClient client, SocketMessageEventArgs e) { if (e is not SocketTextMessageEventArgs et) { this._discord.Logger.LogCritical(VoiceNextEvents.VoiceGatewayError, "Discord Voice Gateway sent binary data - unable to process"); return Task.CompletedTask; } this._discord.Logger.LogTrace(VoiceNextEvents.VoiceWsRx, et.Message); return this.HandleDispatch(JObject.Parse(et.Message)); } /// /// Voices the w s_ socket opened. /// /// The client. /// The e. /// A Task. private Task VoiceWS_SocketOpened(IWebSocketClient client, SocketEventArgs e) => this.StartAsync(); /// /// Voices the ws_ socket exception. /// /// The client. /// The e. /// A Task. private Task VoiceWs_SocketException(IWebSocketClient client, SocketErrorEventArgs e) => this._voiceSocketError.InvokeAsync(this, new SocketErrorEventArgs(this._discord.ServiceProvider) { Exception = e.Exception }); /// /// Ws the send async. /// /// The payload. /// A Task. private async Task WsSendAsync(string payload) { this._discord.Logger.LogTrace(VoiceNextEvents.VoiceWsTx, payload); await this._voiceWs.SendMessageAsync(payload).ConfigureAwait(false); } /// /// Gets the unix timestamp. /// /// The datetine. private static uint UnixTimestamp(DateTime dt) { var ts = dt - s_unixEpoch; var sd = ts.TotalSeconds; var si = (uint)sd; return si; } } } diff --git a/DisCatSharp.VoiceNext/VoiceNextEvents.cs b/DisCatSharp.VoiceNext/VoiceNextEvents.cs index d62714f25..fd412cf00 100644 --- a/DisCatSharp.VoiceNext/VoiceNextEvents.cs +++ b/DisCatSharp.VoiceNext/VoiceNextEvents.cs @@ -1,82 +1,82 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using Microsoft.Extensions.Logging; namespace DisCatSharp.VoiceNext { /// /// Contains well-defined event IDs used by the VoiceNext extension. /// public static class VoiceNextEvents { /// /// Miscellaneous events, that do not fit in any other category. /// - public static EventId Misc { get; } = new EventId(300, "VoiceNext"); + public static EventId Misc { get; } = new(300, "VoiceNext"); /// /// Events pertaining to Voice Gateway connection lifespan, specifically, heartbeats. /// - public static EventId VoiceHeartbeat { get; } = new EventId(301, nameof(VoiceHeartbeat)); + public static EventId VoiceHeartbeat { get; } = new(301, nameof(VoiceHeartbeat)); /// /// Events pertaining to Voice Gateway connection early lifespan, specifically, the establishing thereof as well as negotiating various modes. /// - public static EventId VoiceHandshake { get; } = new EventId(302, nameof(VoiceHandshake)); + public static EventId VoiceHandshake { get; } = new(302, nameof(VoiceHandshake)); /// /// Events emitted when incoming voice data is corrupted, or packets are being dropped. /// - public static EventId VoiceReceiveFailure { get; } = new EventId(303, nameof(VoiceReceiveFailure)); + public static EventId VoiceReceiveFailure { get; } = new(303, nameof(VoiceReceiveFailure)); /// /// Events pertaining to UDP connection lifespan, specifically the keepalive (or heartbeats). /// - public static EventId VoiceKeepalive { get; } = new EventId(304, nameof(VoiceKeepalive)); + public static EventId VoiceKeepalive { get; } = new(304, nameof(VoiceKeepalive)); /// /// Events emitted for high-level dispatch receive events. /// - public static EventId VoiceDispatch { get; } = new EventId(305, nameof(VoiceDispatch)); + public static EventId VoiceDispatch { get; } = new(305, nameof(VoiceDispatch)); /// /// Events emitted for Voice Gateway connection closes, clean or otherwise. /// - public static EventId VoiceConnectionClose { get; } = new EventId(306, nameof(VoiceConnectionClose)); + public static EventId VoiceConnectionClose { get; } = new(306, nameof(VoiceConnectionClose)); /// /// Events emitted when decoding data received via Voice Gateway fails for any reason. /// - public static EventId VoiceGatewayError { get; } = new EventId(307, nameof(VoiceGatewayError)); + public static EventId VoiceGatewayError { get; } = new(307, nameof(VoiceGatewayError)); /// /// Events containing raw (but decompressed) payloads, received from Discord Voice Gateway. /// - public static EventId VoiceWsRx { get; } = new EventId(308, "Voice ↓"); + public static EventId VoiceWsRx { get; } = new(308, "Voice ↓"); /// /// Events containing raw payloads, as they're being sent to Discord Voice Gateway. /// - public static EventId VoiceWsTx { get; } = new EventId(309, "Voice ↑"); + public static EventId VoiceWsTx { get; } = new(309, "Voice ↑"); } } diff --git a/DisCatSharp/Clients/DiscordClient.Dispatch.cs b/DisCatSharp/Clients/DiscordClient.Dispatch.cs index d7db208d2..cbde903be 100644 --- a/DisCatSharp/Clients/DiscordClient.Dispatch.cs +++ b/DisCatSharp/Clients/DiscordClient.Dispatch.cs @@ -1,3229 +1,3229 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; using DisCatSharp.Common.Utilities; using DisCatSharp.Entities; using DisCatSharp.Enums; using DisCatSharp.EventArgs; using DisCatSharp.Exceptions; using DisCatSharp.Net.Abstractions; using DisCatSharp.Net.Serialization; using Microsoft.Extensions.Logging; using Newtonsoft.Json.Linq; namespace DisCatSharp { /// /// Represents a discord client. /// public sealed partial class DiscordClient { #region Private Fields private string _sessionId; - private bool _guildDownloadCompleted = false; + private bool _guildDownloadCompleted; #endregion #region Dispatch Handler /// /// Handles the dispatch. /// /// The payload. internal async Task HandleDispatchAsync(GatewayPayload payload) { if (payload.Data is not JObject dat) { this.Logger.LogWarning(LoggerEvents.WebSocketReceive, "Invalid payload body (this message is probably safe to ignore); opcode: {0} event: {1}; payload: {2}", payload.OpCode, payload.EventName, payload.Data); return; } - await this._payloadReceived.InvokeAsync(this, new(this.ServiceProvider) + await this._payloadReceived.InvokeAsync(this, new PayloadReceivedEventArgs(this.ServiceProvider) { EventName = payload.EventName, PayloadObject = dat }).ConfigureAwait(false); DiscordChannel chn; ulong gid; ulong cid; ulong uid; DiscordStageInstance stg = default; DiscordIntegration itg = default; DiscordThreadChannel trd = default; DiscordThreadChannelMember trdm = default; DiscordScheduledEvent gse = default; TransportUser usr = default; TransportMember mbr = default; TransportUser refUsr = default; TransportMember refMbr = default; JToken rawMbr = default; var rawRefMsg = dat["referenced_message"]; switch (payload.EventName.ToLowerInvariant()) { #region Gateway Status case "ready": var glds = (JArray)dat["guilds"]; await this.OnReadyEventAsync(dat.ToObject(), glds).ConfigureAwait(false); break; case "resumed": await this.OnResumedAsync().ConfigureAwait(false); break; #endregion #region Channel case "channel_create": chn = dat.ToObject(); await this.OnChannelCreateEventAsync(chn).ConfigureAwait(false); break; case "channel_update": await this.OnChannelUpdateEventAsync(dat.ToObject()).ConfigureAwait(false); break; case "channel_delete": chn = dat.ToObject(); await this.OnChannelDeleteEventAsync(chn.IsPrivate ? dat.ToObject() : chn).ConfigureAwait(false); break; case "channel_pins_update": cid = (ulong)dat["channel_id"]; var ts = (string)dat["last_pin_timestamp"]; await this.OnChannelPinsUpdateAsync((ulong?)dat["guild_id"], cid, ts != null ? DateTimeOffset.Parse(ts, CultureInfo.InvariantCulture) : default(DateTimeOffset?)).ConfigureAwait(false); break; #endregion #region Guild case "guild_create": await this.OnGuildCreateEventAsync(dat.ToDiscordObject(), (JArray)dat["members"], dat["presences"].ToDiscordObject>()).ConfigureAwait(false); break; case "guild_update": await this.OnGuildUpdateEventAsync(dat.ToDiscordObject(), (JArray)dat["members"]).ConfigureAwait(false); break; case "guild_delete": await this.OnGuildDeleteEventAsync(dat.ToDiscordObject()).ConfigureAwait(false); break; case "guild_sync": gid = (ulong)dat["id"]; await this.OnGuildSyncEventAsync(this.GuildsInternal[gid], (bool)dat["large"], (JArray)dat["members"], dat["presences"].ToDiscordObject>()).ConfigureAwait(false); break; case "guild_emojis_update": gid = (ulong)dat["guild_id"]; var ems = dat["emojis"].ToObject>(); await this.OnGuildEmojisUpdateEventAsync(this.GuildsInternal[gid], ems).ConfigureAwait(false); break; case "guild_stickers_update": var strs = dat["stickers"].ToDiscordObject>(); await this.OnStickersUpdatedAsync(strs, dat).ConfigureAwait(false); break; case "guild_integrations_update": gid = (ulong)dat["guild_id"]; // discord fires this event inconsistently if the current user leaves a guild. if (!this.GuildsInternal.ContainsKey(gid)) return; await this.OnGuildIntegrationsUpdateEventAsync(this.GuildsInternal[gid]).ConfigureAwait(false); break; /* Ok soooo.. this isn't documented yet It seems to be part of the next version of membership screening (https://discord.com/channels/641574644578648068/689591708962652289/845836910991507486) advaith said the following (https://discord.com/channels/641574644578648068/689591708962652289/845838160047112202): > iirc it happens when a user leaves a server where they havent completed screening yet We have to wait till it's documented, but the fields are: { "user_id": "snowflake_user", "guild_id": "snowflake_guild" } We could handle it rn, but due to the fact that it isn't documented, it's not an good idea. */ case "guild_join_request_delete": break; #endregion #region Guild Ban case "guild_ban_add": usr = dat["user"].ToObject(); gid = (ulong)dat["guild_id"]; await this.OnGuildBanAddEventAsync(usr, this.GuildsInternal[gid]).ConfigureAwait(false); break; case "guild_ban_remove": usr = dat["user"].ToObject(); gid = (ulong)dat["guild_id"]; await this.OnGuildBanRemoveEventAsync(usr, this.GuildsInternal[gid]).ConfigureAwait(false); break; #endregion #region Guild Event case "guild_scheduled_event_create": gse = dat.ToObject(); gid = (ulong)dat["guild_id"]; await this.OnGuildScheduledEventCreateEventAsync(gse, this.GuildsInternal[gid]).ConfigureAwait(false); break; case "guild_scheduled_event_update": gse = dat.ToObject(); gid = (ulong)dat["guild_id"]; await this.OnGuildScheduledEventUpdateEventAsync(gse, this.GuildsInternal[gid]).ConfigureAwait(false); break; case "guild_scheduled_event_delete": gse = dat.ToObject(); gid = (ulong)dat["guild_id"]; await this.OnGuildScheduledEventDeleteEventAsync(gse, this.GuildsInternal[gid]).ConfigureAwait(false); break; case "guild_scheduled_event_user_add": gid = (ulong)dat["guild_id"]; uid = (ulong)dat["user_id"]; await this.OnGuildScheduledEventUserAddedEventAsync((ulong)dat["guild_scheduled_event_id"], uid, this.GuildsInternal[gid]).ConfigureAwait(false); break; case "guild_scheduled_event_user_remove": gid = (ulong)dat["guild_id"]; uid = (ulong)dat["user_id"]; await this.OnGuildScheduledEventUserRemovedEventAsync((ulong)dat["guild_scheduled_event_id"], uid, this.GuildsInternal[gid]).ConfigureAwait(false); break; #endregion #region Guild Integration case "integration_create": gid = (ulong)dat["guild_id"]; itg = dat.ToObject(); // discord fires this event inconsistently if the current user leaves a guild. if (!this.GuildsInternal.ContainsKey(gid)) return; await this.OnGuildIntegrationCreateEventAsync(this.GuildsInternal[gid], itg).ConfigureAwait(false); break; case "integration_update": gid = (ulong)dat["guild_id"]; itg = dat.ToObject(); // discord fires this event inconsistently if the current user leaves a guild. if (!this.GuildsInternal.ContainsKey(gid)) return; await this.OnGuildIntegrationUpdateEventAsync(this.GuildsInternal[gid], itg).ConfigureAwait(false); break; case "integration_delete": gid = (ulong)dat["guild_id"]; // discord fires this event inconsistently if the current user leaves a guild. if (!this.GuildsInternal.ContainsKey(gid)) return; await this.OnGuildIntegrationDeleteEventAsync(this.GuildsInternal[gid], (ulong)dat["id"], (ulong?)dat["application_id"]).ConfigureAwait(false); break; #endregion #region Guild Member case "guild_member_add": gid = (ulong)dat["guild_id"]; await this.OnGuildMemberAddEventAsync(dat.ToObject(), this.GuildsInternal[gid]).ConfigureAwait(false); break; case "guild_member_remove": gid = (ulong)dat["guild_id"]; usr = dat["user"].ToObject(); if (!this.GuildsInternal.ContainsKey(gid)) { // discord fires this event inconsistently if the current user leaves a guild. if (usr.Id != this.CurrentUser.Id) this.Logger.LogError(LoggerEvents.WebSocketReceive, "Could not find {0} in guild cache", gid); return; } await this.OnGuildMemberRemoveEventAsync(usr, this.GuildsInternal[gid]).ConfigureAwait(false); break; case "guild_member_update": gid = (ulong)dat["guild_id"]; await this.OnGuildMemberUpdateEventAsync(dat.ToDiscordObject(), this.GuildsInternal[gid], dat["roles"].ToObject>(), (string)dat["nick"], (bool?)dat["pending"]).ConfigureAwait(false); break; case "guild_members_chunk": await this.OnGuildMembersChunkEventAsync(dat).ConfigureAwait(false); break; #endregion #region Guild Role case "guild_role_create": gid = (ulong)dat["guild_id"]; await this.OnGuildRoleCreateEventAsync(dat["role"].ToObject(), this.GuildsInternal[gid]).ConfigureAwait(false); break; case "guild_role_update": gid = (ulong)dat["guild_id"]; await this.OnGuildRoleUpdateEventAsync(dat["role"].ToObject(), this.GuildsInternal[gid]).ConfigureAwait(false); break; case "guild_role_delete": gid = (ulong)dat["guild_id"]; await this.OnGuildRoleDeleteEventAsync((ulong)dat["role_id"], this.GuildsInternal[gid]).ConfigureAwait(false); break; #endregion #region Invite case "invite_create": gid = (ulong)dat["guild_id"]; cid = (ulong)dat["channel_id"]; await this.OnInviteCreateEventAsync(cid, gid, dat.ToObject()).ConfigureAwait(false); break; case "invite_delete": gid = (ulong)dat["guild_id"]; cid = (ulong)dat["channel_id"]; await this.OnInviteDeleteEventAsync(cid, gid, dat).ConfigureAwait(false); break; #endregion #region Message case "message_ack": cid = (ulong)dat["channel_id"]; var mid = (ulong)dat["message_id"]; await this.OnMessageAckEventAsync(this.InternalGetCachedChannel(cid), mid).ConfigureAwait(false); break; case "message_create": rawMbr = dat["member"]; if (rawMbr != null) mbr = rawMbr.ToObject(); if (rawRefMsg != null && rawRefMsg.HasValues) { if (rawRefMsg.SelectToken("author") != null) { refUsr = rawRefMsg.SelectToken("author").ToObject(); } if (rawRefMsg.SelectToken("member") != null) { refMbr = rawRefMsg.SelectToken("member").ToObject(); } } await this.OnMessageCreateEventAsync(dat.ToDiscordObject(), dat["author"].ToObject(), mbr, refUsr, refMbr).ConfigureAwait(false); break; case "message_update": rawMbr = dat["member"]; if (rawMbr != null) mbr = rawMbr.ToObject(); if (rawRefMsg != null && rawRefMsg.HasValues) { if (rawRefMsg.SelectToken("author") != null) { refUsr = rawRefMsg.SelectToken("author").ToObject(); } if (rawRefMsg.SelectToken("member") != null) { refMbr = rawRefMsg.SelectToken("member").ToObject(); } } await this.OnMessageUpdateEventAsync(dat.ToDiscordObject(), dat["author"]?.ToObject(), mbr, refUsr, refMbr).ConfigureAwait(false); break; // delete event does *not* include message object case "message_delete": await this.OnMessageDeleteEventAsync((ulong)dat["id"], (ulong)dat["channel_id"], (ulong?)dat["guild_id"]).ConfigureAwait(false); break; case "message_delete_bulk": await this.OnMessageBulkDeleteEventAsync(dat["ids"].ToObject(), (ulong)dat["channel_id"], (ulong?)dat["guild_id"]).ConfigureAwait(false); break; #endregion #region Message Reaction case "message_reaction_add": rawMbr = dat["member"]; if (rawMbr != null) mbr = rawMbr.ToObject(); await this.OnMessageReactionAddAsync((ulong)dat["user_id"], (ulong)dat["message_id"], (ulong)dat["channel_id"], (ulong?)dat["guild_id"], mbr, dat["emoji"].ToObject()).ConfigureAwait(false); break; case "message_reaction_remove": await this.OnMessageReactionRemoveAsync((ulong)dat["user_id"], (ulong)dat["message_id"], (ulong)dat["channel_id"], (ulong?)dat["guild_id"], dat["emoji"].ToObject()).ConfigureAwait(false); break; case "message_reaction_remove_all": await this.OnMessageReactionRemoveAllAsync((ulong)dat["message_id"], (ulong)dat["channel_id"], (ulong?)dat["guild_id"]).ConfigureAwait(false); break; case "message_reaction_remove_emoji": await this.OnMessageReactionRemoveEmojiAsync((ulong)dat["message_id"], (ulong)dat["channel_id"], (ulong)dat["guild_id"], dat["emoji"]).ConfigureAwait(false); break; #endregion #region Stage Instance case "stage_instance_create": stg = dat.ToObject(); await this.OnStageInstanceCreateEventAsync(stg).ConfigureAwait(false); break; case "stage_instance_update": stg = dat.ToObject(); await this.OnStageInstanceUpdateEventAsync(stg).ConfigureAwait(false); break; case "stage_instance_delete": stg = dat.ToObject(); await this.OnStageInstanceDeleteEventAsync(stg).ConfigureAwait(false); break; #endregion #region Thread case "thread_create": trd = dat.ToObject(); await this.OnThreadCreateEventAsync(trd).ConfigureAwait(false); break; case "thread_update": trd = dat.ToObject(); await this.OnThreadUpdateEventAsync(trd).ConfigureAwait(false); break; case "thread_delete": trd = dat.ToObject(); await this.OnThreadDeleteEventAsync(trd).ConfigureAwait(false); break; case "thread_list_sync": gid = (ulong)dat["guild_id"]; //get guild await this.OnThreadListSyncEventAsync(this.GuildsInternal[gid], dat["channel_ids"].ToObject>(), dat["threads"].ToObject>(), dat["members"].ToObject>()).ConfigureAwait(false); break; case "thread_member_update": trdm = dat.ToObject(); await this.OnThreadMemberUpdateEventAsync(trdm).ConfigureAwait(false); break; case "thread_members_update": gid = (ulong)dat["guild_id"]; await this.OnThreadMembersUpdateEventAsync(this.GuildsInternal[gid], (ulong)dat["id"], (JArray)dat["added_members"], (JArray)dat["removed_member_ids"], (int)dat["member_count"]).ConfigureAwait(false); break; #endregion #region Activities case "embedded_activity_update": gid = (ulong)dat["guild_id"]; cid = (ulong)dat["channel_id"]; await this.OnEmbeddedActivityUpdateAsync((JObject)dat["embedded_activity"], this.GuildsInternal[gid], cid, (JArray)dat["users"], (ulong)dat["embedded_activity"]["application_id"]).ConfigureAwait(false); break; #endregion #region User/Presence Update case "presence_update": await this.OnPresenceUpdateEventAsync(dat, (JObject)dat["user"]).ConfigureAwait(false); break; case "user_settings_update": await this.OnUserSettingsUpdateEventAsync(dat.ToObject()).ConfigureAwait(false); break; case "user_update": await this.OnUserUpdateEventAsync(dat.ToObject()).ConfigureAwait(false); break; #endregion #region Voice case "voice_state_update": await this.OnVoiceStateUpdateEventAsync(dat).ConfigureAwait(false); break; case "voice_server_update": gid = (ulong)dat["guild_id"]; await this.OnVoiceServerUpdateEventAsync((string)dat["endpoint"], (string)dat["token"], this.GuildsInternal[gid]).ConfigureAwait(false); break; #endregion #region Interaction/Integration/Application case "interaction_create": rawMbr = dat["member"]; if (rawMbr != null) { mbr = dat["member"].ToObject(); usr = mbr.User; } else { usr = dat["user"].ToObject(); } cid = (ulong)dat["channel_id"]; await this.OnInteractionCreateAsync((ulong?)dat["guild_id"], cid, usr, mbr, dat.ToDiscordObject()).ConfigureAwait(false); break; case "application_command_create": await this.OnApplicationCommandCreateAsync(dat.ToObject(), (ulong?)dat["guild_id"]).ConfigureAwait(false); break; case "application_command_update": await this.OnApplicationCommandUpdateAsync(dat.ToObject(), (ulong?)dat["guild_id"]).ConfigureAwait(false); break; case "application_command_delete": await this.OnApplicationCommandDeleteAsync(dat.ToObject(), (ulong?)dat["guild_id"]).ConfigureAwait(false); break; case "guild_application_command_counts_update": var counts = dat["application_command_counts"]; await this.OnGuildApplicationCommandCountsUpdateAsync((int)counts["1"], (int)counts["2"], (int)counts["3"], (ulong)dat["guild_id"]).ConfigureAwait(false); break; case "application_command_permissions_update": var aid = (ulong)dat["application_id"]; if (aid != this.CurrentApplication.Id) return; var pms = dat["permissions"].ToObject>(); gid = (ulong)dat["guild_id"]; await this.OnApplicationCommandPermissionsUpdateAsync(pms, (ulong)dat["id"], gid, aid).ConfigureAwait(false); break; #endregion #region Misc case "gift_code_update": //Not supposed to be dispatched to bots break; case "typing_start": cid = (ulong)dat["channel_id"]; rawMbr = dat["member"]; if (rawMbr != null) mbr = rawMbr.ToObject(); await this.OnTypingStartEventAsync((ulong)dat["user_id"], cid, this.InternalGetCachedChannel(cid), (ulong?)dat["guild_id"], Utilities.GetDateTimeOffset((long)dat["timestamp"]), mbr).ConfigureAwait(false); break; case "webhooks_update": gid = (ulong)dat["guild_id"]; cid = (ulong)dat["channel_id"]; await this.OnWebhooksUpdateAsync(this.GuildsInternal[gid].GetChannel(cid), this.GuildsInternal[gid]).ConfigureAwait(false); break; default: await this.OnUnknownEventAsync(payload).ConfigureAwait(false); this.Logger.LogWarning(LoggerEvents.WebSocketReceive, "Unknown event: {0}\npayload: {1}", payload.EventName, payload.Data); break; #endregion } } #endregion #region Events #region Gateway /// /// Handles the ready event. /// /// The ready. /// The raw guilds. internal async Task OnReadyEventAsync(ReadyPayload ready, JArray rawGuilds) { //ready.CurrentUser.Discord = this; var rusr = ready.CurrentUser; this.CurrentUser.Username = rusr.Username; this.CurrentUser.Discriminator = rusr.Discriminator; this.CurrentUser.AvatarHash = rusr.AvatarHash; this.CurrentUser.MfaEnabled = rusr.MfaEnabled; this.CurrentUser.Verified = rusr.Verified; this.CurrentUser.IsBot = rusr.IsBot; this.GatewayVersion = ready.GatewayVersion; this._sessionId = ready.SessionId; var rawGuildIndex = rawGuilds.ToDictionary(xt => (ulong)xt["id"], xt => (JObject)xt); this.GuildsInternal.Clear(); foreach (var guild in ready.Guilds) { guild.Discord = this; if (guild.ChannelsInternal == null) guild.ChannelsInternal = new ConcurrentDictionary(); foreach (var xc in guild.Channels.Values) { xc.GuildId = guild.Id; xc.Discord = this; foreach (var xo in xc.PermissionOverwritesInternal) { xo.Discord = this; xo.ChannelId = xc.Id; } } if (guild.RolesInternal == null) guild.RolesInternal = new ConcurrentDictionary(); foreach (var xr in guild.Roles.Values) { xr.Discord = this; xr.GuildId = guild.Id; } var rawGuild = rawGuildIndex[guild.Id]; var rawMembers = (JArray)rawGuild["members"]; if (guild.MembersInternal != null) guild.MembersInternal.Clear(); else guild.MembersInternal = new ConcurrentDictionary(); if (rawMembers != null) { foreach (var xj in rawMembers) { var xtm = xj.ToObject(); var xu = new DiscordUser(xtm.User) { Discord = this }; xu = this.UserCache.AddOrUpdate(xtm.User.Id, xu, (id, old) => { old.Username = xu.Username; old.Discriminator = xu.Discriminator; old.AvatarHash = xu.AvatarHash; return old; }); guild.MembersInternal[xtm.User.Id] = new DiscordMember(xtm) { Discord = this, GuildId = guild.Id }; } } if (guild.EmojisInternal == null) guild.EmojisInternal = new ConcurrentDictionary(); foreach (var xe in guild.Emojis.Values) xe.Discord = this; if (guild.VoiceStatesInternal == null) guild.VoiceStatesInternal = new ConcurrentDictionary(); foreach (var xvs in guild.VoiceStates.Values) xvs.Discord = this; this.GuildsInternal[guild.Id] = guild; } await this._ready.InvokeAsync(this, new ReadyEventArgs(this.ServiceProvider)).ConfigureAwait(false); } /// /// Handles the resumed. /// internal Task OnResumedAsync() { this.Logger.LogInformation(LoggerEvents.SessionUpdate, "Session resumed"); return this._resumed.InvokeAsync(this, new ReadyEventArgs(this.ServiceProvider)); } #endregion #region Channel /// /// Handles the channel create event. /// /// The channel. internal async Task OnChannelCreateEventAsync(DiscordChannel channel) { channel.Discord = this; foreach (var xo in channel.PermissionOverwritesInternal) { xo.Discord = this; xo.ChannelId = channel.Id; } this.GuildsInternal[channel.GuildId.Value].ChannelsInternal[channel.Id] = channel; /*if (this.Configuration.AutoRefreshChannelCache) { await this.RefreshChannelsAsync(channel.Guild.Id); }*/ await this._channelCreated.InvokeAsync(this, new ChannelCreateEventArgs(this.ServiceProvider) { Channel = channel, Guild = channel.Guild }).ConfigureAwait(false); } /// /// Handles the channel update event. /// /// The channel. internal async Task OnChannelUpdateEventAsync(DiscordChannel channel) { if (channel == null) return; channel.Discord = this; var gld = channel.Guild; var channelNew = this.InternalGetCachedChannel(channel.Id); DiscordChannel channelOld = null; if (channelNew != null) { channelOld = new DiscordChannel { Bitrate = channelNew.Bitrate, Discord = this, GuildId = channelNew.GuildId, Id = channelNew.Id, //IsPrivate = channel_new.IsPrivate, LastMessageId = channelNew.LastMessageId, Name = channelNew.Name, PermissionOverwritesInternal = new List(channelNew.PermissionOverwritesInternal), Position = channelNew.Position, Topic = channelNew.Topic, Type = channelNew.Type, UserLimit = channelNew.UserLimit, ParentId = channelNew.ParentId, IsNsfw = channelNew.IsNsfw, PerUserRateLimit = channelNew.PerUserRateLimit, RtcRegionId = channelNew.RtcRegionId, QualityMode = channelNew.QualityMode, DefaultAutoArchiveDuration = channelNew.DefaultAutoArchiveDuration }; channelNew.Bitrate = channel.Bitrate; channelNew.Name = channel.Name; channelNew.Position = channel.Position; channelNew.Topic = channel.Topic; channelNew.UserLimit = channel.UserLimit; channelNew.ParentId = channel.ParentId; channelNew.IsNsfw = channel.IsNsfw; channelNew.PerUserRateLimit = channel.PerUserRateLimit; channelNew.Type = channel.Type; channelNew.RtcRegionId = channel.RtcRegionId; channelNew.QualityMode = channel.QualityMode; channelNew.DefaultAutoArchiveDuration = channel.DefaultAutoArchiveDuration; channelNew.PermissionOverwritesInternal.Clear(); foreach (var po in channel.PermissionOverwritesInternal) { po.Discord = this; po.ChannelId = channel.Id; } channelNew.PermissionOverwritesInternal.AddRange(channel.PermissionOverwritesInternal); if (this.Configuration.AutoRefreshChannelCache && gld != null) { await this.RefreshChannelsAsync(channel.Guild.Id); } } else if (gld != null) { gld.ChannelsInternal[channel.Id] = channel; if (this.Configuration.AutoRefreshChannelCache) { await this.RefreshChannelsAsync(channel.Guild.Id); } } await this._channelUpdated.InvokeAsync(this, new ChannelUpdateEventArgs(this.ServiceProvider) { ChannelAfter = channelNew, Guild = gld, ChannelBefore = channelOld }).ConfigureAwait(false); } /// /// Handles the channel delete event. /// /// The channel. internal async Task OnChannelDeleteEventAsync(DiscordChannel channel) { if (channel == null) return; channel.Discord = this; //if (channel.IsPrivate) if (channel.Type == ChannelType.Group || channel.Type == ChannelType.Private) { var dmChannel = channel as DiscordDmChannel; await this._dmChannelDeleted.InvokeAsync(this, new DmChannelDeleteEventArgs(this.ServiceProvider) { Channel = dmChannel }).ConfigureAwait(false); } else { var gld = channel.Guild; if (gld.ChannelsInternal.TryRemove(channel.Id, out var cachedChannel)) channel = cachedChannel; if (this.Configuration.AutoRefreshChannelCache) { await this.RefreshChannelsAsync(channel.Guild.Id); } await this._channelDeleted.InvokeAsync(this, new ChannelDeleteEventArgs(this.ServiceProvider) { Channel = channel, Guild = gld }).ConfigureAwait(false); } } /// /// Refreshes the channels. /// /// The guild id. internal async Task RefreshChannelsAsync(ulong guildId) { var guild = this.InternalGetCachedGuild(guildId); var channels = await this.ApiClient.GetGuildChannelsAsync(guildId); guild.ChannelsInternal.Clear(); foreach (var channel in channels.ToList()) { channel.Discord = this; foreach (var xo in channel.PermissionOverwritesInternal) { xo.Discord = this; xo.ChannelId = channel.Id; } guild.ChannelsInternal[channel.Id] = channel; } } /// /// Handles the channel pins update. /// /// The guild id. /// The channel id. /// The last pin timestamp. internal async Task OnChannelPinsUpdateAsync(ulong? guildId, ulong channelId, DateTimeOffset? lastPinTimestamp) { var guild = this.InternalGetCachedGuild(guildId); var channel = this.InternalGetCachedChannel(channelId) ?? this.InternalGetCachedThread(channelId); var ea = new ChannelPinsUpdateEventArgs(this.ServiceProvider) { Guild = guild, Channel = channel, LastPinTimestamp = lastPinTimestamp }; await this._channelPinsUpdated.InvokeAsync(this, ea).ConfigureAwait(false); } #endregion #region Guild /// /// Handles the guild create event. /// /// The guild. /// The raw members. /// The presences. internal async Task OnGuildCreateEventAsync(DiscordGuild guild, JArray rawMembers, IEnumerable presences) { if (presences != null) { foreach (var xp in presences) { xp.Discord = this; xp.GuildId = guild.Id; xp.Activity = new DiscordActivity(xp.RawActivity); if (xp.RawActivities != null) { xp.InternalActivities = new DiscordActivity[xp.RawActivities.Length]; for (var i = 0; i < xp.RawActivities.Length; i++) xp.InternalActivities[i] = new DiscordActivity(xp.RawActivities[i]); } this.PresencesInternal[xp.InternalUser.Id] = xp; } } var exists = this.GuildsInternal.TryGetValue(guild.Id, out var foundGuild); guild.Discord = this; guild.IsUnavailable = false; var eventGuild = guild; if (exists) guild = foundGuild; if (guild.ChannelsInternal == null) guild.ChannelsInternal = new ConcurrentDictionary(); if (guild.ThreadsInternal == null) guild.ThreadsInternal = new ConcurrentDictionary(); if (guild.RolesInternal == null) guild.RolesInternal = new ConcurrentDictionary(); if (guild.ThreadsInternal == null) guild.ThreadsInternal = new ConcurrentDictionary(); if (guild.StickersInternal == null) guild.StickersInternal = new ConcurrentDictionary(); if (guild.EmojisInternal == null) guild.EmojisInternal = new ConcurrentDictionary(); if (guild.VoiceStatesInternal == null) guild.VoiceStatesInternal = new ConcurrentDictionary(); if (guild.MembersInternal == null) guild.MembersInternal = new ConcurrentDictionary(); if (guild.ScheduledEventsInternal == null) guild.ScheduledEventsInternal = new ConcurrentDictionary(); this.UpdateCachedGuild(eventGuild, rawMembers); guild.JoinedAt = eventGuild.JoinedAt; guild.IsLarge = eventGuild.IsLarge; guild.MemberCount = Math.Max(eventGuild.MemberCount, guild.MembersInternal.Count); guild.IsUnavailable = eventGuild.IsUnavailable; guild.PremiumSubscriptionCount = eventGuild.PremiumSubscriptionCount; guild.PremiumTier = eventGuild.PremiumTier; guild.BannerHash = eventGuild.BannerHash; guild.VanityUrlCode = eventGuild.VanityUrlCode; guild.Description = eventGuild.Description; guild.IsNsfw = eventGuild.IsNsfw; foreach (var kvp in eventGuild.VoiceStatesInternal) guild.VoiceStatesInternal[kvp.Key] = kvp.Value; foreach (var kvp in eventGuild.ChannelsInternal) guild.ChannelsInternal[kvp.Key] = kvp.Value; foreach (var kvp in eventGuild.RolesInternal) guild.RolesInternal[kvp.Key] = kvp.Value; foreach (var kvp in eventGuild.EmojisInternal) guild.EmojisInternal[kvp.Key] = kvp.Value; foreach (var kvp in eventGuild.ThreadsInternal) guild.ThreadsInternal[kvp.Key] = kvp.Value; foreach (var kvp in eventGuild.StickersInternal) guild.StickersInternal[kvp.Key] = kvp.Value; foreach (var kvp in eventGuild.StageInstancesInternal) guild.StageInstancesInternal[kvp.Key] = kvp.Value; foreach (var kvp in eventGuild.ScheduledEventsInternal) guild.ScheduledEventsInternal[kvp.Key] = kvp.Value; foreach (var xc in guild.ChannelsInternal.Values) { xc.GuildId = guild.Id; xc.Discord = this; foreach (var xo in xc.PermissionOverwritesInternal) { xo.Discord = this; xo.ChannelId = xc.Id; } } foreach (var xt in guild.ThreadsInternal.Values) { xt.GuildId = guild.Id; xt.Discord = this; } foreach (var xe in guild.EmojisInternal.Values) xe.Discord = this; foreach (var xs in guild.StickersInternal.Values) xs.Discord = this; foreach (var xvs in guild.VoiceStatesInternal.Values) xvs.Discord = this; foreach (var xsi in guild.StageInstancesInternal.Values) { xsi.Discord = this; xsi.GuildId = guild.Id; } foreach (var xr in guild.RolesInternal.Values) { xr.Discord = this; xr.GuildId = guild.Id; } foreach (var xse in guild.ScheduledEventsInternal.Values) { xse.Discord = this; xse.GuildId = guild.Id; if (xse.Creator != null) xse.Creator.Discord = this; } var old = Volatile.Read(ref this._guildDownloadCompleted); var dcompl = this.GuildsInternal.Values.All(xg => !xg.IsUnavailable); Volatile.Write(ref this._guildDownloadCompleted, dcompl); if (exists) await this._guildAvailable.InvokeAsync(this, new GuildCreateEventArgs(this.ServiceProvider) { Guild = guild }).ConfigureAwait(false); else await this._guildCreated.InvokeAsync(this, new GuildCreateEventArgs(this.ServiceProvider) { Guild = guild }).ConfigureAwait(false); if (dcompl && !old) await this._guildDownloadCompletedEv.InvokeAsync(this, new GuildDownloadCompletedEventArgs(this.Guilds, this.ServiceProvider)).ConfigureAwait(false); } /// /// Handles the guild update event. /// /// The guild. /// The raw members. internal async Task OnGuildUpdateEventAsync(DiscordGuild guild, JArray rawMembers) { DiscordGuild oldGuild; if (!this.GuildsInternal.ContainsKey(guild.Id)) { this.GuildsInternal[guild.Id] = guild; oldGuild = null; } else { var gld = this.GuildsInternal[guild.Id]; oldGuild = new DiscordGuild { Discord = gld.Discord, Name = gld.Name, AfkChannelId = gld.AfkChannelId, AfkTimeout = gld.AfkTimeout, ApplicationId = gld.ApplicationId, DefaultMessageNotifications = gld.DefaultMessageNotifications, ExplicitContentFilter = gld.ExplicitContentFilter, RawFeatures = gld.RawFeatures, IconHash = gld.IconHash, Id = gld.Id, IsLarge = gld.IsLarge, IsSynced = gld.IsSynced, IsUnavailable = gld.IsUnavailable, JoinedAt = gld.JoinedAt, MemberCount = gld.MemberCount, MaxMembers = gld.MaxMembers, MaxPresences = gld.MaxPresences, ApproximateMemberCount = gld.ApproximateMemberCount, ApproximatePresenceCount = gld.ApproximatePresenceCount, MaxVideoChannelUsers = gld.MaxVideoChannelUsers, DiscoverySplashHash = gld.DiscoverySplashHash, PreferredLocale = gld.PreferredLocale, MfaLevel = gld.MfaLevel, OwnerId = gld.OwnerId, SplashHash = gld.SplashHash, SystemChannelId = gld.SystemChannelId, SystemChannelFlags = gld.SystemChannelFlags, Description = gld.Description, WidgetEnabled = gld.WidgetEnabled, WidgetChannelId = gld.WidgetChannelId, VerificationLevel = gld.VerificationLevel, RulesChannelId = gld.RulesChannelId, PublicUpdatesChannelId = gld.PublicUpdatesChannelId, VoiceRegionId = gld.VoiceRegionId, IsNsfw = gld.IsNsfw, PremiumProgressBarEnabled = gld.PremiumProgressBarEnabled, PremiumSubscriptionCount = gld.PremiumSubscriptionCount, PremiumTier = gld.PremiumTier, ChannelsInternal = new ConcurrentDictionary(), ThreadsInternal = new ConcurrentDictionary(), EmojisInternal = new ConcurrentDictionary(), StickersInternal = new ConcurrentDictionary(), MembersInternal = new ConcurrentDictionary(), RolesInternal = new ConcurrentDictionary(), StageInstancesInternal = new ConcurrentDictionary(), VoiceStatesInternal = new ConcurrentDictionary(), ScheduledEventsInternal = new ConcurrentDictionary() }; foreach (var kvp in gld.ChannelsInternal) oldGuild.ChannelsInternal[kvp.Key] = kvp.Value; foreach (var kvp in gld.ThreadsInternal) oldGuild.ThreadsInternal[kvp.Key] = kvp.Value; foreach (var kvp in gld.EmojisInternal) oldGuild.EmojisInternal[kvp.Key] = kvp.Value; foreach (var kvp in gld.StickersInternal) oldGuild.StickersInternal[kvp.Key] = kvp.Value; foreach (var kvp in gld.RolesInternal) oldGuild.RolesInternal[kvp.Key] = kvp.Value; foreach (var kvp in gld.VoiceStatesInternal) oldGuild.VoiceStatesInternal[kvp.Key] = kvp.Value; foreach (var kvp in gld.MembersInternal) oldGuild.MembersInternal[kvp.Key] = kvp.Value; foreach (var kvp in gld.StageInstancesInternal) oldGuild.StageInstancesInternal[kvp.Key] = kvp.Value; foreach (var kvp in gld.ScheduledEventsInternal) oldGuild.ScheduledEventsInternal[kvp.Key] = kvp.Value; } guild.Discord = this; guild.IsUnavailable = false; var eventGuild = guild; guild = this.GuildsInternal[eventGuild.Id]; if (guild.ChannelsInternal == null) guild.ChannelsInternal = new ConcurrentDictionary(); if (guild.ThreadsInternal == null) guild.ThreadsInternal = new ConcurrentDictionary(); if (guild.RolesInternal == null) guild.RolesInternal = new ConcurrentDictionary(); if (guild.EmojisInternal == null) guild.EmojisInternal = new ConcurrentDictionary(); if (guild.StickersInternal == null) guild.StickersInternal = new ConcurrentDictionary(); if (guild.VoiceStatesInternal == null) guild.VoiceStatesInternal = new ConcurrentDictionary(); if (guild.StageInstancesInternal == null) guild.StageInstancesInternal = new ConcurrentDictionary(); if (guild.MembersInternal == null) guild.MembersInternal = new ConcurrentDictionary(); if (guild.ScheduledEventsInternal == null) guild.ScheduledEventsInternal = new ConcurrentDictionary(); this.UpdateCachedGuild(eventGuild, rawMembers); foreach (var xc in guild.ChannelsInternal.Values) { xc.GuildId = guild.Id; xc.Discord = this; foreach (var xo in xc.PermissionOverwritesInternal) { xo.Discord = this; xo.ChannelId = xc.Id; } } foreach (var xc in guild.ThreadsInternal.Values) { xc.GuildId = guild.Id; xc.Discord = this; } foreach (var xe in guild.EmojisInternal.Values) xe.Discord = this; foreach (var xs in guild.StickersInternal.Values) xs.Discord = this; foreach (var xvs in guild.VoiceStatesInternal.Values) xvs.Discord = this; foreach (var xr in guild.RolesInternal.Values) { xr.Discord = this; xr.GuildId = guild.Id; } foreach (var xsi in guild.StageInstancesInternal.Values) { xsi.Discord = this; xsi.GuildId = guild.Id; } foreach (var xse in guild.ScheduledEventsInternal.Values) { xse.Discord = this; xse.GuildId = guild.Id; if (xse.Creator != null) xse.Creator.Discord = this; } await this._guildUpdated.InvokeAsync(this, new GuildUpdateEventArgs(this.ServiceProvider) { GuildBefore = oldGuild, GuildAfter = guild }).ConfigureAwait(false); } /// /// Handles the guild delete event. /// /// The guild. internal async Task OnGuildDeleteEventAsync(DiscordGuild guild) { if (guild.IsUnavailable) { if (!this.GuildsInternal.TryGetValue(guild.Id, out var gld)) return; gld.IsUnavailable = true; await this._guildUnavailable.InvokeAsync(this, new GuildDeleteEventArgs(this.ServiceProvider) { Guild = guild, Unavailable = true }).ConfigureAwait(false); } else { if (!this.GuildsInternal.TryRemove(guild.Id, out var gld)) return; await this._guildDeleted.InvokeAsync(this, new GuildDeleteEventArgs(this.ServiceProvider) { Guild = gld }).ConfigureAwait(false); } } /// /// Handles the guild sync event. /// /// The guild. /// If true, is large. /// The raw members. /// The presences. internal async Task OnGuildSyncEventAsync(DiscordGuild guild, bool isLarge, JArray rawMembers, IEnumerable presences) { presences = presences.Select(xp => { xp.Discord = this; xp.Activity = new DiscordActivity(xp.RawActivity); return xp; }); foreach (var xp in presences) this.PresencesInternal[xp.InternalUser.Id] = xp; guild.IsSynced = true; guild.IsLarge = isLarge; this.UpdateCachedGuild(guild, rawMembers); await this._guildAvailable.InvokeAsync(this, new GuildCreateEventArgs(this.ServiceProvider) { Guild = guild }).ConfigureAwait(false); } /// /// Handles the guild emojis update event. /// /// The guild. /// The new emojis. internal async Task OnGuildEmojisUpdateEventAsync(DiscordGuild guild, IEnumerable newEmojis) { var oldEmojis = new ConcurrentDictionary(guild.EmojisInternal); guild.EmojisInternal.Clear(); foreach (var emoji in newEmojis) { emoji.Discord = this; guild.EmojisInternal[emoji.Id] = emoji; } var ea = new GuildEmojisUpdateEventArgs(this.ServiceProvider) { Guild = guild, EmojisAfter = guild.Emojis, EmojisBefore = new ReadOnlyConcurrentDictionary(oldEmojis) }; await this._guildEmojisUpdated.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the stickers updated. /// /// The new stickers. /// The raw. internal async Task OnStickersUpdatedAsync(IEnumerable newStickers, JObject raw) { var guild = this.InternalGetCachedGuild((ulong)raw["guild_id"]); var oldStickers = new ConcurrentDictionary(guild.StickersInternal); guild.StickersInternal.Clear(); foreach (var nst in newStickers) { if (nst.User is not null) { nst.User.Discord = this; this.UserCache.AddOrUpdate(nst.User.Id, nst.User, (old, @new) => @new); } nst.Discord = this; guild.StickersInternal[nst.Id] = nst; } var sea = new GuildStickersUpdateEventArgs(this.ServiceProvider) { Guild = guild, StickersBefore = oldStickers, StickersAfter = guild.Stickers }; await this._guildStickersUpdated.InvokeAsync(this, sea).ConfigureAwait(false); } /// /// Handles the guild integrations update event. /// /// The guild. internal async Task OnGuildIntegrationsUpdateEventAsync(DiscordGuild guild) { var ea = new GuildIntegrationsUpdateEventArgs(this.ServiceProvider) { Guild = guild }; await this._guildIntegrationsUpdated.InvokeAsync(this, ea).ConfigureAwait(false); } #endregion #region Guild Ban /// /// Handles the guild ban add event. /// /// The user. /// The guild. internal async Task OnGuildBanAddEventAsync(TransportUser user, DiscordGuild guild) { var usr = new DiscordUser(user) { Discord = this }; usr = this.UserCache.AddOrUpdate(user.Id, usr, (id, old) => { old.Username = usr.Username; old.Discriminator = usr.Discriminator; old.AvatarHash = usr.AvatarHash; return old; }); if (!guild.Members.TryGetValue(user.Id, out var mbr)) mbr = new DiscordMember(usr) { Discord = this, GuildId = guild.Id }; var ea = new GuildBanAddEventArgs(this.ServiceProvider) { Guild = guild, Member = mbr }; await this._guildBanAdded.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the guild ban remove event. /// /// The user. /// The guild. internal async Task OnGuildBanRemoveEventAsync(TransportUser user, DiscordGuild guild) { var usr = new DiscordUser(user) { Discord = this }; usr = this.UserCache.AddOrUpdate(user.Id, usr, (id, old) => { old.Username = usr.Username; old.Discriminator = usr.Discriminator; old.AvatarHash = usr.AvatarHash; return old; }); if (!guild.Members.TryGetValue(user.Id, out var mbr)) mbr = new DiscordMember(usr) { Discord = this, GuildId = guild.Id }; var ea = new GuildBanRemoveEventArgs(this.ServiceProvider) { Guild = guild, Member = mbr }; await this._guildBanRemoved.InvokeAsync(this, ea).ConfigureAwait(false); } #endregion #region Guild Scheduled Event /// /// Dispatches the event. /// /// The created event. /// The target guild. internal async Task OnGuildScheduledEventCreateEventAsync(DiscordScheduledEvent scheduledEvent, DiscordGuild guild) { scheduledEvent.Discord = this; guild.ScheduledEventsInternal.AddOrUpdate(scheduledEvent.Id, scheduledEvent, (old, newScheduledEvent) => newScheduledEvent); if (scheduledEvent.Creator != null) { scheduledEvent.Creator.Discord = this; this.UserCache.AddOrUpdate(scheduledEvent.Creator.Id, scheduledEvent.Creator, (id, old) => { old.Username = scheduledEvent.Creator.Username; old.Discriminator = scheduledEvent.Creator.Discriminator; old.AvatarHash = scheduledEvent.Creator.AvatarHash; old.Flags = scheduledEvent.Creator.Flags; return old; }); } await this._guildScheduledEventCreated.InvokeAsync(this, new GuildScheduledEventCreateEventArgs(this.ServiceProvider) { ScheduledEvent = scheduledEvent, Guild = scheduledEvent.Guild }).ConfigureAwait(false); } /// /// Dispatches the event. /// /// The updated event. /// The target guild. internal async Task OnGuildScheduledEventUpdateEventAsync(DiscordScheduledEvent scheduledEvent, DiscordGuild guild) { if (guild == null) return; DiscordScheduledEvent oldEvent; if (!guild.ScheduledEventsInternal.ContainsKey(scheduledEvent.Id)) { oldEvent = null; } else { var ev = guild.ScheduledEventsInternal[scheduledEvent.Id]; oldEvent = new DiscordScheduledEvent { Id = ev.Id, ChannelId = ev.ChannelId, EntityId = ev.EntityId, EntityMetadata = ev.EntityMetadata, CreatorId = ev.CreatorId, Creator = ev.Creator, Discord = this, Description = ev.Description, EntityType = ev.EntityType, ScheduledStartTimeRaw = ev.ScheduledStartTimeRaw, ScheduledEndTimeRaw = ev.ScheduledEndTimeRaw, GuildId = ev.GuildId, Status = ev.Status, Name = ev.Name, UserCount = ev.UserCount }; } if (scheduledEvent.Creator != null) { scheduledEvent.Creator.Discord = this; this.UserCache.AddOrUpdate(scheduledEvent.Creator.Id, scheduledEvent.Creator, (id, old) => { old.Username = scheduledEvent.Creator.Username; old.Discriminator = scheduledEvent.Creator.Discriminator; old.AvatarHash = scheduledEvent.Creator.AvatarHash; old.Flags = scheduledEvent.Creator.Flags; return old; }); } if (scheduledEvent.Status == ScheduledEventStatus.Completed) { guild.ScheduledEventsInternal.TryRemove(scheduledEvent.Id, out var deletedEvent); await this._guildScheduledEventDeleted.InvokeAsync(this, new GuildScheduledEventDeleteEventArgs(this.ServiceProvider) { ScheduledEvent = scheduledEvent, Guild = guild, Reason = ScheduledEventStatus.Completed }).ConfigureAwait(false); } else if (scheduledEvent.Status == ScheduledEventStatus.Canceled) { guild.ScheduledEventsInternal.TryRemove(scheduledEvent.Id, out var deletedEvent); scheduledEvent.Status = ScheduledEventStatus.Canceled; await this._guildScheduledEventDeleted.InvokeAsync(this, new GuildScheduledEventDeleteEventArgs(this.ServiceProvider) { ScheduledEvent = scheduledEvent, Guild = guild, Reason = ScheduledEventStatus.Canceled }).ConfigureAwait(false); } else { this.UpdateScheduledEvent(scheduledEvent, guild); await this._guildScheduledEventUpdated.InvokeAsync(this, new GuildScheduledEventUpdateEventArgs(this.ServiceProvider) { ScheduledEventBefore = oldEvent, ScheduledEventAfter = scheduledEvent, Guild = guild }).ConfigureAwait(false); } } /// /// Dispatches the event. /// /// The deleted event. /// The target guild. internal async Task OnGuildScheduledEventDeleteEventAsync(DiscordScheduledEvent scheduledEvent, DiscordGuild guild) { scheduledEvent.Discord = this; if (scheduledEvent.Status == ScheduledEventStatus.Scheduled) scheduledEvent.Status = ScheduledEventStatus.Canceled; if (scheduledEvent.Creator != null) { scheduledEvent.Creator.Discord = this; this.UserCache.AddOrUpdate(scheduledEvent.Creator.Id, scheduledEvent.Creator, (id, old) => { old.Username = scheduledEvent.Creator.Username; old.Discriminator = scheduledEvent.Creator.Discriminator; old.AvatarHash = scheduledEvent.Creator.AvatarHash; old.Flags = scheduledEvent.Creator.Flags; return old; }); } await this._guildScheduledEventDeleted.InvokeAsync(this, new GuildScheduledEventDeleteEventArgs(this.ServiceProvider) { ScheduledEvent = scheduledEvent, Guild = scheduledEvent.Guild, Reason = scheduledEvent.Status }).ConfigureAwait(false); guild.ScheduledEventsInternal.TryRemove(scheduledEvent.Id, out var deletedEvent); } /// /// Dispatches the event. /// The target event. /// The added user id. /// The target guild. /// internal async Task OnGuildScheduledEventUserAddedEventAsync(ulong guildScheduledEventId, ulong userId, DiscordGuild guild) { var scheduledEvent = this.InternalGetCachedScheduledEvent(guildScheduledEventId) ?? this.UpdateScheduledEvent(new DiscordScheduledEvent { Id = guildScheduledEventId, GuildId = guild.Id, Discord = this, UserCount = 0 }, guild); scheduledEvent.UserCount++; scheduledEvent.Discord = this; guild.Discord = this; var user = this.GetUserAsync(userId, true).Result; user.Discord = this; var member = guild.Members.TryGetValue(userId, out var mem) ? mem : guild.GetMemberAsync(userId).Result; member.Discord = this; await this._guildScheduledEventUserAdded.InvokeAsync(this, new GuildScheduledEventUserAddEventArgs(this.ServiceProvider) { ScheduledEvent = scheduledEvent, Guild = guild, User = user, Member = member }).ConfigureAwait(false); } /// /// Dispatches the event. /// The target event. /// The removed user id. /// The target guild. /// internal async Task OnGuildScheduledEventUserRemovedEventAsync(ulong guildScheduledEventId, ulong userId, DiscordGuild guild) { var scheduledEvent = this.InternalGetCachedScheduledEvent(guildScheduledEventId) ?? this.UpdateScheduledEvent(new DiscordScheduledEvent { Id = guildScheduledEventId, GuildId = guild.Id, Discord = this, UserCount = 0 }, guild); scheduledEvent.UserCount = scheduledEvent.UserCount == 0 ? 0 : scheduledEvent.UserCount - 1; scheduledEvent.Discord = this; guild.Discord = this; var user = this.GetUserAsync(userId, true).Result; user.Discord = this; var member = guild.Members.TryGetValue(userId, out var mem) ? mem : guild.GetMemberAsync(userId).Result; member.Discord = this; await this._guildScheduledEventUserRemoved.InvokeAsync(this, new GuildScheduledEventUserRemoveEventArgs(this.ServiceProvider) { ScheduledEvent = scheduledEvent, Guild = guild, User = user, Member = member }).ConfigureAwait(false); } #endregion #region Guild Integration /// /// Handles the guild integration create event. /// /// The guild. /// The integration. internal async Task OnGuildIntegrationCreateEventAsync(DiscordGuild guild, DiscordIntegration integration) { integration.Discord = this; await this._guildIntegrationCreated.InvokeAsync(this, new GuildIntegrationCreateEventArgs(this.ServiceProvider) { Integration = integration, Guild = guild }).ConfigureAwait(false); } /// /// Handles the guild integration update event. /// /// The guild. /// The integration. internal async Task OnGuildIntegrationUpdateEventAsync(DiscordGuild guild, DiscordIntegration integration) { integration.Discord = this; await this._guildIntegrationUpdated.InvokeAsync(this, new GuildIntegrationUpdateEventArgs(this.ServiceProvider) { Integration = integration, Guild = guild }).ConfigureAwait(false); } /// /// Handles the guild integration delete event. /// /// The guild. /// The integration_id. /// The application_id. internal async Task OnGuildIntegrationDeleteEventAsync(DiscordGuild guild, ulong integrationId, ulong? applicationId) => await this._guildIntegrationDeleted.InvokeAsync(this, new GuildIntegrationDeleteEventArgs(this.ServiceProvider) { Guild = guild, IntegrationId = integrationId, ApplicationId = applicationId }).ConfigureAwait(false); #endregion #region Guild Member /// /// Handles the guild member add event. /// /// The member. /// The guild. internal async Task OnGuildMemberAddEventAsync(TransportMember member, DiscordGuild guild) { var usr = new DiscordUser(member.User) { Discord = this }; usr = this.UserCache.AddOrUpdate(member.User.Id, usr, (id, old) => { old.Username = usr.Username; old.Discriminator = usr.Discriminator; old.AvatarHash = usr.AvatarHash; return old; }); var mbr = new DiscordMember(member) { Discord = this, GuildId = guild.Id }; guild.MembersInternal[mbr.Id] = mbr; guild.MemberCount++; var ea = new GuildMemberAddEventArgs(this.ServiceProvider) { Guild = guild, Member = mbr }; await this._guildMemberAdded.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the guild member remove event. /// /// The user. /// The guild. internal async Task OnGuildMemberRemoveEventAsync(TransportUser user, DiscordGuild guild) { var usr = new DiscordUser(user); if (!guild.MembersInternal.TryRemove(user.Id, out var mbr)) mbr = new DiscordMember(usr) { Discord = this, GuildId = guild.Id }; guild.MemberCount--; _ = this.UserCache.AddOrUpdate(user.Id, usr, (old, @new) => @new); var ea = new GuildMemberRemoveEventArgs(this.ServiceProvider) { Guild = guild, Member = mbr }; await this._guildMemberRemoved.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the guild member update event. /// /// The member. /// The guild. /// The roles. /// The nick. /// If true, pending. internal async Task OnGuildMemberUpdateEventAsync(TransportMember member, DiscordGuild guild, IEnumerable roles, string nick, bool? pending) { var usr = new DiscordUser(member.User) { Discord = this }; usr = this.UserCache.AddOrUpdate(usr.Id, usr, (id, old) => { old.Username = usr.Username; old.Discriminator = usr.Discriminator; old.AvatarHash = usr.AvatarHash; return old; }); if (!guild.Members.TryGetValue(member.User.Id, out var mbr)) mbr = new DiscordMember(usr) { Discord = this, GuildId = guild.Id }; var nickOld = mbr.Nickname; var pendingOld = mbr.IsPending; var rolesOld = new ReadOnlyCollection(new List(mbr.Roles)); var cduOld = mbr.CommunicationDisabledUntil; mbr.AvatarHashInternal = member.AvatarHash; mbr.GuildAvatarHash = member.GuildAvatarHash; mbr.Nickname = nick; mbr.IsPending = pending; mbr.CommunicationDisabledUntil = member.CommunicationDisabledUntil; mbr.RoleIdsInternal.Clear(); mbr.RoleIdsInternal.AddRange(roles); var ea = new GuildMemberUpdateEventArgs(this.ServiceProvider) { Guild = guild, Member = mbr, NicknameAfter = mbr.Nickname, RolesAfter = new ReadOnlyCollection(new List(mbr.Roles)), PendingAfter = mbr.IsPending, TimeoutAfter = mbr.CommunicationDisabledUntil, NicknameBefore = nickOld, RolesBefore = rolesOld, PendingBefore = pendingOld, TimeoutBefore = cduOld }; await this._guildMemberUpdated.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the guild members chunk event. /// /// The dat. internal async Task OnGuildMembersChunkEventAsync(JObject dat) { var guild = this.Guilds[(ulong)dat["guild_id"]]; var chunkIndex = (int)dat["chunk_index"]; var chunkCount = (int)dat["chunk_count"]; var nonce = (string)dat["nonce"]; var mbrs = new HashSet(); var pres = new HashSet(); var members = dat["members"].ToObject(); var memCount = members.Count(); for (var i = 0; i < memCount; i++) { var mbr = new DiscordMember(members[i]) { Discord = this, GuildId = guild.Id }; if (!this.UserCache.ContainsKey(mbr.Id)) this.UserCache[mbr.Id] = new DiscordUser(members[i].User) { Discord = this }; guild.MembersInternal[mbr.Id] = mbr; mbrs.Add(mbr); } guild.MemberCount = guild.MembersInternal.Count; var ea = new GuildMembersChunkEventArgs(this.ServiceProvider) { Guild = guild, Members = new ReadOnlySet(mbrs), ChunkIndex = chunkIndex, ChunkCount = chunkCount, Nonce = nonce, }; if (dat["presences"] != null) { var presences = dat["presences"].ToObject(); var presCount = presences.Count(); for (var i = 0; i < presCount; i++) { var xp = presences[i]; xp.Discord = this; xp.Activity = new DiscordActivity(xp.RawActivity); if (xp.RawActivities != null) { xp.InternalActivities = new DiscordActivity[xp.RawActivities.Length]; for (var j = 0; j < xp.RawActivities.Length; j++) xp.InternalActivities[j] = new DiscordActivity(xp.RawActivities[j]); } pres.Add(xp); } ea.Presences = new ReadOnlySet(pres); } if (dat["not_found"] != null) { var nf = dat["not_found"].ToObject>(); ea.NotFound = new ReadOnlySet(nf); } await this._guildMembersChunked.InvokeAsync(this, ea).ConfigureAwait(false); } #endregion #region Guild Role /// /// Handles the guild role create event. /// /// The role. /// The guild. internal async Task OnGuildRoleCreateEventAsync(DiscordRole role, DiscordGuild guild) { role.Discord = this; role.GuildId = guild.Id; guild.RolesInternal[role.Id] = role; var ea = new GuildRoleCreateEventArgs(this.ServiceProvider) { Guild = guild, Role = role }; await this._guildRoleCreated.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the guild role update event. /// /// The role. /// The guild. internal async Task OnGuildRoleUpdateEventAsync(DiscordRole role, DiscordGuild guild) { var newRole = guild.GetRole(role.Id); var oldRole = new DiscordRole { GuildId = guild.Id, ColorInternal = newRole.ColorInternal, Discord = this, IsHoisted = newRole.IsHoisted, Id = newRole.Id, IsManaged = newRole.IsManaged, IsMentionable = newRole.IsMentionable, Name = newRole.Name, Permissions = newRole.Permissions, Position = newRole.Position }; newRole.GuildId = guild.Id; newRole.ColorInternal = role.ColorInternal; newRole.IsHoisted = role.IsHoisted; newRole.IsManaged = role.IsManaged; newRole.IsMentionable = role.IsMentionable; newRole.Name = role.Name; newRole.Permissions = role.Permissions; newRole.Position = role.Position; var ea = new GuildRoleUpdateEventArgs(this.ServiceProvider) { Guild = guild, RoleAfter = newRole, RoleBefore = oldRole }; await this._guildRoleUpdated.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the guild role delete event. /// /// The role id. /// The guild. internal async Task OnGuildRoleDeleteEventAsync(ulong roleId, DiscordGuild guild) { if (!guild.RolesInternal.TryRemove(roleId, out var role)) this.Logger.LogWarning($"Attempted to delete a nonexistent role ({roleId}) from guild ({guild})."); var ea = new GuildRoleDeleteEventArgs(this.ServiceProvider) { Guild = guild, Role = role }; await this._guildRoleDeleted.InvokeAsync(this, ea).ConfigureAwait(false); } #endregion #region Invite /// /// Handles the invite create event. /// /// The channel id. /// The guild id. /// The invite. internal async Task OnInviteCreateEventAsync(ulong channelId, ulong guildId, DiscordInvite invite) { var guild = this.InternalGetCachedGuild(guildId); var channel = this.InternalGetCachedChannel(channelId); invite.Discord = this; if (invite.Inviter is not null) { invite.Inviter.Discord = this; this.UserCache.AddOrUpdate(invite.Inviter.Id, invite.Inviter, (old, @new) => @new); } guild.Invites[invite.Code] = invite; var ea = new InviteCreateEventArgs(this.ServiceProvider) { Channel = channel, Guild = guild, Invite = invite }; await this._inviteCreated.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the invite delete event. /// /// The channel id. /// The guild id. /// The dat. internal async Task OnInviteDeleteEventAsync(ulong channelId, ulong guildId, JToken dat) { var guild = this.InternalGetCachedGuild(guildId); var channel = this.InternalGetCachedChannel(channelId); if (!guild.Invites.TryRemove(dat["code"].ToString(), out var invite)) { invite = dat.ToObject(); invite.Discord = this; } invite.IsRevoked = true; var ea = new InviteDeleteEventArgs(this.ServiceProvider) { Channel = channel, Guild = guild, Invite = invite }; await this._inviteDeleted.InvokeAsync(this, ea).ConfigureAwait(false); } #endregion #region Message /// /// Handles the message ack event. /// /// The chn. /// The message id. internal async Task OnMessageAckEventAsync(DiscordChannel chn, ulong messageId) { if (this.MessageCache == null || !this.MessageCache.TryGet(xm => xm.Id == messageId && xm.ChannelId == chn.Id, out var msg)) { msg = new DiscordMessage { Id = messageId, ChannelId = chn.Id, Discord = this, }; } await this._messageAcknowledged.InvokeAsync(this, new MessageAcknowledgeEventArgs(this.ServiceProvider) { Message = msg }).ConfigureAwait(false); } /// /// Handles the message create event. /// /// The message. /// The author. /// The member. /// The reference author. /// The reference member. internal async Task OnMessageCreateEventAsync(DiscordMessage message, TransportUser author, TransportMember member, TransportUser referenceAuthor, TransportMember referenceMember) { message.Discord = this; this.PopulateMessageReactionsAndCache(message, author, member); message.PopulateMentions(); if (message.Channel == null && message.ChannelId == default) this.Logger.LogWarning(LoggerEvents.WebSocketReceive, "Channel which the last message belongs to is not in cache - cache state might be invalid!"); if (message.ReferencedMessage != null) { message.ReferencedMessage.Discord = this; this.PopulateMessageReactionsAndCache(message.ReferencedMessage, referenceAuthor, referenceMember); message.ReferencedMessage.PopulateMentions(); } foreach (var sticker in message.Stickers) sticker.Discord = this; var ea = new MessageCreateEventArgs(this.ServiceProvider) { Message = message, MentionedUsers = new ReadOnlyCollection(message.MentionedUsersInternal), MentionedRoles = message.MentionedRolesInternal != null ? new ReadOnlyCollection(message.MentionedRolesInternal) : null, MentionedChannels = message.MentionedChannelsInternal != null ? new ReadOnlyCollection(message.MentionedChannelsInternal) : null }; await this._messageCreated.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the message update event. /// /// The message. /// The author. /// The member. /// The reference author. /// The reference member. internal async Task OnMessageUpdateEventAsync(DiscordMessage message, TransportUser author, TransportMember member, TransportUser referenceAuthor, TransportMember referenceMember) { DiscordGuild guild; message.Discord = this; var eventMessage = message; DiscordMessage oldmsg = null; if (this.Configuration.MessageCacheSize == 0 || this.MessageCache == null || !this.MessageCache.TryGet(xm => xm.Id == eventMessage.Id && xm.ChannelId == eventMessage.ChannelId, out message)) { message = eventMessage; this.PopulateMessageReactionsAndCache(message, author, member); guild = message.Channel?.Guild; if (message.ReferencedMessage != null) { message.ReferencedMessage.Discord = this; this.PopulateMessageReactionsAndCache(message.ReferencedMessage, referenceAuthor, referenceMember); message.ReferencedMessage.PopulateMentions(); } } else { oldmsg = new DiscordMessage(message); guild = message.Channel?.Guild; message.EditedTimestampRaw = eventMessage.EditedTimestampRaw; if (eventMessage.Content != null) message.Content = eventMessage.Content; message.EmbedsInternal.Clear(); message.EmbedsInternal.AddRange(eventMessage.EmbedsInternal); message.Pinned = eventMessage.Pinned; message.IsTts = eventMessage.IsTts; } message.PopulateMentions(); var ea = new MessageUpdateEventArgs(this.ServiceProvider) { Message = message, MessageBefore = oldmsg, MentionedUsers = new ReadOnlyCollection(message.MentionedUsersInternal), MentionedRoles = message.MentionedRolesInternal != null ? new ReadOnlyCollection(message.MentionedRolesInternal) : null, MentionedChannels = message.MentionedChannelsInternal != null ? new ReadOnlyCollection(message.MentionedChannelsInternal) : null }; await this._messageUpdated.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the message delete event. /// /// The message id. /// The channel id. /// The guild id. internal async Task OnMessageDeleteEventAsync(ulong messageId, ulong channelId, ulong? guildId) { var channel = this.InternalGetCachedChannel(channelId) ?? this.InternalGetCachedThread(channelId); var guild = this.InternalGetCachedGuild(guildId); if (channel == null || this.Configuration.MessageCacheSize == 0 || this.MessageCache == null || !this.MessageCache.TryGet(xm => xm.Id == messageId && xm.ChannelId == channelId, out var msg)) { msg = new DiscordMessage { Id = messageId, ChannelId = channelId, Discord = this, }; } if (this.Configuration.MessageCacheSize > 0) this.MessageCache?.Remove(xm => xm.Id == msg.Id && xm.ChannelId == channelId); var ea = new MessageDeleteEventArgs(this.ServiceProvider) { Channel = channel, Message = msg, Guild = guild }; await this._messageDeleted.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the message bulk delete event. /// /// The message ids. /// The channel id. /// The guild id. internal async Task OnMessageBulkDeleteEventAsync(ulong[] messageIds, ulong channelId, ulong? guildId) { var channel = this.InternalGetCachedChannel(channelId) ?? this.InternalGetCachedThread(channelId); var msgs = new List(messageIds.Length); foreach (var messageId in messageIds) { if (channel == null || this.Configuration.MessageCacheSize == 0 || this.MessageCache == null || !this.MessageCache.TryGet(xm => xm.Id == messageId && xm.ChannelId == channelId, out var msg)) { msg = new DiscordMessage { Id = messageId, ChannelId = channelId, Discord = this, }; } if (this.Configuration.MessageCacheSize > 0) this.MessageCache?.Remove(xm => xm.Id == msg.Id && xm.ChannelId == channelId); msgs.Add(msg); } var guild = this.InternalGetCachedGuild(guildId); var ea = new MessageBulkDeleteEventArgs(this.ServiceProvider) { Channel = channel, Messages = new ReadOnlyCollection(msgs), Guild = guild }; await this._messagesBulkDeleted.InvokeAsync(this, ea).ConfigureAwait(false); } #endregion #region Message Reaction /// /// Handles the message reaction add. /// /// The user id. /// The message id. /// The channel id. /// The guild id. /// The mbr. /// The emoji. internal async Task OnMessageReactionAddAsync(ulong userId, ulong messageId, ulong channelId, ulong? guildId, TransportMember mbr, DiscordEmoji emoji) { var channel = this.InternalGetCachedChannel(channelId) ?? this.InternalGetCachedThread(channelId); var guild = this.InternalGetCachedGuild(guildId); emoji.Discord = this; var usr = this.UpdateUser(new DiscordUser { Id = userId, Discord = this }, guildId, guild, mbr); if (channel == null || this.Configuration.MessageCacheSize == 0 || this.MessageCache == null || !this.MessageCache.TryGet(xm => xm.Id == messageId && xm.ChannelId == channelId, out var msg)) { msg = new DiscordMessage { Id = messageId, ChannelId = channelId, Discord = this, ReactionsInternal = new List() }; } var react = msg.ReactionsInternal.FirstOrDefault(xr => xr.Emoji == emoji); if (react == null) { msg.ReactionsInternal.Add(react = new DiscordReaction { Count = 1, Emoji = emoji, IsMe = this.CurrentUser.Id == userId }); } else { react.Count++; react.IsMe |= this.CurrentUser.Id == userId; } var ea = new MessageReactionAddEventArgs(this.ServiceProvider) { Message = msg, User = usr, Guild = guild, Emoji = emoji }; await this._messageReactionAdded.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the message reaction remove. /// /// The user id. /// The message id. /// The channel id. /// The guild id. /// The emoji. internal async Task OnMessageReactionRemoveAsync(ulong userId, ulong messageId, ulong channelId, ulong? guildId, DiscordEmoji emoji) { var channel = this.InternalGetCachedChannel(channelId) ?? this.InternalGetCachedThread(channelId); emoji.Discord = this; if (!this.UserCache.TryGetValue(userId, out var usr)) usr = new DiscordUser { Id = userId, Discord = this }; if (channel?.Guild != null) usr = channel.Guild.Members.TryGetValue(userId, out var member) ? member : new DiscordMember(usr) { Discord = this, GuildId = channel.GuildId.Value }; if (channel == null || this.Configuration.MessageCacheSize == 0 || this.MessageCache == null || !this.MessageCache.TryGet(xm => xm.Id == messageId && xm.ChannelId == channelId, out var msg)) { msg = new DiscordMessage { Id = messageId, ChannelId = channelId, Discord = this }; } var react = msg.ReactionsInternal?.FirstOrDefault(xr => xr.Emoji == emoji); if (react != null) { react.Count--; react.IsMe &= this.CurrentUser.Id != userId; if (msg.ReactionsInternal != null && react.Count <= 0) // shit happens for (var i = 0; i < msg.ReactionsInternal.Count; i++) if (msg.ReactionsInternal[i].Emoji == emoji) { msg.ReactionsInternal.RemoveAt(i); break; } } var guild = this.InternalGetCachedGuild(guildId); var ea = new MessageReactionRemoveEventArgs(this.ServiceProvider) { Message = msg, User = usr, Guild = guild, Emoji = emoji }; await this._messageReactionRemoved.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the message reaction remove all. /// /// The message id. /// The channel id. /// The guild id. internal async Task OnMessageReactionRemoveAllAsync(ulong messageId, ulong channelId, ulong? guildId) { var channel = this.InternalGetCachedChannel(channelId) ?? this.InternalGetCachedThread(channelId); if (channel == null || this.Configuration.MessageCacheSize == 0 || this.MessageCache == null || !this.MessageCache.TryGet(xm => xm.Id == messageId && xm.ChannelId == channelId, out var msg)) { msg = new DiscordMessage { Id = messageId, ChannelId = channelId, Discord = this }; } msg.ReactionsInternal?.Clear(); var guild = this.InternalGetCachedGuild(guildId); var ea = new MessageReactionsClearEventArgs(this.ServiceProvider) { Message = msg }; await this._messageReactionsCleared.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the message reaction remove emoji. /// /// The message id. /// The channel id. /// The guild id. /// The dat. internal async Task OnMessageReactionRemoveEmojiAsync(ulong messageId, ulong channelId, ulong guildId, JToken dat) { var guild = this.InternalGetCachedGuild(guildId); var channel = this.InternalGetCachedChannel(channelId) ?? this.InternalGetCachedThread(channelId); if (channel == null || this.Configuration.MessageCacheSize == 0 || this.MessageCache == null || !this.MessageCache.TryGet(xm => xm.Id == messageId && xm.ChannelId == channelId, out var msg)) { msg = new DiscordMessage { Id = messageId, ChannelId = channelId, Discord = this }; } var partialEmoji = dat.ToObject(); if (!guild.EmojisInternal.TryGetValue(partialEmoji.Id, out var emoji)) { emoji = partialEmoji; emoji.Discord = this; } msg.ReactionsInternal?.RemoveAll(r => r.Emoji.Equals(emoji)); var ea = new MessageReactionRemoveEmojiEventArgs(this.ServiceProvider) { Channel = channel, Guild = guild, Message = msg, Emoji = emoji }; await this._messageReactionRemovedEmoji.InvokeAsync(this, ea).ConfigureAwait(false); } #endregion #region Stage Instance /// /// Dispatches the event. /// /// The created stage instance. internal async Task OnStageInstanceCreateEventAsync(DiscordStageInstance stage) { stage.Discord = this; var guild = this.InternalGetCachedGuild(stage.GuildId); guild.StageInstancesInternal[stage.Id] = stage; await this._stageInstanceCreated.InvokeAsync(this, new StageInstanceCreateEventArgs(this.ServiceProvider) { StageInstance = stage, Guild = stage.Guild }).ConfigureAwait(false); } /// /// Dispatches the event. /// /// The updated stage instance. internal async Task OnStageInstanceUpdateEventAsync(DiscordStageInstance stage) { stage.Discord = this; var guild = this.InternalGetCachedGuild(stage.GuildId); guild.StageInstancesInternal[stage.Id] = stage; await this._stageInstanceUpdated.InvokeAsync(this, new StageInstanceUpdateEventArgs(this.ServiceProvider) { StageInstance = stage, Guild = stage.Guild }).ConfigureAwait(false); } /// /// Dispatches the event. /// /// The deleted stage instance. internal async Task OnStageInstanceDeleteEventAsync(DiscordStageInstance stage) { stage.Discord = this; var guild = this.InternalGetCachedGuild(stage.GuildId); guild.StageInstancesInternal[stage.Id] = stage; await this._stageInstanceDeleted.InvokeAsync(this, new StageInstanceDeleteEventArgs(this.ServiceProvider) { StageInstance = stage, Guild = stage.Guild }).ConfigureAwait(false); } #endregion #region Thread /// /// Dispatches the event. /// /// The created thread. internal async Task OnThreadCreateEventAsync(DiscordThreadChannel thread) { thread.Discord = this; this.InternalGetCachedGuild(thread.GuildId).ThreadsInternal.AddOrUpdate(thread.Id, thread, (oldThread, newThread) => newThread); await this._threadCreated.InvokeAsync(this, new ThreadCreateEventArgs(this.ServiceProvider) { Thread = thread, Guild = thread.Guild, Parent = thread.Parent }).ConfigureAwait(false); } /// /// Dispatches the event. /// /// The updated thread. internal async Task OnThreadUpdateEventAsync(DiscordThreadChannel thread) { if (thread == null) return; thread.Discord = this; var guild = thread.Guild; var threadNew = this.InternalGetCachedThread(thread.Id); DiscordThreadChannel threadOld = null; ThreadUpdateEventArgs updateEvent; if (threadNew != null) { threadOld = new DiscordThreadChannel { Discord = this, Type = threadNew.Type, ThreadMetadata = thread.ThreadMetadata, ThreadMembersInternal = threadNew.ThreadMembersInternal, ParentId = thread.ParentId, OwnerId = thread.OwnerId, Name = thread.Name, LastMessageId = threadNew.LastMessageId, MessageCount = thread.MessageCount, MemberCount = thread.MemberCount, GuildId = thread.GuildId, LastPinTimestampRaw = threadNew.LastPinTimestampRaw, PerUserRateLimit = threadNew.PerUserRateLimit, CurrentMember = threadNew.CurrentMember }; threadNew.ThreadMetadata = thread.ThreadMetadata; threadNew.ParentId = thread.ParentId; threadNew.OwnerId = thread.OwnerId; threadNew.Name = thread.Name; threadNew.LastMessageId = thread.LastMessageId.HasValue ? thread.LastMessageId : threadOld.LastMessageId; threadNew.MessageCount = thread.MessageCount; threadNew.MemberCount = thread.MemberCount; threadNew.GuildId = thread.GuildId; updateEvent = new ThreadUpdateEventArgs(this.ServiceProvider) { ThreadAfter = thread, ThreadBefore = threadOld, Guild = thread.Guild, Parent = thread.Parent }; } else { updateEvent = new ThreadUpdateEventArgs(this.ServiceProvider) { ThreadAfter = thread, Guild = thread.Guild, Parent = thread.Parent }; guild.ThreadsInternal[thread.Id] = thread; } await this._threadUpdated.InvokeAsync(this, updateEvent).ConfigureAwait(false); } /// /// Dispatches the event. /// /// The deleted thread. internal async Task OnThreadDeleteEventAsync(DiscordThreadChannel thread) { if (thread == null) return; thread.Discord = this; var gld = thread.Guild; if (gld.ThreadsInternal.TryRemove(thread.Id, out var cachedThread)) thread = cachedThread; await this._threadDeleted.InvokeAsync(this, new ThreadDeleteEventArgs(this.ServiceProvider) { Thread = thread, Guild = thread.Guild, Parent = thread.Parent, Type = thread.Type }).ConfigureAwait(false); } /// /// Dispatches the event. /// /// The synced guild. /// The synced channel ids. /// The synced threads. /// The synced members. internal async Task OnThreadListSyncEventAsync(DiscordGuild guild, IReadOnlyList channelIds, IReadOnlyList threads, IReadOnlyList members) { guild.Discord = this; var channels = channelIds.Select(x => guild.GetChannel(x.Value)); //getting channel objects foreach (var chan in channels) { chan.Discord = this; } threads.Select(x => x.Discord = this); await this._threadListSynced.InvokeAsync(this, new ThreadListSyncEventArgs(this.ServiceProvider) { Guild = guild, Channels = channels.ToList().AsReadOnly(), Threads = threads, Members = members.ToList().AsReadOnly() }).ConfigureAwait(false); } /// /// Dispatches the event. /// /// The updated member. internal async Task OnThreadMemberUpdateEventAsync(DiscordThreadChannelMember member) { member.Discord = this; var thread = this.InternalGetCachedThread(member.Id); if (thread == null) { var tempThread = await this.ApiClient.GetThreadAsync(member.Id); thread = this.GuildsInternal[member.GuildId].ThreadsInternal.AddOrUpdate(member.Id, tempThread, (old, newThread) => newThread); } thread.CurrentMember = member; thread.Guild.ThreadsInternal.AddOrUpdate(member.Id, thread, (oldThread, newThread) => newThread); await this._threadMemberUpdated.InvokeAsync(this, new ThreadMemberUpdateEventArgs(this.ServiceProvider) { ThreadMember = member, Thread = thread }).ConfigureAwait(false); } /// /// Dispatches the event. /// /// The target guild. /// The thread id of the target thread this update belongs to. /// The added members. /// The ids of the removed members. /// The new member count. internal async Task OnThreadMembersUpdateEventAsync(DiscordGuild guild, ulong threadId, JArray membersAdded, JArray membersRemoved, int memberCount) { var thread = this.InternalGetCachedThread(threadId); if (thread == null) { var tempThread = await this.ApiClient.GetThreadAsync(threadId); thread = guild.ThreadsInternal.AddOrUpdate(threadId, tempThread, (old, newThread) => newThread); } thread.Discord = this; guild.Discord = this; List addedMembers = new(); List removedMemberIds = new(); if (membersAdded != null) { foreach (var xj in membersAdded) { var xtm = xj.ToDiscordObject(); xtm.Discord = this; xtm.GuildId = guild.Id; if (xtm != null) addedMembers.Add(xtm); if (xtm.Id == this.CurrentUser.Id) thread.CurrentMember = xtm; } } var removedMembers = new List(); if (membersRemoved != null) { foreach (var removedId in membersRemoved) { removedMembers.Add(guild.MembersInternal.TryGetValue((ulong)removedId, out var member) ? member : new DiscordMember { Id = (ulong)removedId, GuildId = guild.Id, Discord = this }); } } if (removedMemberIds.Contains(this.CurrentUser.Id)) //indicates the bot was removed from the thread thread.CurrentMember = null; thread.MemberCount = memberCount; var threadMembersUpdateArg = new ThreadMembersUpdateEventArgs(this.ServiceProvider) { Guild = guild, Thread = thread, AddedMembers = addedMembers, RemovedMembers = removedMembers, MemberCount = memberCount }; await this._threadMembersUpdated.InvokeAsync(this, threadMembersUpdateArg).ConfigureAwait(false); } #endregion #region Activities /// /// Dispatches the event. /// /// The transport activity. /// The guild. /// The channel id. /// The users in the activity. /// The application id. /// A Task. internal async Task OnEmbeddedActivityUpdateAsync(JObject trActivity, DiscordGuild guild, ulong channelId, JArray jUsers, ulong appId) => await Task.Delay(20); /*{ try { var users = j_users?.ToObject>(); DiscordActivity old = null; var uid = $"{guild.Id}_{channel_id}_{app_id}"; if (this._embeddedActivities.TryGetValue(uid, out var activity)) { old = new DiscordActivity(activity); DiscordJson.PopulateObject(tr_activity, activity); } else { activity = tr_activity.ToObject(); this._embeddedActivities[uid] = activity; } var activity_users = new List(); var channel = this.InternalGetCachedChannel(channel_id) ?? await this.ApiClient.GetChannelAsync(channel_id); if (users != null) { foreach (var user in users) { var activity_user = guild._members.TryGetValue(user, out var member) ? member : new DiscordMember { Id = user, _guild_id = guild.Id, Discord = this }; activity_users.Add(activity_user); } } else activity_users = null; var ea = new EmbeddedActivityUpdateEventArgs(this.ServiceProvider) { Guild = guild, Users = activity_users, Channel = channel }; await this._embeddedActivityUpdated.InvokeAsync(this, ea).ConfigureAwait(false); } catch (Exception ex) { this.Logger.LogError(ex, ex.Message); } }*/ #endregion #region User/Presence Update /// /// Handles the presence update event. /// /// The raw presence. /// The raw user. internal async Task OnPresenceUpdateEventAsync(JObject rawPresence, JObject rawUser) { var uid = (ulong)rawUser["id"]; DiscordPresence old = null; if (this.PresencesInternal.TryGetValue(uid, out var presence)) { old = new DiscordPresence(presence); DiscordJson.PopulateObject(rawPresence, presence); } else { presence = rawPresence.ToObject(); presence.Discord = this; presence.Activity = new DiscordActivity(presence.RawActivity); this.PresencesInternal[presence.InternalUser.Id] = presence; } // reuse arrays / avoid linq (this is a hot zone) if (presence.Activities == null || rawPresence["activities"] == null) { presence.InternalActivities = Array.Empty(); } else { if (presence.InternalActivities.Length != presence.RawActivities.Length) presence.InternalActivities = new DiscordActivity[presence.RawActivities.Length]; for (var i = 0; i < presence.InternalActivities.Length; i++) presence.InternalActivities[i] = new DiscordActivity(presence.RawActivities[i]); if (presence.InternalActivities.Length > 0) { presence.RawActivity = presence.RawActivities[0]; if (presence.Activity != null) presence.Activity.UpdateWith(presence.RawActivity); else presence.Activity = new DiscordActivity(presence.RawActivity); } } if (this.UserCache.TryGetValue(uid, out var usr)) { if (old != null) { old.InternalUser.Username = usr.Username; old.InternalUser.Discriminator = usr.Discriminator; old.InternalUser.AvatarHash = usr.AvatarHash; } if (rawUser["username"] is object) usr.Username = (string)rawUser["username"]; if (rawUser["discriminator"] is object) usr.Discriminator = (string)rawUser["discriminator"]; if (rawUser["avatar"] is object) usr.AvatarHash = (string)rawUser["avatar"]; presence.InternalUser.Username = usr.Username; presence.InternalUser.Discriminator = usr.Discriminator; presence.InternalUser.AvatarHash = usr.AvatarHash; } var usrafter = usr ?? new DiscordUser(presence.InternalUser); var ea = new PresenceUpdateEventArgs(this.ServiceProvider) { Status = presence.Status, Activity = presence.Activity, User = usr, PresenceBefore = old, PresenceAfter = presence, UserBefore = old != null ? new DiscordUser(old.InternalUser) : usrafter, UserAfter = usrafter }; await this._presenceUpdated.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the user settings update event. /// /// The user. internal async Task OnUserSettingsUpdateEventAsync(TransportUser user) { var usr = new DiscordUser(user) { Discord = this }; var ea = new UserSettingsUpdateEventArgs(this.ServiceProvider) { User = usr }; await this._userSettingsUpdated.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the user update event. /// /// The user. internal async Task OnUserUpdateEventAsync(TransportUser user) { var usrOld = new DiscordUser { AvatarHash = this.CurrentUser.AvatarHash, Discord = this, Discriminator = this.CurrentUser.Discriminator, Email = this.CurrentUser.Email, Id = this.CurrentUser.Id, IsBot = this.CurrentUser.IsBot, MfaEnabled = this.CurrentUser.MfaEnabled, Username = this.CurrentUser.Username, Verified = this.CurrentUser.Verified }; this.CurrentUser.AvatarHash = user.AvatarHash; this.CurrentUser.Discriminator = user.Discriminator; this.CurrentUser.Email = user.Email; this.CurrentUser.Id = user.Id; this.CurrentUser.IsBot = user.IsBot; this.CurrentUser.MfaEnabled = user.MfaEnabled; this.CurrentUser.Username = user.Username; this.CurrentUser.Verified = user.Verified; var ea = new UserUpdateEventArgs(this.ServiceProvider) { UserAfter = this.CurrentUser, UserBefore = usrOld }; await this._userUpdated.InvokeAsync(this, ea).ConfigureAwait(false); } #endregion #region Voice /// /// Handles the voice state update event. /// /// The raw. internal async Task OnVoiceStateUpdateEventAsync(JObject raw) { var gid = (ulong)raw["guild_id"]; var uid = (ulong)raw["user_id"]; var gld = this.GuildsInternal[gid]; var vstateNew = raw.ToObject(); vstateNew.Discord = this; gld.VoiceStatesInternal.TryRemove(uid, out var vstateOld); if (vstateNew.Channel != null) { gld.VoiceStatesInternal[vstateNew.UserId] = vstateNew; } if (gld.MembersInternal.TryGetValue(uid, out var mbr)) { mbr.IsMuted = vstateNew.IsServerMuted; mbr.IsDeafened = vstateNew.IsServerDeafened; } else { var transportMbr = vstateNew.TransportMember; this.UpdateUser(new DiscordUser(transportMbr.User) { Discord = this }, gid, gld, transportMbr); } var ea = new VoiceStateUpdateEventArgs(this.ServiceProvider) { Guild = vstateNew.Guild, Channel = vstateNew.Channel, User = vstateNew.User, SessionId = vstateNew.SessionId, Before = vstateOld, After = vstateNew }; await this._voiceStateUpdated.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the voice server update event. /// /// The endpoint. /// The token. /// The guild. internal async Task OnVoiceServerUpdateEventAsync(string endpoint, string token, DiscordGuild guild) { var ea = new VoiceServerUpdateEventArgs(this.ServiceProvider) { Endpoint = endpoint, VoiceToken = token, Guild = guild }; await this._voiceServerUpdated.InvokeAsync(this, ea).ConfigureAwait(false); } #endregion #region Commands /// /// Handles the application command create. /// /// The cmd. /// The guild_id. internal async Task OnApplicationCommandCreateAsync(DiscordApplicationCommand cmd, ulong? guildId) { cmd.Discord = this; var guild = this.InternalGetCachedGuild(guildId); if (guild == null && guildId.HasValue) { guild = new DiscordGuild { Id = guildId.Value, Discord = this }; } var ea = new ApplicationCommandEventArgs(this.ServiceProvider) { Guild = guild, Command = cmd }; await this._applicationCommandCreated.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the application command update. /// /// The cmd. /// The guild_id. internal async Task OnApplicationCommandUpdateAsync(DiscordApplicationCommand cmd, ulong? guildId) { cmd.Discord = this; var guild = this.InternalGetCachedGuild(guildId); if (guild == null && guildId.HasValue) { guild = new DiscordGuild { Id = guildId.Value, Discord = this }; } var ea = new ApplicationCommandEventArgs(this.ServiceProvider) { Guild = guild, Command = cmd }; await this._applicationCommandUpdated.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the application command delete. /// /// The cmd. /// The guild_id. internal async Task OnApplicationCommandDeleteAsync(DiscordApplicationCommand cmd, ulong? guildId) { cmd.Discord = this; var guild = this.InternalGetCachedGuild(guildId); if (guild == null && guildId.HasValue) { guild = new DiscordGuild { Id = guildId.Value, Discord = this }; } var ea = new ApplicationCommandEventArgs(this.ServiceProvider) { Guild = guild, Command = cmd }; await this._applicationCommandDeleted.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the guild application command counts update. /// /// The count. /// The count. /// The count. /// The guild_id. /// Count of application commands. internal async Task OnGuildApplicationCommandCountsUpdateAsync(int sc, int ucmc, int mcmc, ulong guildId) { var guild = this.InternalGetCachedGuild(guildId); if (guild == null) { guild = new DiscordGuild { Id = guildId, Discord = this }; } var ea = new GuildApplicationCommandCountEventArgs(this.ServiceProvider) { SlashCommands = sc, UserContextMenuCommands = ucmc, MessageContextMenuCommands = mcmc, Guild = guild }; await this._guildApplicationCommandCountUpdated.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the application command permissions update. /// /// The new permissions. /// The command id. /// The guild id. /// The application id. internal async Task OnApplicationCommandPermissionsUpdateAsync(IEnumerable perms, ulong cId, ulong guildId, ulong aId) { if (aId != this.CurrentApplication.Id) return; var guild = this.InternalGetCachedGuild(guildId); DiscordApplicationCommand cmd; try { cmd = await this.GetGuildApplicationCommandAsync(guildId, cId); } catch (NotFoundException) { cmd = await this.GetGlobalApplicationCommandAsync(cId); } if (guild == null) { guild = new DiscordGuild { Id = guildId, Discord = this }; } var ea = new ApplicationCommandPermissionsUpdateEventArgs(this.ServiceProvider) { Permissions = perms.ToList(), Command = cmd, ApplicationId = aId, Guild = guild }; await this._applicationCommandPermissionsUpdated.InvokeAsync(this, ea).ConfigureAwait(false); } #endregion #region Interaction /// /// Handles the interaction create. /// /// The guild id. /// The channel id. /// The user. /// The member. /// The interaction. internal async Task OnInteractionCreateAsync(ulong? guildId, ulong channelId, TransportUser user, TransportMember member, DiscordInteraction interaction) { var usr = new DiscordUser(user) { Discord = this }; interaction.ChannelId = channelId; interaction.GuildId = guildId; interaction.Discord = this; interaction.Data.Discord = this; if (member != null) { usr = new DiscordMember(member) { GuildId = guildId.Value, Discord = this }; this.UpdateUser(usr, guildId, interaction.Guild, member); } else { this.UserCache.AddOrUpdate(usr.Id, usr, (old, @new) => @new); } interaction.User = usr; var resolved = interaction.Data.Resolved; if (resolved != null) { if (resolved.Users != null) { foreach (var c in resolved.Users) { c.Value.Discord = this; this.UserCache.AddOrUpdate(c.Value.Id, c.Value, (old, @new) => @new); } } if (resolved.Members != null) { foreach (var c in resolved.Members) { c.Value.Discord = this; c.Value.Id = c.Key; c.Value.GuildId = guildId.Value; c.Value.User.Discord = this; this.UserCache.AddOrUpdate(c.Value.User.Id, c.Value.User, (old, @new) => @new); } } if (resolved.Channels != null) { foreach (var c in resolved.Channels) { c.Value.Discord = this; if (guildId.HasValue) c.Value.GuildId = guildId.Value; } } if (resolved.Roles != null) { foreach (var c in resolved.Roles) { c.Value.Discord = this; if (guildId.HasValue) c.Value.GuildId = guildId.Value; } } if (resolved.Messages != null) { foreach (var m in resolved.Messages) { m.Value.Discord = this; if (guildId.HasValue) m.Value.GuildId = guildId.Value; } } } if (interaction.Type is InteractionType.Component || interaction.Type is InteractionType.ModalSubmit) { if (interaction.Message != null) { interaction.Message.Discord = this; interaction.Message.ChannelId = interaction.ChannelId; } var cea = new ComponentInteractionCreateEventArgs(this.ServiceProvider) { Message = interaction.Message, Interaction = interaction }; await this._componentInteractionCreated.InvokeAsync(this, cea).ConfigureAwait(false); } else { if (interaction.Data.Target.HasValue) // Context-Menu. // { var targetId = interaction.Data.Target.Value; DiscordUser targetUser = null; DiscordMember targetMember = null; DiscordMessage targetMessage = null; interaction.Data.Resolved.Messages?.TryGetValue(targetId, out targetMessage); interaction.Data.Resolved.Members?.TryGetValue(targetId, out targetMember); interaction.Data.Resolved.Users?.TryGetValue(targetId, out targetUser); var ctea = new ContextMenuInteractionCreateEventArgs(this.ServiceProvider) { Interaction = interaction, TargetUser = targetMember ?? targetUser, TargetMessage = targetMessage, Type = interaction.Data.Type, }; await this._contextMenuInteractionCreated.InvokeAsync(this, ctea).ConfigureAwait(false); } else { var ea = new InteractionCreateEventArgs(this.ServiceProvider) { Interaction = interaction }; await this._interactionCreated.InvokeAsync(this, ea).ConfigureAwait(false); } } } #endregion #region Misc /// /// Handles the typing start event. /// /// The user id. /// The channel id. /// The channel. /// The guild id. /// The started. /// The mbr. internal async Task OnTypingStartEventAsync(ulong userId, ulong channelId, DiscordChannel channel, ulong? guildId, DateTimeOffset started, TransportMember mbr) { if (channel == null) { channel = new DiscordChannel { Discord = this, Id = channelId, GuildId = guildId ?? default, }; } var guild = this.InternalGetCachedGuild(guildId); var usr = this.UpdateUser(new DiscordUser { Id = userId, Discord = this }, guildId, guild, mbr); var ea = new TypingStartEventArgs(this.ServiceProvider) { Channel = channel, User = usr, Guild = guild, StartedAt = started }; await this._typingStarted.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the webhooks update. /// /// The channel. /// The guild. internal async Task OnWebhooksUpdateAsync(DiscordChannel channel, DiscordGuild guild) { var ea = new WebhooksUpdateEventArgs(this.ServiceProvider) { Channel = channel, Guild = guild }; await this._webhooksUpdated.InvokeAsync(this, ea).ConfigureAwait(false); } /// /// Handles the unknown event. /// /// The payload. internal async Task OnUnknownEventAsync(GatewayPayload payload) { var ea = new UnknownEventArgs(this.ServiceProvider) { EventName = payload.EventName, Json = (payload.Data as JObject)?.ToString() }; await this._unknownEvent.InvokeAsync(this, ea).ConfigureAwait(false); } #endregion #endregion } } diff --git a/DisCatSharp/Clients/DiscordClient.WebSocket.cs b/DisCatSharp/Clients/DiscordClient.WebSocket.cs index 203515a60..25b0c247d 100644 --- a/DisCatSharp/Clients/DiscordClient.WebSocket.cs +++ b/DisCatSharp/Clients/DiscordClient.WebSocket.cs @@ -1,601 +1,601 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Concurrent; using System.IO; using System.Threading; using System.Threading.Tasks; using DisCatSharp.Entities; using DisCatSharp.EventArgs; using DisCatSharp.Net.Abstractions; using DisCatSharp.Net.WebSocket; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace DisCatSharp { /// /// Represents a discord websocket client. /// public sealed partial class DiscordClient { #region Private Fields private int _heartbeatInterval; private DateTimeOffset _lastHeartbeat; [System.Diagnostics.CodeAnalysis.SuppressMessage("CodeQuality", "IDE0052:Remove unread private members", Justification = "")] private Task _heartbeatTask; internal static DateTimeOffset DiscordEpoch = new(2015, 1, 1, 0, 0, 0, TimeSpan.Zero); - private int _skippedHeartbeats = 0; + private int _skippedHeartbeats; private long _lastSequence; internal IWebSocketClient WebSocketClient; private PayloadDecompressor _payloadDecompressor; private CancellationTokenSource _cancelTokenSource; private CancellationToken _cancelToken; #endregion #region Connection Semaphore /// /// Gets the socket locks. /// - private static ConcurrentDictionary s_socketLocks { get; } = new ConcurrentDictionary(); + private static ConcurrentDictionary s_socketLocks { get; } = new(); /// /// Gets the session lock. /// private readonly ManualResetEventSlim _sessionLock = new(true); #endregion #region Internal Connection Methods /// /// Internals the reconnect async. /// /// If true, start new session. /// The code. /// The message. /// A Task. private Task InternalReconnectAsync(bool startNewSession = false, int code = 1000, string message = "") { if (startNewSession) this._sessionId = null; _ = this.WebSocketClient.DisconnectAsync(code, message); return Task.CompletedTask; } /// /// Internals the connect async. /// /// A Task. internal async Task InternalConnectAsync() { SocketLock socketLock = null; try { if (this.GatewayInfo == null) await this.InternalUpdateGatewayAsync().ConfigureAwait(false); await this.InitializeAsync().ConfigureAwait(false); socketLock = this.GetSocketLock(); await socketLock.LockAsync().ConfigureAwait(false); } catch { socketLock?.UnlockAfter(TimeSpan.Zero); throw; } if (!this.Presences.ContainsKey(this.CurrentUser.Id)) { this.PresencesInternal[this.CurrentUser.Id] = new DiscordPresence { Discord = this, RawActivity = new TransportActivity(), Activity = new DiscordActivity(), Status = UserStatus.Online, InternalUser = new TransportUser { Id = this.CurrentUser.Id, Username = this.CurrentUser.Username, Discriminator = this.CurrentUser.Discriminator, AvatarHash = this.CurrentUser.AvatarHash } }; } else { var pr = this.PresencesInternal[this.CurrentUser.Id]; pr.RawActivity = new TransportActivity(); pr.Activity = new DiscordActivity(); pr.Status = UserStatus.Online; } Volatile.Write(ref this._skippedHeartbeats, 0); this.WebSocketClient = this.Configuration.WebSocketClientFactory(this.Configuration.Proxy, this.ServiceProvider); this._payloadDecompressor = this.Configuration.GatewayCompressionLevel != GatewayCompressionLevel.None ? new PayloadDecompressor(this.Configuration.GatewayCompressionLevel) : null; this._cancelTokenSource = new CancellationTokenSource(); this._cancelToken = this._cancelTokenSource.Token; this.WebSocketClient.Connected += SocketOnConnect; this.WebSocketClient.Disconnected += SocketOnDisconnect; this.WebSocketClient.MessageReceived += SocketOnMessage; this.WebSocketClient.ExceptionThrown += SocketOnException; var gwuri = new QueryUriBuilder(this.GatewayUri) .AddParameter("v", this.Configuration.ApiVersion) .AddParameter("encoding", "json"); if (this.Configuration.GatewayCompressionLevel == GatewayCompressionLevel.Stream) gwuri.AddParameter("compress", "zlib-stream"); await this.WebSocketClient.ConnectAsync(gwuri.Build()).ConfigureAwait(false); Task SocketOnConnect(IWebSocketClient sender, SocketEventArgs e) => this._socketOpened.InvokeAsync(this, e); async Task SocketOnMessage(IWebSocketClient sender, SocketMessageEventArgs e) { string msg = null; if (e is SocketTextMessageEventArgs etext) { msg = etext.Message; } else if (e is SocketBinaryMessageEventArgs ebin) // :DDDD { using var ms = new MemoryStream(); if (!this._payloadDecompressor.TryDecompress(new ArraySegment(ebin.Message), ms)) { this.Logger.LogError(LoggerEvents.WebSocketReceiveFailure, "Payload decompression failed"); return; } ms.Position = 0; using var sr = new StreamReader(ms, Utilities.UTF8); msg = await sr.ReadToEndAsync().ConfigureAwait(false); } try { this.Logger.LogTrace(LoggerEvents.GatewayWsRx, msg); await this.HandleSocketMessageAsync(msg).ConfigureAwait(false); } catch (Exception ex) { this.Logger.LogError(LoggerEvents.WebSocketReceiveFailure, ex, "Socket handler suppressed an exception"); } } Task SocketOnException(IWebSocketClient sender, SocketErrorEventArgs e) => this._socketErrored.InvokeAsync(this, e); async Task SocketOnDisconnect(IWebSocketClient sender, SocketCloseEventArgs e) { // release session and connection this._connectionLock.Set(); this._sessionLock.Set(); if (!this._disposed) this._cancelTokenSource.Cancel(); this.Logger.LogDebug(LoggerEvents.ConnectionClose, "Connection closed ({0}, '{1}')", e.CloseCode, e.CloseMessage); await this._socketClosed.InvokeAsync(this, e).ConfigureAwait(false); if (this.Configuration.AutoReconnect && (e.CloseCode < 4001 || e.CloseCode >= 5000)) { this.Logger.LogCritical(LoggerEvents.ConnectionClose, "Connection terminated ({0}, '{1}'), reconnecting", e.CloseCode, e.CloseMessage); if (this._status == null) await this.ConnectAsync().ConfigureAwait(false); else if (this._status.IdleSince.HasValue) await this.ConnectAsync(this._status.ActivityInternal, this._status.Status, Utilities.GetDateTimeOffsetFromMilliseconds(this._status.IdleSince.Value)).ConfigureAwait(false); else await this.ConnectAsync(this._status.ActivityInternal, this._status.Status).ConfigureAwait(false); } else { this.Logger.LogCritical(LoggerEvents.ConnectionClose, "Connection terminated ({0}, '{1}')", e.CloseCode, e.CloseMessage); } } } #endregion #region WebSocket (Events) /// /// Handles the socket message async. /// /// The data. /// A Task. internal async Task HandleSocketMessageAsync(string data) { var payload = JsonConvert.DeserializeObject(data); this._lastSequence = payload.Sequence ?? this._lastSequence; switch (payload.OpCode) { case GatewayOpCode.Dispatch: await this.HandleDispatchAsync(payload).ConfigureAwait(false); break; case GatewayOpCode.Heartbeat: await this.OnHeartbeatAsync((long)payload.Data).ConfigureAwait(false); break; case GatewayOpCode.Reconnect: await this.OnReconnectAsync().ConfigureAwait(false); break; case GatewayOpCode.InvalidSession: await this.OnInvalidateSessionAsync((bool)payload.Data).ConfigureAwait(false); break; case GatewayOpCode.Hello: await this.OnHelloAsync((payload.Data as JObject).ToObject()).ConfigureAwait(false); break; case GatewayOpCode.HeartbeatAck: await this.OnHeartbeatAckAsync().ConfigureAwait(false); break; default: this.Logger.LogWarning(LoggerEvents.WebSocketReceive, "Unknown Discord opcode: {0}\nPayload: {1}", payload.OpCode, payload.Data); break; } } /// /// Ons the heartbeat async. /// /// The seq. /// A Task. internal async Task OnHeartbeatAsync(long seq) { this.Logger.LogTrace(LoggerEvents.WebSocketReceive, "Received HEARTBEAT (OP1)"); await this.SendHeartbeatAsync(seq).ConfigureAwait(false); } /// /// Ons the reconnect async. /// /// A Task. internal async Task OnReconnectAsync() { this.Logger.LogTrace(LoggerEvents.WebSocketReceive, "Received RECONNECT (OP7)"); await this.InternalReconnectAsync(code: 4000, message: "OP7 acknowledged").ConfigureAwait(false); } /// /// Ons the invalidate session async. /// /// If true, data. /// A Task. internal async Task OnInvalidateSessionAsync(bool data) { // begin a session if one is not open already if (this._sessionLock.Wait(0)) this._sessionLock.Reset(); // we are sending a fresh resume/identify, so lock the socket var socketLock = this.GetSocketLock(); await socketLock.LockAsync().ConfigureAwait(false); socketLock.UnlockAfter(TimeSpan.FromSeconds(5)); if (data) { this.Logger.LogTrace(LoggerEvents.WebSocketReceive, "Received INVALID_SESSION (OP9, true)"); await Task.Delay(6000).ConfigureAwait(false); await this.SendResumeAsync().ConfigureAwait(false); } else { this.Logger.LogTrace(LoggerEvents.WebSocketReceive, "Received INVALID_SESSION (OP9, false)"); this._sessionId = null; await this.SendIdentifyAsync(this._status).ConfigureAwait(false); } } /// /// Ons the hello async. /// /// The hello. /// A Task. internal async Task OnHelloAsync(GatewayHello hello) { this.Logger.LogTrace(LoggerEvents.WebSocketReceive, "Received HELLO (OP10)"); if (this._sessionLock.Wait(0)) { this._sessionLock.Reset(); this.GetSocketLock().UnlockAfter(TimeSpan.FromSeconds(5)); } else { this.Logger.LogWarning(LoggerEvents.SessionUpdate, "Attempt to start a session while another session is active"); return; } Interlocked.CompareExchange(ref this._skippedHeartbeats, 0, 0); this._heartbeatInterval = hello.HeartbeatInterval; this._heartbeatTask = Task.Run(this.HeartbeatLoopAsync, this._cancelToken); if (string.IsNullOrEmpty(this._sessionId)) await this.SendIdentifyAsync(this._status).ConfigureAwait(false); else await this.SendResumeAsync().ConfigureAwait(false); } /// /// Ons the heartbeat ack async. /// /// A Task. internal async Task OnHeartbeatAckAsync() { Interlocked.Decrement(ref this._skippedHeartbeats); var ping = (int)(DateTime.Now - this._lastHeartbeat).TotalMilliseconds; this.Logger.LogTrace(LoggerEvents.WebSocketReceive, "Received HEARTBEAT_ACK (OP11, {0}ms)", ping); Volatile.Write(ref this._ping, ping); var args = new HeartbeatEventArgs(this.ServiceProvider) { Ping = this.Ping, Timestamp = DateTimeOffset.Now }; await this._heartbeated.InvokeAsync(this, args).ConfigureAwait(false); } /// /// Heartbeats the loop async. /// /// A Task. internal async Task HeartbeatLoopAsync() { this.Logger.LogDebug(LoggerEvents.Heartbeat, "Heartbeat task started"); var token = this._cancelToken; try { while (true) { await this.SendHeartbeatAsync(this._lastSequence).ConfigureAwait(false); await Task.Delay(this._heartbeatInterval, token).ConfigureAwait(false); token.ThrowIfCancellationRequested(); } } catch (OperationCanceledException) { } } #endregion #region Internal Gateway Methods /// /// Internals the update status async. /// /// The activity. /// The user status. /// The idle since. /// A Task. internal async Task InternalUpdateStatusAsync(DiscordActivity activity, UserStatus? userStatus, DateTimeOffset? idleSince) { if (activity != null && activity.Name != null && activity.Name.Length > 128) throw new Exception("Game name can't be longer than 128 characters!"); var sinceUnix = idleSince != null ? (long?)Utilities.GetUnixTime(idleSince.Value) : null; var act = activity ?? new DiscordActivity(); var status = new StatusUpdate { Activity = new TransportActivity(act), IdleSince = sinceUnix, IsAfk = idleSince != null, Status = userStatus ?? UserStatus.Online }; // Solution to have status persist between sessions this._status = status; var statusUpdate = new GatewayPayload { OpCode = GatewayOpCode.StatusUpdate, Data = status }; var statusstr = JsonConvert.SerializeObject(statusUpdate); await this.WsSendAsync(statusstr).ConfigureAwait(false); if (!this.PresencesInternal.ContainsKey(this.CurrentUser.Id)) { this.PresencesInternal[this.CurrentUser.Id] = new DiscordPresence { Discord = this, Activity = act, Status = userStatus ?? UserStatus.Online, InternalUser = new TransportUser { Id = this.CurrentUser.Id } }; } else { var pr = this.PresencesInternal[this.CurrentUser.Id]; pr.Activity = act; pr.Status = userStatus ?? pr.Status; } } /// /// Sends the heartbeat async. /// /// The seq. /// A Task. internal async Task SendHeartbeatAsync(long seq) { var moreThan5 = Volatile.Read(ref this._skippedHeartbeats) > 5; var guildsComp = Volatile.Read(ref this._guildDownloadCompleted); if (guildsComp && moreThan5) { this.Logger.LogCritical(LoggerEvents.HeartbeatFailure, "Server failed to acknowledge more than 5 heartbeats - connection is zombie"); var args = new ZombiedEventArgs(this.ServiceProvider) { Failures = Volatile.Read(ref this._skippedHeartbeats), GuildDownloadCompleted = true }; await this._zombied.InvokeAsync(this, args).ConfigureAwait(false); await this.InternalReconnectAsync(code: 4001, message: "Too many heartbeats missed").ConfigureAwait(false); return; } else if (!guildsComp && moreThan5) { var args = new ZombiedEventArgs(this.ServiceProvider) { Failures = Volatile.Read(ref this._skippedHeartbeats), GuildDownloadCompleted = false }; await this._zombied.InvokeAsync(this, args).ConfigureAwait(false); this.Logger.LogWarning(LoggerEvents.HeartbeatFailure, "Server failed to acknowledge more than 5 heartbeats, but the guild download is still running - check your connection speed"); } Volatile.Write(ref this._lastSequence, seq); this.Logger.LogTrace(LoggerEvents.Heartbeat, "Sending heartbeat"); var heartbeat = new GatewayPayload { OpCode = GatewayOpCode.Heartbeat, Data = seq }; var heartbeatStr = JsonConvert.SerializeObject(heartbeat); await this.WsSendAsync(heartbeatStr).ConfigureAwait(false); this._lastHeartbeat = DateTimeOffset.Now; Interlocked.Increment(ref this._skippedHeartbeats); } /// /// Sends the identify async. /// /// The status. /// A Task. internal async Task SendIdentifyAsync(StatusUpdate status) { var identify = new GatewayIdentify { Token = Utilities.GetFormattedToken(this), Compress = this.Configuration.GatewayCompressionLevel == GatewayCompressionLevel.Payload, LargeThreshold = this.Configuration.LargeThreshold, ShardInfo = new ShardInfo { ShardId = this.Configuration.ShardId, ShardCount = this.Configuration.ShardCount }, Presence = status, Intents = this.Configuration.Intents, Discord = this }; var payload = new GatewayPayload { OpCode = GatewayOpCode.Identify, Data = identify }; var payloadstr = JsonConvert.SerializeObject(payload); await this.WsSendAsync(payloadstr).ConfigureAwait(false); this.Logger.LogDebug(LoggerEvents.Intents, "Registered gateway intents ({0})", this.Configuration.Intents); } /// /// Sends the resume async. /// /// A Task. internal async Task SendResumeAsync() { var resume = new GatewayResume { Token = Utilities.GetFormattedToken(this), SessionId = this._sessionId, SequenceNumber = Volatile.Read(ref this._lastSequence) }; var resumePayload = new GatewayPayload { OpCode = GatewayOpCode.Resume, Data = resume }; var resumestr = JsonConvert.SerializeObject(resumePayload); await this.WsSendAsync(resumestr).ConfigureAwait(false); } /// /// Internals the update gateway async. /// /// A Task. internal async Task InternalUpdateGatewayAsync() { var info = await this.GetGatewayInfoAsync().ConfigureAwait(false); this.GatewayInfo = info; this.GatewayUri = new Uri(info.Url); } /// /// Ws the send async. /// /// The payload. /// A Task. internal async Task WsSendAsync(string payload) { this.Logger.LogTrace(LoggerEvents.GatewayWsTx, payload); await this.WebSocketClient.SendMessageAsync(payload).ConfigureAwait(false); } #endregion #region Semaphore Methods /// /// Gets the socket lock. /// /// A SocketLock. private SocketLock GetSocketLock() => s_socketLocks.GetOrAdd(this.CurrentApplication.Id, appId => new SocketLock(appId, this.GatewayInfo.SessionBucket.MaxConcurrency)); #endregion } } diff --git a/DisCatSharp/Clients/DiscordClient.cs b/DisCatSharp/Clients/DiscordClient.cs index b2e8c0e75..81c7927e7 100644 --- a/DisCatSharp/Clients/DiscordClient.cs +++ b/DisCatSharp/Clients/DiscordClient.cs @@ -1,1316 +1,1316 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Globalization; using System.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 = null; + 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 WS 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.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. /// /// 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. /// /// 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. /// /// If true, start 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 false). /// 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. /// /// Channel to send to. /// 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. /// /// Channel to send to. /// 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. /// Message content to send. /// 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 . /// /// 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 . /// /// 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 = Optional.FromNoValue(); if (icon.HasValue && icon.Value != null) using (var imgtool = new ImageTool(icon.Value)) iconb64 = imgtool.GetBase64(); else if (icon.HasValue) iconb64 = null; 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 = Optional.FromNoValue(); if (icon.HasValue && icon.Value != null) using (var imgtool = new ImageTool(icon.Value)) iconb64 = imgtool.GetBase64(); else if (icon.HasValue) iconb64 = null; 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 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 connections. /// /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task> GetConnectionsAsync() => this.ApiClient.GetUsersConnectionsAsync(); /// /// 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 = Optional.FromNoValue(); if (avatar.HasValue && avatar.Value != null) using (var imgtool = new ImageTool(avatar.Value)) av64 = imgtool.GetBase64(); else if (avatar.HasValue) av64 = null; 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. /// /// A list of global application commands. public Task> GetGlobalApplicationCommandsAsync() => this.ApiClient.GetGlobalApplicationCommandsAsync(this.CurrentApplication.Id); /// /// 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.DefaultPermission, mdl.NameLocalizations, mdl.DescriptionLocalizations).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. /// A list of application commands in the guild. public Task> GetGuildApplicationCommandsAsync(ulong guildId) => this.ApiClient.GetGuildApplicationCommandsAsync(this.CurrentApplication.Id, guildId); /// /// 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.DefaultPermission, mdl.NameLocalizations, mdl.DescriptionLocalizations).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.GetApplicationCommandPermissionAsync(this.CurrentApplication.Id, guildId, commandId); /// /// Overwrites the existing permissions for a application command in a guild. New permissions are automatically created and missing permissions are deleted. /// A command takes up to 10 permission overwrites. /// /// The id of the guild. /// The id of the command. /// List of permissions. public Task OverwriteGuildApplicationCommandPermissionsAsync(ulong guildId, ulong commandId, IEnumerable permissions) => this.ApiClient.OverwriteGuildApplicationCommandPermissionsAsync(this.CurrentApplication.Id, guildId, commandId, permissions); /// /// Overwrites the existing application command permissions in a guild. New permissions are automatically created and missing permissions are deleted. /// Each command takes up to 10 permission overwrites. /// /// The id of the guild. /// The list of permissions to overwrite with. public Task> BulkOverwriteGuildApplicationCommandsAsync(ulong guildId, IEnumerable permissionsOverwrites) => this.ApiClient.BulkOverwriteApplicationCommandPermissionsAsync(this.CurrentApplication.Id, guildId, permissionsOverwrites); #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 chached 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 chached 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 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.Count > 0) { 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.Count > 0) { 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.Count > 0) { 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(); } 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/Clients/DiscordShardedClient.cs b/DisCatSharp/Clients/DiscordShardedClient.cs index 2849e699f..e46488e3d 100644 --- a/DisCatSharp/Clients/DiscordShardedClient.cs +++ b/DisCatSharp/Clients/DiscordShardedClient.cs @@ -1,745 +1,747 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. #pragma warning disable CS0618 using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.Globalization; using System.Linq; using System.Net.Http; using System.Reflection; using System.Threading.Tasks; using DisCatSharp.Common.Utilities; using DisCatSharp.Entities; using DisCatSharp.EventArgs; using DisCatSharp.Net; using Microsoft.Extensions.Logging; using Newtonsoft.Json.Linq; namespace DisCatSharp { /// /// A Discord client that shards automatically. /// public sealed partial class DiscordShardedClient { #region Public Properties /// /// Gets the logger for this client. /// public ILogger Logger { get; } /// /// Gets all client shards. /// public IReadOnlyDictionary ShardClients { get; } /// /// Gets the gateway info for the client's session. /// public GatewayInfo GatewayInfo { get; private set; } /// /// Gets the current user. /// public DiscordUser CurrentUser { get; private set; } /// /// Gets the current application. /// public DiscordApplication CurrentApplication { get; private set; } /// /// Gets the library team. /// public DisCatSharpTeam LibraryDeveloperTeam => this.GetShard(0).LibraryDeveloperTeam; /// /// Gets the list of available voice regions. Note that this property will not contain VIP voice regions. /// public IReadOnlyDictionary VoiceRegions => this._voiceRegionsLazy?.Value; #endregion #region Private Properties/Fields /// /// Gets the configuration. /// private readonly DiscordConfiguration _configuration; /// /// Gets the list of available voice regions. This property is meant as a way to modify . /// private ConcurrentDictionary _internalVoiceRegions; private readonly ConcurrentDictionary _shards = new(); private Lazy> _voiceRegionsLazy; private bool _isStarted; private readonly bool _manuallySharding; #endregion #region Constructor /// /// Initializes new auto-sharding Discord client. /// /// Configuration to use. public DiscordShardedClient(DiscordConfiguration config) { this.InternalSetup(); if (config.ShardCount > 1) this._manuallySharding = true; this._configuration = config; this.ShardClients = new ReadOnlyConcurrentDictionary(this._shards); if (this._configuration.LoggerFactory == null) { this._configuration.LoggerFactory = new DefaultLoggerFactory(); this._configuration.LoggerFactory.AddProvider(new DefaultLoggerProvider(this._configuration.MinimumLogLevel, this._configuration.LogTimestampFormat)); } this.Logger = this._configuration.LoggerFactory.CreateLogger(); } #endregion #region Public Methods /// /// Initializes and connects all shards. /// /// /// /// public async Task StartAsync() { if (this._isStarted) throw new InvalidOperationException("This client has already been started."); this._isStarted = true; try { 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.Value); var shardc = await this.InitializeShardsAsync().ConfigureAwait(false); var connectTasks = new List(); this.Logger.LogInformation(LoggerEvents.ShardStartup, "Booting {0} shards.", shardc); for (var i = 0; i < shardc; i++) { //This should never happen, but in case it does... if (this.GatewayInfo.SessionBucket.MaxConcurrency < 1) this.GatewayInfo.SessionBucket.MaxConcurrency = 1; if (this.GatewayInfo.SessionBucket.MaxConcurrency == 1) await this.ConnectShardAsync(i).ConfigureAwait(false); else { //Concurrent login. connectTasks.Add(this.ConnectShardAsync(i)); if (connectTasks.Count == this.GatewayInfo.SessionBucket.MaxConcurrency) { await Task.WhenAll(connectTasks).ConfigureAwait(false); connectTasks.Clear(); } } } } catch (Exception ex) { await this.InternalStopAsync(false).ConfigureAwait(false); var message = $"Shard initialization failed, check inner exceptions for details: "; this.Logger.LogCritical(LoggerEvents.ShardClientError, $"{message}\n{ex}"); throw new AggregateException(message, ex); } } /// /// Disconnects and disposes of all shards. /// /// /// public Task StopAsync() => this.InternalStopAsync(); /// /// Gets a shard from a guild ID. /// /// If automatically sharding, this will use the method. /// Otherwise if manually sharding, it will instead iterate through each shard's guild caches. /// /// /// The guild ID for the shard. /// The found shard. Otherwise if the shard was not found for the guild ID. public DiscordClient GetShard(ulong guildId) { var index = this._manuallySharding ? this.GetShardIdFromGuilds(guildId) : Utilities.GetShardId(guildId, this.ShardClients.Count); return index != -1 ? this._shards[index] : null; } /// /// Gets a shard from a guild. /// /// If automatically sharding, this will use the method. /// Otherwise if manually sharding, it will instead iterate through each shard's guild caches. /// /// /// The guild for the shard. /// The found shard. Otherwise if the shard was not found for the guild. public DiscordClient GetShard(DiscordGuild guild) => this.GetShard(guild.Id); /// /// Updates playing statuses on all shards. /// /// Activity to set. /// Status of the user. /// Since when is the client performing the specified activity. /// Asynchronous operation. public async Task UpdateStatusAsync(DiscordActivity activity = null, UserStatus? userStatus = null, DateTimeOffset? idleSince = null) { var tasks = new List(); foreach (var client in this._shards.Values) tasks.Add(client.UpdateStatusAsync(activity, userStatus, idleSince)); await Task.WhenAll(tasks).ConfigureAwait(false); } #endregion #region Internal Methods /// /// Initializes the shards async. /// /// A Task. internal async Task InitializeShardsAsync() { if (this._shards.Count != 0) return this._shards.Count; this.GatewayInfo = await this.GetGatewayInfoAsync().ConfigureAwait(false); var shardc = this._configuration.ShardCount == 1 ? this.GatewayInfo.ShardCount : this._configuration.ShardCount; var lf = new ShardedLoggerFactory(this.Logger); for (var i = 0; i < shardc; i++) { var cfg = new DiscordConfiguration(this._configuration) { ShardId = i, ShardCount = shardc, LoggerFactory = lf }; var client = new DiscordClient(cfg); if (!this._shards.TryAdd(i, client)) throw new InvalidOperationException("Could not initialize shards."); } return shardc; } #endregion #region Private Methods/Version Property /// /// Gets the gateway info async. /// /// A Task. private async Task GetGatewayInfoAsync() { var url = $"{Utilities.GetApiBaseUri(this._configuration)}{Endpoints.GATEWAY}{Endpoints.BOT}"; var http = new HttpClient(); http.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", Utilities.GetUserAgent()); http.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", Utilities.GetFormattedToken(this._configuration)); if (this._configuration != null && this._configuration.Override != null) { http.DefaultRequestHeaders.TryAddWithoutValidation("x-super-properties", this._configuration.Override); } this.Logger.LogDebug(LoggerEvents.ShardRest, $"Obtaining gateway information from GET {Endpoints.GATEWAY}{Endpoints.BOT}..."); var resp = await http.GetAsync(url).ConfigureAwait(false); http.Dispose(); if (!resp.IsSuccessStatusCode) { var ratelimited = await HandleHttpError(url, resp).ConfigureAwait(false); if (ratelimited) return await this.GetGatewayInfoAsync().ConfigureAwait(false); } var timer = new Stopwatch(); timer.Start(); var jo = JObject.Parse(await resp.Content.ReadAsStringAsync().ConfigureAwait(false)); var info = jo.ToObject(); //There is a delay from parsing here. timer.Stop(); info.SessionBucket.ResetAfterInternal -= (int)timer.ElapsedMilliseconds; info.SessionBucket.ResetAfter = DateTimeOffset.UtcNow + TimeSpan.FromMilliseconds(info.SessionBucket.ResetAfterInternal); return info; async Task HandleHttpError(string reqUrl, HttpResponseMessage msg) { var code = (int)msg.StatusCode; if (code == 401 || code == 403) { throw new Exception($"Authentication failed, check your token and try again: {code} {msg.ReasonPhrase}"); } else if (code == 429) { this.Logger.LogError(LoggerEvents.ShardClientError, $"Ratelimit hit, requeuing request to {reqUrl}"); var hs = msg.Headers.ToDictionary(xh => xh.Key, xh => string.Join("\n", xh.Value), StringComparer.OrdinalIgnoreCase); var waitInterval = 0; if (hs.TryGetValue("Retry-After", out var retryAfterRaw)) waitInterval = int.Parse(retryAfterRaw, CultureInfo.InvariantCulture); await Task.Delay(waitInterval).ConfigureAwait(false); return true; } else if (code >= 500) { throw new Exception($"Internal Server Error: {code} {msg.ReasonPhrase}"); } else { throw new Exception($"An unsuccessful HTTP status code was encountered: {code} {msg.ReasonPhrase}"); } } } private readonly Lazy _versionString = new(() => { var a = typeof(DiscordShardedClient).GetTypeInfo().Assembly; var iv = a.GetCustomAttribute(); if (iv != null) return iv.InformationalVersion; var v = a.GetName().Version; var vs = v.ToString(3); if (v.Revision > 0) vs = $"{vs}, CI build {v.Revision}"; return vs; }); private readonly string _botLibrary = "DisCatSharp"; #endregion #region Private Connection Methods /// /// Connects the shard async. /// /// The i. /// A Task. private async Task ConnectShardAsync(int i) { if (!this._shards.TryGetValue(i, out var client)) throw new Exception($"Could not initialize shard {i}."); if (this.GatewayInfo != null) { client.GatewayInfo = this.GatewayInfo; client.GatewayUri = new Uri(client.GatewayInfo.Url); } if (this.CurrentUser != null) client.CurrentUser = this.CurrentUser; if (this.CurrentApplication != null) client.CurrentApplication = this.CurrentApplication; if (this._internalVoiceRegions != null) { client.InternalVoiceRegions = this._internalVoiceRegions; client.VoiceRegionsLazy = new Lazy>(() => new ReadOnlyDictionary(client.InternalVoiceRegions)); } this.HookEventHandlers(client); client.IsShard = true; await client.ConnectAsync().ConfigureAwait(false); this.Logger.LogInformation(LoggerEvents.ShardStartup, "Booted shard {0}.", i); if (this.CurrentUser == null) this.CurrentUser = client.CurrentUser; if (this.CurrentApplication == null) this.CurrentApplication = client.CurrentApplication; if (this._internalVoiceRegions == null) { this._internalVoiceRegions = client.InternalVoiceRegions; this._voiceRegionsLazy = new Lazy>(() => new ReadOnlyDictionary(this._internalVoiceRegions)); } } /// /// Internals the stop async. /// /// If true, enable logger. /// A Task. private Task InternalStopAsync(bool enableLogger = true) { if (!this._isStarted) throw new InvalidOperationException("This client has not been started."); if (enableLogger) this.Logger.LogInformation(LoggerEvents.ShardShutdown, "Disposing {0} shards.", this._shards.Count); this._isStarted = false; this._voiceRegionsLazy = null; this.GatewayInfo = null; this.CurrentUser = null; this.CurrentApplication = null; for (var i = 0; i < this._shards.Count; i++) { if (this._shards.TryGetValue(i, out var client)) { this.UnhookEventHandlers(client); client.Dispose(); if (enableLogger) this.Logger.LogInformation(LoggerEvents.ShardShutdown, "Disconnected shard {0}.", i); } } this._shards.Clear(); return Task.CompletedTask; } #endregion #region Event Handler Initialization/Registering /// /// Internals the setup. /// private void InternalSetup() { this._clientErrored = new AsyncEvent("CLIENT_ERRORED", DiscordClient.EventExecutionLimit, this.Goof); this._socketErrored = new AsyncEvent("SOCKET_ERRORED", DiscordClient.EventExecutionLimit, this.Goof); this._socketOpened = new AsyncEvent("SOCKET_OPENED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._socketClosed = new AsyncEvent("SOCKET_CLOSED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._ready = new AsyncEvent("READY", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._resumed = new AsyncEvent("RESUMED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._channelCreated = new AsyncEvent("CHANNEL_CREATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._channelUpdated = new AsyncEvent("CHANNEL_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._channelDeleted = new AsyncEvent("CHANNEL_DELETED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._dmChannelDeleted = new AsyncEvent("DM_CHANNEL_DELETED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._channelPinsUpdated = new AsyncEvent("CHANNEL_PINS_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildCreated = new AsyncEvent("GUILD_CREATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildAvailable = new AsyncEvent("GUILD_AVAILABLE", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildUpdated = new AsyncEvent("GUILD_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildDeleted = new AsyncEvent("GUILD_DELETED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildUnavailable = new AsyncEvent("GUILD_UNAVAILABLE", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildDownloadCompleted = new AsyncEvent("GUILD_DOWNLOAD_COMPLETED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._inviteCreated = new AsyncEvent("INVITE_CREATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._inviteDeleted = new AsyncEvent("INVITE_DELETED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._messageCreated = new AsyncEvent("MESSAGE_CREATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._presenceUpdated = new AsyncEvent("PRESENCE_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildBanAdded = new AsyncEvent("GUILD_BAN_ADDED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildBanRemoved = new AsyncEvent("GUILD_BAN_REMOVED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildEmojisUpdated = new AsyncEvent("GUILD_EMOJI_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildStickersUpdated = new AsyncEvent("GUILD_STICKER_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildIntegrationsUpdated = new AsyncEvent("GUILD_INTEGRATIONS_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildMemberAdded = new AsyncEvent("GUILD_MEMBER_ADDED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildMemberRemoved = new AsyncEvent("GUILD_MEMBER_REMOVED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildMemberUpdated = new AsyncEvent("GUILD_MEMBER_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildRoleCreated = new AsyncEvent("GUILD_ROLE_CREATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildRoleUpdated = new AsyncEvent("GUILD_ROLE_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildRoleDeleted = new AsyncEvent("GUILD_ROLE_DELETED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._messageUpdated = new AsyncEvent("MESSAGE_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._messageDeleted = new AsyncEvent("MESSAGE_DELETED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._messageBulkDeleted = new AsyncEvent("MESSAGE_BULK_DELETED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._interactionCreated = new AsyncEvent("INTERACTION_CREATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._componentInteractionCreated = new AsyncEvent("COMPONENT_INTERACTED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._contextMenuInteractionCreated = new AsyncEvent("CONTEXT_MENU_INTERACTED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._typingStarted = new AsyncEvent("TYPING_STARTED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._userSettingsUpdated = new AsyncEvent("USER_SETTINGS_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._userUpdated = new AsyncEvent("USER_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._voiceStateUpdated = new AsyncEvent("VOICE_STATE_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._voiceServerUpdated = new AsyncEvent("VOICE_SERVER_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildMembersChunk = new AsyncEvent("GUILD_MEMBERS_CHUNKED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._unknownEvent = new AsyncEvent("UNKNOWN_EVENT", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._messageReactionAdded = new AsyncEvent("MESSAGE_REACTION_ADDED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._messageReactionRemoved = new AsyncEvent("MESSAGE_REACTION_REMOVED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._messageReactionsCleared = new AsyncEvent("MESSAGE_REACTIONS_CLEARED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._messageReactionRemovedEmoji = new AsyncEvent("MESSAGE_REACTION_REMOVED_EMOJI", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._webhooksUpdated = new AsyncEvent("WEBHOOKS_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._heartbeated = new AsyncEvent("HEARTBEATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._applicationCommandCreated = new AsyncEvent("APPLICATION_COMMAND_CREATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._applicationCommandUpdated = new AsyncEvent("APPLICATION_COMMAND_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._applicationCommandDeleted = new AsyncEvent("APPLICATION_COMMAND_DELETED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildApplicationCommandCountUpdated = new AsyncEvent("GUILD_APPLICATION_COMMAND_COUNTS_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._applicationCommandPermissionsUpdated = new AsyncEvent("APPLICATION_COMMAND_PERMISSIONS_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildIntegrationCreated = new AsyncEvent("INTEGRATION_CREATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildIntegrationUpdated = new AsyncEvent("INTEGRATION_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildIntegrationDeleted = new AsyncEvent("INTEGRATION_DELETED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._stageInstanceCreated = new AsyncEvent("STAGE_INSTANCE_CREATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._stageInstanceUpdated = new AsyncEvent("STAGE_INSTANCE_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._stageInstanceDeleted = new AsyncEvent("STAGE_INSTANCE_DELETED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._threadCreated = new AsyncEvent("THREAD_CREATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._threadUpdated = new AsyncEvent("THREAD_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._threadDeleted = new AsyncEvent("THREAD_DELETED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._threadListSynced = new AsyncEvent("THREAD_LIST_SYNCED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._threadMemberUpdated = new AsyncEvent("THREAD_MEMBER_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._threadMembersUpdated = new AsyncEvent("THREAD_MEMBERS_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._zombied = new AsyncEvent("ZOMBIED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._payloadReceived = new AsyncEvent("PAYLOAD_RECEIVED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildScheduledEventCreated = new AsyncEvent("GUILD_SCHEDULED_EVENT_CREATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildScheduledEventUpdated = new AsyncEvent("GUILD_SCHEDULED_EVENT_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildScheduledEventDeleted = new AsyncEvent("GUILD_SCHEDULED_EVENT_DELETED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildScheduledEventUserAdded = new AsyncEvent("GUILD_SCHEDULED_EVENT_USER_ADDED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._guildScheduledEventUserRemoved = new AsyncEvent("GUILD_SCHEDULED_EVENT_USER_REMOVED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); this._embeddedActivityUpdated = new AsyncEvent("EMBEDDED_ACTIVITY_UPDATED", DiscordClient.EventExecutionLimit, this.EventErrorHandler); } /// /// Hooks the event handlers. /// /// The client. private void HookEventHandlers(DiscordClient client) { client.ClientErrored += this.Client_ClientError; client.SocketErrored += this.Client_SocketError; client.SocketOpened += this.Client_SocketOpened; client.SocketClosed += this.Client_SocketClosed; client.Ready += this.Client_Ready; client.Resumed += this.Client_Resumed; client.ChannelCreated += this.Client_ChannelCreated; client.ChannelUpdated += this.Client_ChannelUpdated; client.ChannelDeleted += this.Client_ChannelDeleted; client.DmChannelDeleted += this.Client_DMChannelDeleted; client.ChannelPinsUpdated += this.Client_ChannelPinsUpdated; client.GuildCreated += this.Client_GuildCreated; client.GuildAvailable += this.Client_GuildAvailable; client.GuildUpdated += this.Client_GuildUpdated; client.GuildDeleted += this.Client_GuildDeleted; client.GuildUnavailable += this.Client_GuildUnavailable; client.GuildDownloadCompleted += this.Client_GuildDownloadCompleted; client.InviteCreated += this.Client_InviteCreated; client.InviteDeleted += this.Client_InviteDeleted; client.MessageCreated += this.Client_MessageCreated; client.PresenceUpdated += this.Client_PresenceUpdate; client.GuildBanAdded += this.Client_GuildBanAdd; client.GuildBanRemoved += this.Client_GuildBanRemove; client.GuildEmojisUpdated += this.Client_GuildEmojisUpdate; client.GuildStickersUpdated += this.Client_GuildStickersUpdate; client.GuildIntegrationsUpdated += this.Client_GuildIntegrationsUpdate; client.GuildMemberAdded += this.Client_GuildMemberAdd; client.GuildMemberRemoved += this.Client_GuildMemberRemove; client.GuildMemberUpdated += this.Client_GuildMemberUpdate; client.GuildRoleCreated += this.Client_GuildRoleCreate; client.GuildRoleUpdated += this.Client_GuildRoleUpdate; client.GuildRoleDeleted += this.Client_GuildRoleDelete; client.MessageUpdated += this.Client_MessageUpdate; client.MessageDeleted += this.Client_MessageDelete; client.MessagesBulkDeleted += this.Client_MessageBulkDelete; client.InteractionCreated += this.Client_InteractionCreate; client.ComponentInteractionCreated += this.Client_ComponentInteractionCreate; client.ContextMenuInteractionCreated += this.Client_ContextMenuInteractionCreate; client.TypingStarted += this.Client_TypingStart; client.UserSettingsUpdated += this.Client_UserSettingsUpdate; client.UserUpdated += this.Client_UserUpdate; client.VoiceStateUpdated += this.Client_VoiceStateUpdate; client.VoiceServerUpdated += this.Client_VoiceServerUpdate; client.GuildMembersChunked += this.Client_GuildMembersChunk; client.UnknownEvent += this.Client_UnknownEvent; client.MessageReactionAdded += this.Client_MessageReactionAdd; client.MessageReactionRemoved += this.Client_MessageReactionRemove; client.MessageReactionsCleared += this.Client_MessageReactionRemoveAll; client.MessageReactionRemovedEmoji += this.Client_MessageReactionRemovedEmoji; client.WebhooksUpdated += this.Client_WebhooksUpdate; client.Heartbeated += this.Client_HeartBeated; client.ApplicationCommandCreated += this.Client_ApplicationCommandCreated; client.ApplicationCommandUpdated += this.Client_ApplicationCommandUpdated; client.ApplicationCommandDeleted += this.Client_ApplicationCommandDeleted; client.GuildApplicationCommandCountUpdated += this.Client_GuildApplicationCommandCountUpdated; client.ApplicationCommandPermissionsUpdated += this.Client_ApplicationCommandPermissionsUpdated; client.GuildIntegrationCreated += this.Client_GuildIntegrationCreated; client.GuildIntegrationUpdated += this.Client_GuildIntegrationUpdated; client.GuildIntegrationDeleted += this.Client_GuildIntegrationDeleted; client.StageInstanceCreated += this.Client_StageInstanceCreated; client.StageInstanceUpdated += this.Client_StageInstanceUpdated; client.StageInstanceDeleted += this.Client_StageInstanceDeleted; client.ThreadCreated += this.Client_ThreadCreated; client.ThreadUpdated += this.Client_ThreadUpdated; client.ThreadDeleted += this.Client_ThreadDeleted; client.ThreadListSynced += this.Client_ThreadListSynced; client.ThreadMemberUpdated += this.Client_ThreadMemberUpdated; client.ThreadMembersUpdated += this.Client_ThreadMembersUpdated; client.Zombied += this.Client_Zombied; client.PayloadReceived += this.Client_PayloadReceived; client.GuildScheduledEventCreated += this.Client_GuildScheduledEventCreated; client.GuildScheduledEventUpdated += this.Client_GuildScheduledEventUpdated; client.GuildScheduledEventDeleted += this.Client_GuildScheduledEventDeleted; client.GuildScheduledEventUserAdded += this.Client_GuildScheduledEventUserAdded; ; client.GuildScheduledEventUserRemoved += this.Client_GuildScheduledEventUserRemoved; client.EmbeddedActivityUpdated += this.Client_EmbeddedActivityUpdated; } /// /// Unhooks the event handlers. /// /// The client. private void UnhookEventHandlers(DiscordClient client) { client.ClientErrored -= this.Client_ClientError; client.SocketErrored -= this.Client_SocketError; client.SocketOpened -= this.Client_SocketOpened; client.SocketClosed -= this.Client_SocketClosed; client.Ready -= this.Client_Ready; client.Resumed -= this.Client_Resumed; client.ChannelCreated -= this.Client_ChannelCreated; client.ChannelUpdated -= this.Client_ChannelUpdated; client.ChannelDeleted -= this.Client_ChannelDeleted; client.DmChannelDeleted -= this.Client_DMChannelDeleted; client.ChannelPinsUpdated -= this.Client_ChannelPinsUpdated; client.GuildCreated -= this.Client_GuildCreated; client.GuildAvailable -= this.Client_GuildAvailable; client.GuildUpdated -= this.Client_GuildUpdated; client.GuildDeleted -= this.Client_GuildDeleted; client.GuildUnavailable -= this.Client_GuildUnavailable; client.GuildDownloadCompleted -= this.Client_GuildDownloadCompleted; client.InviteCreated -= this.Client_InviteCreated; client.InviteDeleted -= this.Client_InviteDeleted; client.MessageCreated -= this.Client_MessageCreated; client.PresenceUpdated -= this.Client_PresenceUpdate; client.GuildBanAdded -= this.Client_GuildBanAdd; client.GuildBanRemoved -= this.Client_GuildBanRemove; client.GuildEmojisUpdated -= this.Client_GuildEmojisUpdate; client.GuildStickersUpdated -= this.Client_GuildStickersUpdate; client.GuildIntegrationsUpdated -= this.Client_GuildIntegrationsUpdate; client.GuildMemberAdded -= this.Client_GuildMemberAdd; client.GuildMemberRemoved -= this.Client_GuildMemberRemove; client.GuildMemberUpdated -= this.Client_GuildMemberUpdate; client.GuildRoleCreated -= this.Client_GuildRoleCreate; client.GuildRoleUpdated -= this.Client_GuildRoleUpdate; client.GuildRoleDeleted -= this.Client_GuildRoleDelete; client.MessageUpdated -= this.Client_MessageUpdate; client.MessageDeleted -= this.Client_MessageDelete; client.MessagesBulkDeleted -= this.Client_MessageBulkDelete; client.InteractionCreated -= this.Client_InteractionCreate; client.ComponentInteractionCreated -= this.Client_ComponentInteractionCreate; client.ContextMenuInteractionCreated -= this.Client_ContextMenuInteractionCreate; client.TypingStarted -= this.Client_TypingStart; client.UserSettingsUpdated -= this.Client_UserSettingsUpdate; client.UserUpdated -= this.Client_UserUpdate; client.VoiceStateUpdated -= this.Client_VoiceStateUpdate; client.VoiceServerUpdated -= this.Client_VoiceServerUpdate; client.GuildMembersChunked -= this.Client_GuildMembersChunk; client.UnknownEvent -= this.Client_UnknownEvent; client.MessageReactionAdded -= this.Client_MessageReactionAdd; client.MessageReactionRemoved -= this.Client_MessageReactionRemove; client.MessageReactionsCleared -= this.Client_MessageReactionRemoveAll; client.MessageReactionRemovedEmoji -= this.Client_MessageReactionRemovedEmoji; client.WebhooksUpdated -= this.Client_WebhooksUpdate; client.Heartbeated -= this.Client_HeartBeated; client.ApplicationCommandCreated -= this.Client_ApplicationCommandCreated; client.ApplicationCommandUpdated -= this.Client_ApplicationCommandUpdated; client.ApplicationCommandDeleted -= this.Client_ApplicationCommandDeleted; client.GuildApplicationCommandCountUpdated -= this.Client_GuildApplicationCommandCountUpdated; client.ApplicationCommandPermissionsUpdated -= this.Client_ApplicationCommandPermissionsUpdated; client.GuildIntegrationCreated -= this.Client_GuildIntegrationCreated; client.GuildIntegrationUpdated -= this.Client_GuildIntegrationUpdated; client.GuildIntegrationDeleted -= this.Client_GuildIntegrationDeleted; client.StageInstanceCreated -= this.Client_StageInstanceCreated; client.StageInstanceUpdated -= this.Client_StageInstanceUpdated; client.StageInstanceDeleted -= this.Client_StageInstanceDeleted; client.ThreadCreated -= this.Client_ThreadCreated; client.ThreadUpdated -= this.Client_ThreadUpdated; client.ThreadDeleted -= this.Client_ThreadDeleted; client.ThreadListSynced -= this.Client_ThreadListSynced; client.ThreadMemberUpdated -= this.Client_ThreadMemberUpdated; client.ThreadMembersUpdated -= this.Client_ThreadMembersUpdated; client.Zombied -= this.Client_Zombied; client.PayloadReceived -= this.Client_PayloadReceived; client.GuildScheduledEventCreated -= this.Client_GuildScheduledEventCreated; client.GuildScheduledEventUpdated -= this.Client_GuildScheduledEventUpdated; client.GuildScheduledEventDeleted -= this.Client_GuildScheduledEventDeleted; client.GuildScheduledEventUserAdded -= this.Client_GuildScheduledEventUserAdded; ; client.GuildScheduledEventUserRemoved -= this.Client_GuildScheduledEventUserRemoved; client.EmbeddedActivityUpdated -= this.Client_EmbeddedActivityUpdated; } /// /// Gets the shard id from guilds. /// /// The id. /// An int. private int GetShardIdFromGuilds(ulong id) { foreach (var s in this._shards.Values) { if (s.GuildsInternal.TryGetValue(id, out _)) { return s.ShardId; } } return -1; } #endregion #region Destructor ~DiscordShardedClient() - => this.InternalStopAsync(false).GetAwaiter().GetResult(); + { + this.InternalStopAsync(false).GetAwaiter().GetResult(); + } #endregion } } diff --git a/DisCatSharp/Clients/DiscordWebhookClient.cs b/DisCatSharp/Clients/DiscordWebhookClient.cs index 004d4651e..4069864de 100644 --- a/DisCatSharp/Clients/DiscordWebhookClient.cs +++ b/DisCatSharp/Clients/DiscordWebhookClient.cs @@ -1,283 +1,283 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Globalization; using System.Linq; using System.Net; using System.Text.RegularExpressions; using System.Threading.Tasks; using DisCatSharp.Entities; using DisCatSharp.Exceptions; using DisCatSharp.Net; using Microsoft.Extensions.Logging; namespace DisCatSharp { /// /// Represents a webhook-only client. This client can be used to execute Discord webhooks. /// public class DiscordWebhookClient { /// /// Gets the logger for this client. /// public ILogger Logger { get; } /// /// Gets the webhook regex. /// this regex has 2 named capture groups: "id" and "token". /// - private static Regex s_webhookRegex { get; } = new Regex(@"(?:https?:\/\/)?discord(?:app)?.com\/api\/(?:v\d\/)?webhooks\/(?\d+)\/(?[A-Za-z0-9_\-]+)", RegexOptions.ECMAScript); + private static Regex s_webhookRegex { get; } = new(@"(?:https?:\/\/)?discord(?:app)?.com\/api\/(?:v\d\/)?webhooks\/(?\d+)\/(?[A-Za-z0-9_\-]+)", RegexOptions.ECMAScript); /// /// Gets the collection of registered webhooks. /// public IReadOnlyList Webhooks { get; } /// /// Gets or sets the username override for registered webhooks. Note that this only takes effect when broadcasting. /// public string Username { get; set; } /// /// Gets or set the avatar override for registered webhooks. Note that this only takes effect when broadcasting. /// public string AvatarUrl { get; set; } internal List Hooks; internal DiscordApiClient Apiclient; internal LogLevel MinimumLogLevel; internal string LogTimestampFormat; /// /// Creates a new webhook client. /// public DiscordWebhookClient() : this(null, null) { } /// /// Creates a new webhook client, with specified HTTP proxy, timeout, and logging settings. /// /// Proxy to use for HTTP connections. /// Timeout to use for HTTP requests. Set to to disable timeouts. /// Whether to use the system clock for computing rate limit resets. See for more details. /// The optional logging factory to use for this client. /// The minimum logging level for messages. /// The timestamp format to use for the logger. public DiscordWebhookClient(IWebProxy proxy = null, TimeSpan? timeout = null, bool useRelativeRateLimit = true, ILoggerFactory loggerFactory = null, LogLevel minimumLogLevel = LogLevel.Information, string logTimestampFormat = "yyyy-MM-dd HH:mm:ss zzz") { this.MinimumLogLevel = minimumLogLevel; this.LogTimestampFormat = logTimestampFormat; if (loggerFactory == null) { loggerFactory = new DefaultLoggerFactory(); loggerFactory.AddProvider(new DefaultLoggerProvider(this)); } this.Logger = loggerFactory.CreateLogger(); var parsedTimeout = timeout ?? TimeSpan.FromSeconds(10); this.Apiclient = new DiscordApiClient(proxy, parsedTimeout, useRelativeRateLimit, this.Logger); this.Hooks = new List(); this.Webhooks = new ReadOnlyCollection(this.Hooks); } /// /// Registers a webhook with this client. This retrieves a webhook based on the ID and token supplied. /// /// The ID of the webhook to add. /// The token of the webhook to add. /// The registered webhook. public async Task AddWebhookAsync(ulong id, string token) { if (string.IsNullOrWhiteSpace(token)) throw new ArgumentNullException(nameof(token)); token = token.Trim(); if (this.Hooks.Any(x => x.Id == id)) throw new InvalidOperationException("This webhook is registered with this client."); var wh = await this.Apiclient.GetWebhookWithTokenAsync(id, token).ConfigureAwait(false); this.Hooks.Add(wh); return wh; } /// /// Registers a webhook with this client. This retrieves a webhook from webhook URL. /// /// URL of the webhook to retrieve. This URL must contain both ID and token. /// The registered webhook. public Task AddWebhookAsync(Uri url) { if (url == null) throw new ArgumentNullException(nameof(url)); var m = s_webhookRegex.Match(url.ToString()); if (!m.Success) throw new ArgumentException("Invalid webhook URL supplied.", nameof(url)); var idraw = m.Groups["id"]; var tokenraw = m.Groups["token"]; if (!ulong.TryParse(idraw.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var id)) throw new ArgumentException("Invalid webhook URL supplied.", nameof(url)); var token = tokenraw.Value; return this.AddWebhookAsync(id, token); } /// /// Registers a webhook with this client. This retrieves a webhook using the supplied full discord client. /// /// ID of the webhook to register. /// Discord client to which the webhook will belong. /// The registered webhook. public async Task AddWebhookAsync(ulong id, BaseDiscordClient client) { if (client == null) throw new ArgumentNullException(nameof(client)); if (this.Hooks.Any(x => x.Id == id)) throw new ArgumentException("This webhook is already registered with this client."); var wh = await client.ApiClient.GetWebhookAsync(id).ConfigureAwait(false); // personally I don't think we need to override anything. // it would even make sense to keep the hook as-is, in case // it's returned without a token for some bizarre reason // remember -- discord is not really consistent //var nwh = new DiscordWebhook() //{ // ApiClient = _apiclient, // AvatarHash = wh.AvatarHash, // ChannelId = wh.ChannelId, // GuildId = wh.GuildId, // Id = wh.Id, // Name = wh.Name, // Token = wh.Token, // User = wh.User, // Discord = null //}; this.Hooks.Add(wh); return wh; } /// /// Registers a webhook with this client. This reuses the supplied webhook object. /// /// Webhook to register. /// The registered webhook. public DiscordWebhook AddWebhook(DiscordWebhook webhook) { if (webhook == null) throw new ArgumentNullException(nameof(webhook)); if (this.Hooks.Any(x => x.Id == webhook.Id)) throw new ArgumentException("This webhook is already registered with this client."); // see line 128-131 for explanation // For christ's sake, update the line numbers if they change. //var nwh = new DiscordWebhook() //{ // ApiClient = _apiclient, // AvatarHash = webhook.AvatarHash, // ChannelId = webhook.ChannelId, // GuildId = webhook.GuildId, // Id = webhook.Id, // Name = webhook.Name, // Token = webhook.Token, // User = webhook.User, // Discord = null //}; this.Hooks.Add(webhook); return webhook; } /// /// Unregisters a webhook with this client. /// /// ID of the webhook to unregister. /// The unregistered webhook. public DiscordWebhook RemoveWebhook(ulong id) { if (!this.Hooks.Any(x => x.Id == id)) throw new ArgumentException("This webhook is not registered with this client."); var wh = this.GetRegisteredWebhook(id); this.Hooks.Remove(wh); return wh; } /// /// Gets a registered webhook with specified ID. /// /// ID of the registered webhook to retrieve. /// The requested webhook. public DiscordWebhook GetRegisteredWebhook(ulong id) => this.Hooks.FirstOrDefault(xw => xw.Id == id); /// /// Broadcasts a message to all registered webhooks. /// /// Webhook builder filled with data to send. /// public async Task> BroadcastMessageAsync(DiscordWebhookBuilder builder) { var deadhooks = new List(); var messages = new Dictionary(); foreach (var hook in this.Hooks) { try { messages.Add(hook, await hook.ExecuteAsync(builder).ConfigureAwait(false)); } catch (NotFoundException) { deadhooks.Add(hook); } } // Removing dead webhooks from collection foreach (var xwh in deadhooks) this.Hooks.Remove(xwh); return messages; } ~DiscordWebhookClient() { this.Hooks.Clear(); this.Hooks = null; this.Apiclient.Rest.Dispose(); } } } diff --git a/DisCatSharp/DiscordConfiguration.cs b/DisCatSharp/DiscordConfiguration.cs index 35016f651..12df820d2 100644 --- a/DisCatSharp/DiscordConfiguration.cs +++ b/DisCatSharp/DiscordConfiguration.cs @@ -1,275 +1,275 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Net; using DisCatSharp.Net.Udp; using DisCatSharp.Net.WebSocket; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace DisCatSharp { /// /// Represents configuration for and . /// public sealed class DiscordConfiguration { /// /// Sets the token used to identify the client. /// public string Token { internal get => this._token; set { if (string.IsNullOrWhiteSpace(value)) throw new ArgumentNullException(nameof(value), "Token cannot be null, empty, or all whitespace."); this._token = value.Trim(); } } private string _token = ""; /// /// Sets the type of the token used to identify the client. /// Defaults to . /// public TokenType TokenType { internal get; set; } = TokenType.Bot; /// /// Sets the minimum logging level for messages. /// Typically, the default value of is ok for most uses. /// public LogLevel MinimumLogLevel { internal get; set; } = LogLevel.Information; /// /// Overwrites the api version. /// Defaults to 9. /// public string ApiVersion { internal get; set; } = "9"; /// /// Sets whether to rely on Discord for NTP (Network Time Protocol) synchronization with the "X-Ratelimit-Reset-After" header. /// If the system clock is unsynced, setting this to true will ensure ratelimits are synced with Discord and reduce the risk of hitting one. /// This should only be set to false if the system clock is synced with NTP. /// Defaults to true. /// public bool UseRelativeRatelimit { internal get; set; } = true; /// /// Allows you to overwrite the time format used by the internal debug logger. /// Only applicable when is set left at default value. Defaults to ISO 8601-like format. /// public string LogTimestampFormat { internal get; set; } = "yyyy-MM-dd HH:mm:ss zzz"; /// /// Sets the member count threshold at which guilds are considered large. /// Defaults to 250. /// public int LargeThreshold { internal get; set; } = 250; /// /// Sets whether to automatically reconnect in case a connection is lost. /// Defaults to true. /// public bool AutoReconnect { internal get; set; } = true; /// /// Sets the ID of the shard to connect to. /// If not sharding, or sharding automatically, this value should be left with the default value of 0. /// - public int ShardId { internal get; set; } = 0; + public int ShardId { internal get; set; } /// /// Sets the total number of shards the bot is on. If not sharding, this value should be left with a default value of 1. /// If sharding automatically, this value will indicate how many shards to boot. If left default for automatic sharding, the client will determine the shard count automatically. /// public int ShardCount { internal get; set; } = 1; /// /// Sets the level of compression for WebSocket traffic. /// Disabling this option will increase the amount of traffic sent via WebSocket. Setting will enable compression for READY and GUILD_CREATE payloads. Setting will enable compression for the entire WebSocket stream, drastically reducing amount of traffic. /// Defaults to . /// public GatewayCompressionLevel GatewayCompressionLevel { internal get; set; } = GatewayCompressionLevel.Stream; /// /// Sets the size of the global message cache. /// Setting this to 0 will disable message caching entirely. Defaults to 1024. /// public int MessageCacheSize { internal get; set; } = 1024; /// /// Sets the proxy to use for HTTP and WebSocket connections to Discord. /// Defaults to null. /// - public IWebProxy Proxy { internal get; set; } = null; + public IWebProxy Proxy { internal get; set; } /// /// Sets the timeout for HTTP requests. /// Set to to disable timeouts. /// Defaults to 20 seconds. /// public TimeSpan HttpTimeout { internal get; set; } = TimeSpan.FromSeconds(20); /// /// Defines that the client should attempt to reconnect indefinitely. /// This is typically a very bad idea to set to true, as it will swallow all connection errors. /// Defaults to false. /// - public bool ReconnectIndefinitely { internal get; set; } = false; + public bool ReconnectIndefinitely { internal get; set; } /// /// Sets whether the client should attempt to cache members if exclusively using unprivileged intents. /// /// This will only take effect if there are no or /// intents specified. Otherwise, this will always be overwritten to true. /// /// Defaults to true. /// public bool AlwaysCacheMembers { internal get; set; } = true; /// /// Sets the gateway intents for this client. /// If set, the client will only receive events that they specify with intents. /// Defaults to . /// public DiscordIntents Intents { internal get; set; } = DiscordIntents.AllUnprivileged; /// /// Sets the factory method used to create instances of WebSocket clients. /// Use and equivalents on other implementations to switch out client implementations. /// Defaults to . /// public WebSocketClientFactoryDelegate WebSocketClientFactory { internal get => this._webSocketClientFactory; set { if (value == null) throw new InvalidOperationException("You need to supply a valid WebSocket client factory method."); this._webSocketClientFactory = value; } } private WebSocketClientFactoryDelegate _webSocketClientFactory = WebSocketClient.CreateNew; /// /// Sets the factory method used to create instances of UDP clients. /// Use and equivalents on other implementations to switch out client implementations. /// Defaults to . /// public UdpClientFactoryDelegate UdpClientFactory { internal get => this._udpClientFactory; set => this._udpClientFactory = value ?? throw new InvalidOperationException("You need to supply a valid UDP client factory method."); } private UdpClientFactoryDelegate _udpClientFactory = DcsUdpClient.CreateNew; /// /// Sets the logger implementation to use. /// To create your own logger, implement the instance. /// Defaults to built-in implementation. /// - public ILoggerFactory LoggerFactory { internal get; set; } = null; + public ILoggerFactory LoggerFactory { internal get; set; } /// /// Sets if the bot's status should show the mobile icon. /// Defaults to false. /// - public bool MobileStatus { internal get; set; } = false; + public bool MobileStatus { internal get; set; } /// /// Use canary. /// Defaults to false. /// - public bool UseCanary { internal get; set; } = false; + public bool UseCanary { internal get; set; } /// /// Refresh full guild channel cache. /// Defaults to false. /// - public bool AutoRefreshChannelCache { internal get; set; } = false; + public bool AutoRefreshChannelCache { internal get; set; } /// /// Do not use, this is meant for DisCatSharp Devs. /// Defaults to null. /// - public string Override { internal get; set; } = null; + public string Override { internal get; set; } /// /// Sets the service provider. /// This allows passing data around without resorting to static members. /// Defaults to null. /// public IServiceProvider ServiceProvider { internal get; set; } = new ServiceCollection().BuildServiceProvider(true); /// /// Creates a new configuration with default values. /// public DiscordConfiguration() { } /// /// Utilized via Dependency Injection Pipeline /// /// [ActivatorUtilitiesConstructor] public DiscordConfiguration(IServiceProvider provider) { this.ServiceProvider = provider; } /// /// Creates a clone of another discord configuration. /// /// Client configuration to clone. public DiscordConfiguration(DiscordConfiguration other) { this.Token = other.Token; this.TokenType = other.TokenType; this.MinimumLogLevel = other.MinimumLogLevel; this.UseRelativeRatelimit = other.UseRelativeRatelimit; this.LogTimestampFormat = other.LogTimestampFormat; this.LargeThreshold = other.LargeThreshold; this.AutoReconnect = other.AutoReconnect; this.ShardId = other.ShardId; this.ShardCount = other.ShardCount; this.GatewayCompressionLevel = other.GatewayCompressionLevel; this.MessageCacheSize = other.MessageCacheSize; this.WebSocketClientFactory = other.WebSocketClientFactory; this.UdpClientFactory = other.UdpClientFactory; this.Proxy = other.Proxy; this.HttpTimeout = other.HttpTimeout; this.ReconnectIndefinitely = other.ReconnectIndefinitely; this.Intents = other.Intents; this.LoggerFactory = other.LoggerFactory; this.MobileStatus = other.MobileStatus; this.UseCanary = other.UseCanary; this.AutoRefreshChannelCache = other.AutoRefreshChannelCache; this.ApiVersion = other.ApiVersion; this.ServiceProvider = other.ServiceProvider; this.Override = other.Override; } } } diff --git a/DisCatSharp/Entities/Channel/DiscordChannel.cs b/DisCatSharp/Entities/Channel/DiscordChannel.cs index be1121fef..c5f875b2d 100644 --- a/DisCatSharp/Entities/Channel/DiscordChannel.cs +++ b/DisCatSharp/Entities/Channel/DiscordChannel.cs @@ -1,1402 +1,1371 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Globalization; using System.IO; using System.Linq; using System.Threading.Tasks; using DisCatSharp.Enums; using DisCatSharp.Net; using DisCatSharp.Net.Abstractions; using DisCatSharp.Net.Models; using Newtonsoft.Json; namespace DisCatSharp.Entities { /// /// Represents a discord channel. /// public class DiscordChannel : SnowflakeObject, IEquatable { /// /// Gets ID of the guild to which this channel belongs. /// [JsonProperty("guild_id", NullValueHandling = NullValueHandling.Ignore)] public ulong? GuildId { get; internal set; } /// /// Gets ID of the category that contains this channel. /// [JsonProperty("parent_id", NullValueHandling = NullValueHandling.Include)] public ulong? ParentId { get; internal set; } /// /// Gets the category that contains this channel. /// [JsonIgnore] public DiscordChannel Parent => this.ParentId.HasValue ? this.Guild.GetChannel(this.ParentId.Value) : null; /// /// Gets the name of this channel. /// [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] public string Name { get; internal set; } /// /// Gets the type of this channel. /// [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] public ChannelType Type { get; internal set; } /// /// Gets this channels's banner hash, when applicable. /// [JsonProperty("banner")] public string BannerHash { get; internal set; } /// /// Gets this channels's banner in url form. /// [JsonIgnore] public string BannerUrl => !string.IsNullOrWhiteSpace(this.BannerHash) ? $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Uri}{Endpoints.CHANNELS}/{this.Id.ToString(CultureInfo.InvariantCulture)}{Endpoints.BANNERS}/{this.BannerHash}.{(this.BannerHash.StartsWith("a_") ? "gif" : "png")}" : null; /// /// Gets the position of this channel. /// [JsonProperty("position", NullValueHandling = NullValueHandling.Ignore)] public int Position { get; internal set; } /// /// Gets the maximum available position to move the channel to. /// This can contain outdated informations. /// public int GetMaxPosition() { var channels = this.Guild.Channels.Values; return this.ParentId != null ? this.Type == ChannelType.Text || this.Type == ChannelType.News ? channels.Where(xc => xc.ParentId == this.ParentId && (xc.Type == ChannelType.Text || xc.Type == ChannelType.News)).OrderBy(xc => xc.Position).ToArray().Last().Position : this.Type == ChannelType.Voice || this.Type == ChannelType.Stage ? channels.Where(xc => xc.ParentId == this.ParentId && (xc.Type == ChannelType.Voice || xc.Type == ChannelType.Stage)).OrderBy(xc => xc.Position).ToArray().Last().Position : channels.Where(xc => xc.ParentId == this.ParentId && xc.Type == this.Type).OrderBy(xc => xc.Position).ToArray().Last().Position : channels.Where(xc => xc.ParentId == null && xc.Type == this.Type).OrderBy(xc => xc.Position).ToArray().Last().Position; } /// /// Gets the minimum available position to move the channel to. /// public int GetMinPosition() { var channels = this.Guild.Channels.Values; return this.ParentId != null ? this.Type == ChannelType.Text || this.Type == ChannelType.News ? channels.Where(xc => xc.ParentId == this.ParentId && (xc.Type == ChannelType.Text || xc.Type == ChannelType.News)).OrderBy(xc => xc.Position).ToArray().First().Position : this.Type == ChannelType.Voice || this.Type == ChannelType.Stage ? channels.Where(xc => xc.ParentId == this.ParentId && (xc.Type == ChannelType.Voice || xc.Type == ChannelType.Stage)).OrderBy(xc => xc.Position).ToArray().First().Position : channels.Where(xc => xc.ParentId == this.ParentId && xc.Type == this.Type).OrderBy(xc => xc.Position).ToArray().First().Position : channels.Where(xc => xc.ParentId == null && xc.Type == this.Type).OrderBy(xc => xc.Position).ToArray().First().Position; } /// /// Gets whether this channel is a DM channel. /// [JsonIgnore] public bool IsPrivate => this.Type == ChannelType.Private || this.Type == ChannelType.Group; /// /// Gets whether this channel is a channel category. /// [JsonIgnore] public bool IsCategory => this.Type == ChannelType.Category; /// /// Gets whether this channel is a stage channel. /// [JsonIgnore] public bool IsStage => this.Type == ChannelType.Stage; /// /// Gets the guild to which this channel belongs. /// [JsonIgnore] public DiscordGuild Guild => this.GuildId.HasValue && this.Discord.Guilds.TryGetValue(this.GuildId.Value, out var guild) ? guild : null; /// /// Gets a collection of permission overwrites for this channel. /// [JsonIgnore] public IReadOnlyList PermissionOverwrites => this._permissionOverwritesLazy.Value; [JsonProperty("permission_overwrites", NullValueHandling = NullValueHandling.Ignore)] internal List PermissionOverwritesInternal = new(); [JsonIgnore] private readonly Lazy> _permissionOverwritesLazy; /// /// Gets the channel's topic. This is applicable to text channels only. /// [JsonProperty("topic", NullValueHandling = NullValueHandling.Ignore)] public string Topic { get; internal set; } /// /// Gets the ID of the last message sent in this channel. This is applicable to text channels only. /// [JsonProperty("last_message_id", NullValueHandling = NullValueHandling.Ignore)] public ulong? LastMessageId { get; internal set; } /// /// Gets this channel's bitrate. This is applicable to voice channels only. /// [JsonProperty("bitrate", NullValueHandling = NullValueHandling.Ignore)] public int? Bitrate { get; internal set; } /// /// Gets this channel's user limit. This is applicable to voice channels only. /// [JsonProperty("user_limit", NullValueHandling = NullValueHandling.Ignore)] public int? UserLimit { get; internal set; } /// /// Gets the slow mode delay configured for this channel. /// All bots, as well as users with or permissions in the channel are exempt from slow mode. /// [JsonProperty("rate_limit_per_user")] public int? PerUserRateLimit { get; internal set; } /// /// Gets this channel's video quality mode. This is applicable to voice channels only. /// [JsonProperty("video_quality_mode", NullValueHandling = NullValueHandling.Ignore)] public VideoQualityMode? QualityMode { get; internal set; } /// /// Gets when the last pinned message was pinned. /// [JsonIgnore] public 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 as raw string. /// [JsonProperty("last_pin_timestamp", NullValueHandling = NullValueHandling.Ignore)] internal string LastPinTimestampRaw { get; set; } /// /// Gets this channel's default duration for newly created threads, in minutes, to automatically archive the thread after recent activity. /// [JsonProperty("default_auto_archive_duration", NullValueHandling = NullValueHandling.Ignore)] public ThreadAutoArchiveDuration? DefaultAutoArchiveDuration { get; internal set; } /// /// Gets this channel's mention string. /// [JsonIgnore] public string Mention => Formatter.Mention(this); /// /// Gets this channel's children. This applies only to channel categories. /// [JsonIgnore] - public IReadOnlyList Children - { - get - { - return !this.IsCategory - ? throw new ArgumentException("Only channel categories contain children.") - : this.Guild.ChannelsInternal.Values.Where(e => e.ParentId == this.Id).ToList(); - } - } + public IReadOnlyList Children => + !this.IsCategory + ? throw new ArgumentException("Only channel categories contain children.") + : this.Guild.ChannelsInternal.Values.Where(e => e.ParentId == this.Id).ToList(); /// /// Gets the list of members currently in the channel (if voice channel), or members who can see the channel (otherwise). /// [JsonIgnore] - public virtual IReadOnlyList Users - { - get - { - return this.Guild == null - ? throw new InvalidOperationException("Cannot query users outside of guild channels.") - : this.IsVoiceJoinable() + public virtual IReadOnlyList Users => + this.Guild == null + ? throw new InvalidOperationException("Cannot query users outside of guild channels.") + : this.IsVoiceJoinable() ? this.Guild.Members.Values.Where(x => x.VoiceState?.ChannelId == this.Id).ToList() : this.Guild.Members.Values.Where(x => (this.PermissionsFor(x) & Permissions.AccessChannels) == Permissions.AccessChannels).ToList(); - } - } /// /// Gets whether this channel is an NSFW channel. /// [JsonProperty("nsfw")] public bool IsNsfw { get; internal set; } /// /// Gets this channel's region id (if voice channel). /// [JsonProperty("rtc_region", NullValueHandling = NullValueHandling.Ignore)] internal string RtcRegionId { get; set; } /// /// Gets this channel's region override (if voice channel). /// [JsonIgnore] public DiscordVoiceRegion RtcRegion => this.RtcRegionId != null ? this.Discord.VoiceRegions[this.RtcRegionId] : null; /// /// Only sent on the resolved channels of interaction responses for application commands. Gets the permissions of the user in this channel who invoked the command. /// [JsonProperty("permissions")] public Permissions? UserPermissions { get; internal set; } /// /// Initializes a new instance of the class. /// internal DiscordChannel() { this._permissionOverwritesLazy = new Lazy>(() => new ReadOnlyCollection(this.PermissionOverwritesInternal)); } #region Methods /// /// Sends a message to this channel. /// /// Content of the message to send. /// The sent message. /// Thrown when the client does not have the permission if TTS is true 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(string content) - { - return !this.IsWriteable() + public Task SendMessageAsync(string content) => + !this.IsWriteable() ? throw new ArgumentException("Cannot send a text message to a non-text channel.") : this.Discord.ApiClient.CreateMessageAsync(this.Id, content, null, sticker: null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false); - } /// /// Sends a message to this channel. /// /// Embed to attach to the message. /// The sent message. /// Thrown when the client does not have the permission 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(DiscordEmbed embed) - { - return !this.IsWriteable() + public Task SendMessageAsync(DiscordEmbed embed) => + !this.IsWriteable() ? throw new ArgumentException("Cannot send a text message to a non-text channel.") : this.Discord.ApiClient.CreateMessageAsync(this.Id, null, embed != null ? new[] { embed } : null, sticker: null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false); - } /// /// Sends a message to this channel. /// /// Embed to attach to the message. /// Content of the message to send. /// The sent message. /// Thrown when the client does not have the permission if TTS is true 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(string content, DiscordEmbed embed) - { - return !this.IsWriteable() + public Task SendMessageAsync(string content, DiscordEmbed embed) => + !this.IsWriteable() ? throw new ArgumentException("Cannot send a text message to a non-text channel.") : this.Discord.ApiClient.CreateMessageAsync(this.Id, content, embed != null ? new[] { embed } : null, sticker: null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false); - } /// /// 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 TTS is true 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(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 TTS is true 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(Action action) { var builder = new DiscordMessageBuilder(); action(builder); return !this.IsWriteable() ? throw new ArgumentException("Cannot send a text message to a non-text channel.") : this.Discord.ApiClient.CreateMessageAsync(this.Id, builder); } /// /// Deletes a guild channel /// /// Reason for audit logs. /// 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 DeleteAsync(string reason = null) => this.Discord.ApiClient.DeleteChannelAsync(this.Id, reason); /// /// Clones this channel. This operation will create a channel with identical settings to this one. Note that this will not copy messages. /// /// Reason for audit logs. /// Newly-created channel. /// 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 async Task CloneAsync(string reason = null) { if (this.Guild == null) throw new InvalidOperationException("Non-guild channels cannot be cloned."); var ovrs = new List(); foreach (var ovr in this.PermissionOverwritesInternal) #pragma warning disable CS0618 // Type or member is obsolete ovrs.Add(await new DiscordOverwriteBuilder().FromAsync(ovr).ConfigureAwait(false)); #pragma warning restore CS0618 // Type or member is obsolete var bitrate = this.Bitrate; var userLimit = this.UserLimit; Optional perUserRateLimit = this.PerUserRateLimit; if (!this.IsVoiceJoinable()) { bitrate = null; userLimit = null; } if (this.Type == ChannelType.Stage) { userLimit = null; } if (!this.IsWriteable()) { perUserRateLimit = Optional.FromNoValue(); } return await this.Guild.CreateChannelAsync(this.Name, this.Type, this.Parent, this.Topic, bitrate, userLimit, ovrs, this.IsNsfw, perUserRateLimit, this.QualityMode, reason).ConfigureAwait(false); } /// /// Returns a specific message /// /// The id of the message /// 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 async Task GetMessageAsync(ulong id) - { - return 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) + public 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); - } /// /// Modifies the current channel. /// /// Action to perform on this channel /// Thrown when the client does not have the . /// Thrown when the client does not have the correct for modifying the . /// 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 ModifyAsync(Action action) { var mdl = new ChannelEditModel(); action(mdl); if (mdl.DefaultAutoArchiveDuration.HasValue) { if (!Utilities.CheckThreadAutoArchiveDurationFeature(this.Guild, mdl.DefaultAutoArchiveDuration.Value)) throw new NotSupportedException($"Cannot modify DefaultAutoArchiveDuration. Guild needs boost tier {(mdl.DefaultAutoArchiveDuration.Value == ThreadAutoArchiveDuration.ThreeDays ? "one" : "two")}."); } if (mdl.Banner.HasValue) { if (!this.Guild.Features.CanSetChannelBanner) throw new NotSupportedException($"Cannot modify Banner. Guild needs boost tier three."); } var bannerb64 = Optional.FromNoValue(); if (mdl.Banner.HasValue && mdl.Banner.Value != null) using (var imgtool = new ImageTool(mdl.Banner.Value)) bannerb64 = imgtool.GetBase64(); else if (mdl.Banner.HasValue) bannerb64 = null; return this.Discord.ApiClient.ModifyChannelAsync(this.Id, mdl.Name, mdl.Position, mdl.Topic, mdl.Nsfw, mdl.Parent.HasValue ? mdl.Parent.Value?.Id : default(Optional), mdl.Bitrate, mdl.Userlimit, mdl.PerUserRateLimit, mdl.RtcRegion.IfPresent(r => r?.Id), mdl.QualityMode, mdl.DefaultAutoArchiveDuration, mdl.Type, mdl.PermissionOverwrites, bannerb64, mdl.AuditLogReason); } /// /// Updates the channel position when it doesn't have a category. /// /// Use for moving to other categories. /// Use to move out of a category. /// Use for moving within a category. /// /// Position the channel should be moved to. /// Reason for audit logs. /// 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 ModifyPositionAsync(int position, string reason = null) { if (this.Guild == null) throw new ArgumentException("Cannot modify order of non-guild channels."); if (!this.IsMovable()) throw new NotSupportedException("You can't move this type of channel in categories."); if (this.ParentId != null) throw new ArgumentException("Cannot modify order of channels within a category. Use ModifyPositionInCategoryAsync instead."); var chns = this.Guild.ChannelsInternal.Values.Where(xc => xc.Type == this.Type).OrderBy(xc => xc.Position).ToArray(); var pmds = new RestGuildChannelReorderPayload[chns.Length]; for (var i = 0; i < chns.Length; i++) { pmds[i] = new RestGuildChannelReorderPayload { ChannelId = chns[i].Id, }; pmds[i].Position = chns[i].Id == this.Id ? position : chns[i].Position >= position ? chns[i].Position + 1 : chns[i].Position; } return this.Discord.ApiClient.ModifyGuildChannelPositionAsync(this.Guild.Id, pmds, reason); } /// /// Updates the channel position within it's own category. /// /// Use for moving to other categories. /// Use to move out of a category. /// Use to move channels outside a category. /// /// The position. /// The reason. /// 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. /// Thrown when is out of range. /// Thrown when function is called on a channel without a parent channel. public async Task ModifyPositionInCategoryAsync(int position, string reason = null) { //if (this.ParentId == null) // throw new ArgumentException("You can call this function only on channels in categories."); if (!this.IsMovableInParent()) throw new NotSupportedException("You can't move this type of channel in categories."); var isUp = position > this.Position; var channels = await this.InternalRefreshChannelsAsync(); var chns = this.ParentId != null ? this.Type == ChannelType.Text || this.Type == ChannelType.News ? channels.Where(xc => xc.ParentId == this.ParentId && (xc.Type == ChannelType.Text || xc.Type == ChannelType.News)) : this.Type == ChannelType.Voice || this.Type == ChannelType.Stage ? channels.Where(xc => xc.ParentId == this.ParentId && (xc.Type == ChannelType.Voice || xc.Type == ChannelType.Stage)) : channels.Where(xc => xc.ParentId == this.ParentId && xc.Type == this.Type) : this.Type == ChannelType.Text || this.Type == ChannelType.News ? channels.Where(xc => xc.ParentId == null && (xc.Type == ChannelType.Text || xc.Type == ChannelType.News)) : this.Type == ChannelType.Voice || this.Type == ChannelType.Stage ? channels.Where(xc => xc.ParentId == null && (xc.Type == ChannelType.Voice || xc.Type == ChannelType.Stage)) : channels.Where(xc => xc.ParentId == null && xc.Type == this.Type); var ochns = chns.OrderBy(xc => xc.Position).ToArray(); var min = ochns.First().Position; var max = ochns.Last().Position; if (position > max || position < min) throw new IndexOutOfRangeException($"Position is not in range. {position} is {(position > max ? "greater then the maximal" : "lower then the minimal")} position."); var pmds = new RestGuildChannelReorderPayload[ochns.Length]; for (var i = 0; i < ochns.Length; i++) { pmds[i] = new RestGuildChannelReorderPayload { ChannelId = ochns[i].Id, }; if (ochns[i].Id == this.Id) { pmds[i].Position = position; } else { if (isUp) { if (ochns[i].Position <= position && ochns[i].Position > this.Position) { pmds[i].Position = ochns[i].Position - 1; } else if (ochns[i].Position < this.Position || ochns[i].Position > position) { pmds[i].Position = ochns[i].Position; } } else { if (ochns[i].Position >= position && ochns[i].Position < this.Position) { pmds[i].Position = ochns[i].Position + 1; } else if (ochns[i].Position > this.Position || ochns[i].Position < position) { pmds[i].Position = ochns[i].Position; } } } } await this.Discord.ApiClient.ModifyGuildChannelPositionAsync(this.Guild.Id, pmds, reason).ConfigureAwait(false); } /// /// Internaly refreshes the channel list. /// private async Task> InternalRefreshChannelsAsync() { await this.RefreshPositionsAsync(); return this.Guild.Channels.Values.ToList().AsReadOnly(); } /// /// Refreshes the positions. /// public async Task RefreshPositionsAsync() { var channels = await this.Discord.ApiClient.GetGuildChannelsAsync(this.Guild.Id); this.Guild.ChannelsInternal.Clear(); foreach (var channel in channels.ToList()) { channel.Discord = this.Discord; foreach (var xo in channel.PermissionOverwritesInternal) { xo.Discord = this.Discord; xo.ChannelId = channel.Id; } this.Guild.ChannelsInternal[channel.Id] = channel; } } /// /// Updates the channel position within it's own category. /// Valid modes: '+' or 'down' to move a channel down | '-' or 'up' to move a channel up. /// /// Use for moving to other categories. /// Use to move out of a category. /// Use to move channels outside a category. /// /// The mode. Valid: '+' or 'down' to move a channel down | '-' or 'up' to move a channel up /// The position. /// The reason. /// 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. /// Thrown when is out of range. /// Thrown when function is called on a channel without a parent channel, a wrong mode is givven or given position is zero. public Task ModifyPositionInCategorySmartAsync(string mode, int position, string reason = null) { if (!this.IsMovableInParent()) throw new NotSupportedException("You can't move this type of channel in categories."); if (mode != "+" && mode != "-" && mode != "down" && mode != "up") throw new ArgumentException("Error with the selected mode: Valid is '+' or 'down' to move a channel down and '-' or 'up' to move a channel up"); var positive = mode == "+" || mode == "positive" || mode == "down"; var negative = mode == "-" || mode == "negative" || mode == "up"; return positive ? position < this.GetMaxPosition() ? this.ModifyPositionInCategoryAsync(this.Position + position, reason) : throw new IndexOutOfRangeException($"Position is not in range of category.") : negative ? position > this.GetMinPosition() ? this.ModifyPositionInCategoryAsync(this.Position - position, reason) : throw new IndexOutOfRangeException($"Position is not in range of category.") : throw new ArgumentException("You can only modify with +X or -X. 0 is not valid."); } /// /// Updates the channel parent, moving the channel to the bottom of the new category. /// /// New parent for channel. Will move out of parent if null. /// Sync permissions with parent. Defaults to null. /// Reason for audit logs. /// 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. #pragma warning disable CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. public Task ModifyParentAsync(DiscordChannel? newParent = null, bool? lockPermissions = null, string reason = null) #pragma warning restore CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. { if (this.Guild == null) throw new ArgumentException("Cannot modify parent of non-guild channels."); if (!this.IsMovableInParent()) throw new NotSupportedException("You can't move this type of channel in categories."); if (newParent.Type is not ChannelType.Category) throw new ArgumentException("Only category type channels can be parents."); var position = this.Guild.ChannelsInternal.Values.Where(xc => xc.Type == this.Type && xc.ParentId == newParent.Id) // gets list same type channels in parent .Select(xc => xc.Position).DefaultIfEmpty(-1).Max() + 1; // returns highest position of list +1, default val: 0 var chns = this.Guild.ChannelsInternal.Values.Where(xc => xc.Type == this.Type) .OrderBy(xc => xc.Position).ToArray(); var pmds = new RestGuildChannelNewParentPayload[chns.Length]; for (var i = 0; i < chns.Length; i++) { pmds[i] = new RestGuildChannelNewParentPayload { ChannelId = chns[i].Id, Position = chns[i].Position >= position ? chns[i].Position + 1 : chns[i].Position, }; if (chns[i].Id == this.Id) { pmds[i].Position = position; pmds[i].ParentId = newParent is not null ? newParent.Id : null; pmds[i].LockPermissions = lockPermissions; } } return this.Discord.ApiClient.ModifyGuildChannelParentAsync(this.Guild.Id, pmds, reason); } /// /// Moves the channel out of a category. /// /// Reason for audit logs. /// 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 RemoveParentAsync(string reason = null) { if (this.Guild == null) throw new ArgumentException("Cannot modify parent of non-guild channels."); if (!this.IsMovableInParent()) throw new NotSupportedException("You can't move this type of channel in categories."); var position = this.Guild.ChannelsInternal.Values.Where(xc => xc.Type == this.Type && xc.Parent is null) //gets list of same type channels with no parent .Select(xc => xc.Position).DefaultIfEmpty(-1).Max() + 1; // returns highest position of list +1, default val: 0 var chns = this.Guild.ChannelsInternal.Values.Where(xc => xc.Type == this.Type) .OrderBy(xc => xc.Position).ToArray(); var pmds = new RestGuildChannelNoParentPayload[chns.Length]; for (var i = 0; i < chns.Length; i++) { pmds[i] = new RestGuildChannelNoParentPayload { ChannelId = chns[i].Id, }; if (chns[i].Id == this.Id) { pmds[i].Position = 1; pmds[i].ParentId = null; } else { pmds[i].Position = chns[i].Position < this.Position ? chns[i].Position + 1 : chns[i].Position; } } return this.Discord.ApiClient.DetachGuildChannelParentAsync(this.Guild.Id, pmds, reason); } /// /// 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 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> 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 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> 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 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> GetMessagesAroundAsync(ulong around, int limit = 100) => this.GetMessagesInternalAsync(limit, null, null, around); /// /// Returns a list of messages from the last message in the channel. /// The amount of messages to fetch. /// /// 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> 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.IsWriteable()) throw new ArgumentException("Cannot get the messages of a non-text 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 channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public 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.Count() < 2) { await this.Discord.ApiClient.DeleteMessageAsync(this.Id, msgs.Single(), reason).ConfigureAwait(false); return; } for (var i = 0; i < msgs.Count(); 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 channel does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task DeleteMessageAsync(DiscordMessage message, string reason = null) => this.Discord.ApiClient.DeleteMessageAsync(this.Id, message.Id, reason); /// /// Returns a list of invite objects /// /// 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> GetInvitesAsync() - { - return this.Guild == null + public Task> GetInvitesAsync() => + this.Guild == null ? throw new ArgumentException("Cannot get the invites of a channel that does not belong to a guild.") : this.Discord.ApiClient.GetChannelInvitesAsync(this.Id); - } /// /// Create a new invite object /// /// Duration of invite in seconds before expiry, or 0 for never. Defaults to 86400. /// Max number of uses or 0 for unlimited. Defaults to 0 /// Whether this invite should be temporary. Defaults to false. /// Whether this invite should be unique. Defaults to false. /// The target type. Defaults to null. /// The target activity. Defaults to null. /// The target user id. Defaults to null. /// The audit log reason. /// 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 CreateInviteAsync(int maxAge = 86400, int maxUses = 0, bool temporary = false, bool unique = false, TargetType? targetType = null, TargetActivity? targetApplication = null, ulong? targetUser = null, string reason = null) => this.Discord.ApiClient.CreateChannelInviteAsync(this.Id, maxAge, maxUses, targetType, targetApplication, targetUser, temporary, unique, reason); #region Stage /// /// Opens a stage. /// /// Topic of the stage. /// Whether @everyone should be notified. /// Privacy level of the stage (Defaults to . /// Audit log reason. /// Stage instance /// 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 async Task OpenStageAsync(string topic, bool sendStartNotification = false, StagePrivacyLevel privacyLevel = StagePrivacyLevel.GuildOnly, string reason = null) => await this.Discord.ApiClient.CreateStageInstanceAsync(this.Id, topic, sendStartNotification, privacyLevel, reason); /// /// Modifies a stage topic. /// /// New topic of the stage. /// New privacy level of the stage. /// Audit log reason. /// 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 async Task ModifyStageAsync(Optional topic, Optional privacyLevel, string reason = null) => await this.Discord.ApiClient.ModifyStageInstanceAsync(this.Id, topic, privacyLevel, reason); /// /// Closes a stage. /// /// Audit log reason. /// 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 async Task CloseStageAsync(string reason = null) => await this.Discord.ApiClient.DeleteStageInstanceAsync(this.Id, reason); /// /// Gets a stage. /// /// The requested stage. /// 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. public async Task GetStageAsync() => await this.Discord.ApiClient.GetStageInstanceAsync(this.Id); #endregion #region Threads /// /// Creates a thread. /// Depending on whether it is created inside an or an it is either an or an . /// Depending on whether the is set to it is either an or an (default). /// /// The name of the thread. /// till it gets archived. Defaults to . /// Can be either an , or an . /// The per user ratelimit, aka slowdown. /// Audit log reason. /// The created thread. /// Thrown when the client does not have the or or if creating a private thread the permission. /// Thrown when the guild hasn't enabled threads atm. /// 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 . Or if is not enabled for guild. This happens, if the guild does not have - public async Task CreateThreadAsync(string name, ThreadAutoArchiveDuration autoArchiveDuration = ThreadAutoArchiveDuration.OneHour, ChannelType type = ChannelType.PublicThread, int? rateLimitPerUser = null, string reason = null) - { - return (type != ChannelType.NewsThread && type != ChannelType.PublicThread && type != ChannelType.PrivateThread) + public async Task CreateThreadAsync(string name, ThreadAutoArchiveDuration autoArchiveDuration = ThreadAutoArchiveDuration.OneHour, ChannelType type = ChannelType.PublicThread, int? rateLimitPerUser = null, string reason = null) => + type != ChannelType.NewsThread && type != ChannelType.PublicThread && type != ChannelType.PrivateThread ? throw new NotSupportedException("Wrong thread type given.") - : (!this.IsThreadHolder()) - ? throw new NotSupportedException("Parent channel can't have threads.") - : type == ChannelType.PrivateThread - ? Utilities.CheckThreadPrivateFeature(this.Guild) - ? Utilities.CheckThreadAutoArchiveDurationFeature(this.Guild, autoArchiveDuration) - ? await this.Discord.ApiClient.CreateThreadWithoutMessageAsync(this.Id, name, autoArchiveDuration, type, rateLimitPerUser, reason) - : throw new NotSupportedException($"Cannot modify ThreadAutoArchiveDuration. Guild needs boost tier {(autoArchiveDuration == ThreadAutoArchiveDuration.ThreeDays ? "one" : "two")}.") - : throw new NotSupportedException($"Cannot create a private thread. Guild needs to be boost tier two.") - : Utilities.CheckThreadAutoArchiveDurationFeature(this.Guild, autoArchiveDuration) - ? await this.Discord.ApiClient.CreateThreadWithoutMessageAsync(this.Id, name, autoArchiveDuration, this.Type == ChannelType.News ? ChannelType.NewsThread : ChannelType.PublicThread, rateLimitPerUser, reason) - : throw new NotSupportedException($"Cannot modify ThreadAutoArchiveDuration. Guild needs boost tier {(autoArchiveDuration == ThreadAutoArchiveDuration.ThreeDays ? "one" : "two")}."); - } + : !this.IsThreadHolder() + ? throw new NotSupportedException("Parent channel can't have threads.") + : type == ChannelType.PrivateThread + ? Utilities.CheckThreadPrivateFeature(this.Guild) + ? Utilities.CheckThreadAutoArchiveDurationFeature(this.Guild, autoArchiveDuration) + ? await this.Discord.ApiClient.CreateThreadWithoutMessageAsync(this.Id, name, autoArchiveDuration, type, rateLimitPerUser, reason) + : throw new NotSupportedException($"Cannot modify ThreadAutoArchiveDuration. Guild needs boost tier {(autoArchiveDuration == ThreadAutoArchiveDuration.ThreeDays ? "one" : "two")}.") + : throw new NotSupportedException($"Cannot create a private thread. Guild needs to be boost tier two.") + : Utilities.CheckThreadAutoArchiveDurationFeature(this.Guild, autoArchiveDuration) + ? await this.Discord.ApiClient.CreateThreadWithoutMessageAsync(this.Id, name, autoArchiveDuration, this.Type == ChannelType.News ? ChannelType.NewsThread : ChannelType.PublicThread, rateLimitPerUser, reason) + : throw new NotSupportedException($"Cannot modify ThreadAutoArchiveDuration. Guild needs boost tier {(autoArchiveDuration == ThreadAutoArchiveDuration.ThreeDays ? "one" : "two")}."); /// /// Creates a scheduled event based on the channel type. /// /// The name. /// The scheduled start time. /// The description. /// The reason. /// A scheduled event. /// Thrown when the ressource does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task CreateScheduledEventAsync(string name, DateTimeOffset scheduledStartTime, string description = null, string reason = null) { if (!this.IsVoiceJoinable()) throw new NotSupportedException("Cannot create a scheduled event for this type of channel. Channel type must be either voice or stage."); var type = this.Type == ChannelType.Voice ? ScheduledEventEntityType.Voice : ScheduledEventEntityType.StageInstance; return await this.Guild.CreateScheduledEventAsync(name, scheduledStartTime, null, this, null, description, type, reason); } /// /// Gets joined archived private threads. Can contain more threads. /// If the result's value 'HasMore' is true, you need to recall this function to get older threads. /// /// Get threads created before this thread id. /// Defines the limit of returned . /// 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 async Task GetJoinedPrivateArchivedThreadsAsync(ulong? before, int? limit) => await this.Discord.ApiClient.GetJoinedPrivateArchivedThreadsAsync(this.Id, before, limit); /// /// Gets archived public threads. Can contain more threads. /// If the result's value 'HasMore' is true, you need to recall this function to get older threads. /// /// Get threads created before this thread id. /// Defines the limit of returned . /// 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 async Task GetPublicArchivedThreadsAsync(ulong? before, int? limit) => await this.Discord.ApiClient.GetPublicArchivedThreadsAsync(this.Id, before, limit); /// /// Gets archived private threads. Can contain more threads. /// If the result's value 'HasMore' is true, you need to recall this function to get older threads. /// /// Get threads created before this thread id. /// Defines the limit of returned . /// 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. public async Task GetPrivateArchivedThreadsAsync(ulong? before, int? limit) => await this.Discord.ApiClient.GetPrivateArchivedThreadsAsync(this.Id, before, limit); #endregion /// /// Adds a channel permission overwrite for specified role. /// /// The role to have the permission added. /// The permissions to allow. /// The permissions to deny. /// Reason for audit logs. /// 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 AddOverwriteAsync(DiscordRole role, Permissions allow = Permissions.None, Permissions deny = Permissions.None, string reason = null) => this.Discord.ApiClient.EditChannelPermissionsAsync(this.Id, role.Id, allow, deny, "role", reason); /// /// Adds a channel permission overwrite for specified member. /// /// The member to have the permission added. /// The permissions to allow. /// The permissions to deny. /// Reason for audit logs. /// 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 AddOverwriteAsync(DiscordMember member, Permissions allow = Permissions.None, Permissions deny = Permissions.None, string reason = null) => this.Discord.ApiClient.EditChannelPermissionsAsync(this.Id, member.Id, allow, deny, "member", reason); /// /// Deletes a channel permission overwrite for specified member. /// /// The member to have the permission deleted. /// Reason for audit logs. /// 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 DeleteOverwriteAsync(DiscordMember member, string reason = null) => this.Discord.ApiClient.DeleteChannelPermissionAsync(this.Id, member.Id, reason); /// /// Deletes a channel permission overwrite for specified role. /// /// The role to have the permission deleted. /// Reason for audit logs. /// 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 DeleteOverwriteAsync(DiscordRole role, string reason = null) => this.Discord.ApiClient.DeleteChannelPermissionAsync(this.Id, role.Id, reason); /// /// Post a typing indicator /// /// 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 TriggerTypingAsync() - { - return !this.IsWriteable() + public Task TriggerTypingAsync() => + !this.IsWriteable() ? 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. /// 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> GetPinnedMessagesAsync() - { - return !this.IsWriteable() + public Task> GetPinnedMessagesAsync() => + !this.IsWriteable() ? throw new ArgumentException("A non-text channel does not have pinned messages.") : this.Discord.ApiClient.GetPinnedMessagesAsync(this.Id); - } /// /// Create a new webhook /// /// The name of the webhook. /// The image for the default webhook avatar. /// Reason for audit logs. /// 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 async Task CreateWebhookAsync(string name, Optional avatar = default, string reason = null) { var av64 = Optional.FromNoValue(); if (avatar.HasValue && avatar.Value != null) using (var imgtool = new ImageTool(avatar.Value)) av64 = imgtool.GetBase64(); else if (avatar.HasValue) av64 = null; return await this.Discord.ApiClient.CreateWebhookAsync(this.Id, name, av64, reason).ConfigureAwait(false); } /// /// Returns a list of webhooks /// /// Thrown when the client does not have the permission. /// Thrown when the channel does not exist. /// Thrown when Discord is unable to process the request. public Task> GetWebhooksAsync() => this.Discord.ApiClient.GetChannelWebhooksAsync(this.Id); /// /// Moves a member to this voice channel /// /// The member to be moved. /// Thrown when the client does not have the permission. /// Thrown when the channel does not exists or if the Member does not exists. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task PlaceMemberAsync(DiscordMember member) { if (!this.IsVoiceJoinable()) throw new ArgumentException("Cannot place a member in a non-voice channel."); await this.Discord.ApiClient.ModifyGuildMemberAsync(this.Guild.Id, member.Id, default, default, default, default, this.Id, null).ConfigureAwait(false); } /// /// Follows a news channel /// /// Channel to crosspost messages to /// Thrown when trying to follow a non-news channel /// Thrown when the current user doesn't have on the target channel - public Task FollowAsync(DiscordChannel targetChannel) - { - return this.Type != ChannelType.News + public Task FollowAsync(DiscordChannel targetChannel) => + this.Type != ChannelType.News ? throw new ArgumentException("Cannot follow a non-news channel.") : this.Discord.ApiClient.FollowChannelAsync(this.Id, targetChannel.Id); - } /// /// Publishes a message in a news channel to following channels /// /// Message to publish /// Thrown when the message has already been crossposted /// /// Thrown when the current user doesn't have and/or /// - public Task CrosspostMessageAsync(DiscordMessage message) - { - return (message.Flags & MessageFlags.Crossposted) == MessageFlags.Crossposted + public Task CrosspostMessageAsync(DiscordMessage message) => + (message.Flags & MessageFlags.Crossposted) == MessageFlags.Crossposted ? throw new ArgumentException("Message is already crossposted.") : this.Discord.ApiClient.CrosspostMessageAsync(this.Id, message.Id); - } /// /// Updates the current user's suppress state in this channel, if stage channel. /// /// Toggles the suppress state. /// Sets the time the user requested to speak. /// Thrown when the channel is not a stage channel. public async Task UpdateCurrentUserVoiceStateAsync(bool? suppress, DateTimeOffset? requestToSpeakTimestamp = null) { if (this.Type != ChannelType.Stage) throw new ArgumentException("Voice state can only be updated in a stage channel."); await this.Discord.ApiClient.UpdateCurrentUserVoiceStateAsync(this.GuildId.Value, this.Id, suppress, requestToSpeakTimestamp).ConfigureAwait(false); } /// /// Calculates permissions for a given member. /// /// Member to calculate permissions for. /// Calculated permissions for a given member. public Permissions PermissionsFor(DiscordMember mbr) { // future note: might be able to simplify @everyone role checks to just check any role ... but I'm not sure // xoxo, ~uwx // // you should use a single tilde // ~emzi // user > role > everyone // allow > deny > undefined // => // user allow > user deny > role allow > role deny > everyone allow > everyone deny // thanks to meew0 if (this.IsPrivate || this.Guild == null) return Permissions.None; if (this.Guild.OwnerId == mbr.Id) return PermissionMethods.FullPerms; Permissions perms; // assign @everyone permissions var everyoneRole = this.Guild.EveryoneRole; perms = everyoneRole.Permissions; // roles that member is in var mbRoles = mbr.Roles.Where(xr => xr.Id != everyoneRole.Id).ToArray(); // assign permissions from member's roles (in order) perms |= mbRoles.Aggregate(Permissions.None, (c, role) => c | role.Permissions); // Adminstrator grants all permissions and cannot be overridden if ((perms & Permissions.Administrator) == Permissions.Administrator) return PermissionMethods.FullPerms; // channel overrides for roles that member is in var mbRoleOverrides = mbRoles .Select(xr => this.PermissionOverwritesInternal.FirstOrDefault(xo => xo.Id == xr.Id)) .Where(xo => xo != null) .ToList(); // assign channel permission overwrites for @everyone pseudo-role var everyoneOverwrites = this.PermissionOverwritesInternal.FirstOrDefault(xo => xo.Id == everyoneRole.Id); if (everyoneOverwrites != null) { perms &= ~everyoneOverwrites.Denied; perms |= everyoneOverwrites.Allowed; } // assign channel permission overwrites for member's roles (explicit deny) perms &= ~mbRoleOverrides.Aggregate(Permissions.None, (c, overs) => c | overs.Denied); // assign channel permission overwrites for member's roles (explicit allow) perms |= mbRoleOverrides.Aggregate(Permissions.None, (c, overs) => c | overs.Allowed); // channel overrides for just this member var mbOverrides = this.PermissionOverwritesInternal.FirstOrDefault(xo => xo.Id == mbr.Id); if (mbOverrides == null) return perms; // assign channel permission overwrites for just this member perms &= ~mbOverrides.Denied; perms |= mbOverrides.Allowed; return perms; } /// /// Returns a string representation of this channel. /// /// String representation of this channel. - public override string ToString() - { - return this.Type == ChannelType.Category + public override string ToString() => + this.Type == ChannelType.Category ? $"Channel Category {this.Name} ({this.Id})" : this.Type == ChannelType.Text || this.Type == ChannelType.News || this.IsThread() - ? $"Channel #{this.Name} ({this.Id})" - : this.IsVoiceJoinable() - ? $"Channel #!{this.Name} ({this.Id})" - : !string.IsNullOrWhiteSpace(this.Name) ? $"Channel {this.Name} ({this.Id})" : $"Channel {this.Id}"; - } + ? $"Channel #{this.Name} ({this.Id})" + : this.IsVoiceJoinable() + ? $"Channel #!{this.Name} ({this.Id})" + : !string.IsNullOrWhiteSpace(this.Name) ? $"Channel {this.Name} ({this.Id})" : $"Channel {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 DiscordChannel); /// /// Checks whether this is equal to another . /// /// to compare to. /// Whether the is equal to this . public bool Equals(DiscordChannel 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 ==(DiscordChannel e1, DiscordChannel 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 !=(DiscordChannel e1, DiscordChannel e2) => !(e1 == e2); } } diff --git a/DisCatSharp/Entities/Channel/Overwrite/DiscordOverwrite.cs b/DisCatSharp/Entities/Channel/Overwrite/DiscordOverwrite.cs index 606062e3e..8e6409278 100644 --- a/DisCatSharp/Entities/Channel/Overwrite/DiscordOverwrite.cs +++ b/DisCatSharp/Entities/Channel/Overwrite/DiscordOverwrite.cs @@ -1,124 +1,118 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Threading.Tasks; using Newtonsoft.Json; namespace DisCatSharp.Entities { /// /// Represents a permission overwrite for a channel. /// public class DiscordOverwrite : SnowflakeObject { /// /// Gets the type of the overwrite. Either "role" or "member". /// [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] public OverwriteType Type { get; internal set; } /// /// Gets the allowed permission set. /// [JsonProperty("allow", NullValueHandling = NullValueHandling.Ignore)] public Permissions Allowed { get; internal set; } /// /// Gets the denied permission set. /// [JsonProperty("deny", NullValueHandling = NullValueHandling.Ignore)] public Permissions Denied { get; internal set; } [JsonIgnore] internal ulong ChannelId; #region Methods /// /// Deletes this channel overwrite. /// /// Reason as to why this overwrite gets deleted. /// public Task DeleteAsync(string reason = null) => this.Discord.ApiClient.DeleteChannelPermissionAsync(this.ChannelId, this.Id, reason); /// /// Updates this channel overwrite. /// /// Permissions that are allowed. /// Permissions that are denied. /// Reason as to why you made this change. /// /// Thrown when the client does not have the permission. /// Thrown when the overwrite does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task UpdateAsync(Permissions? allow = null, Permissions? deny = null, string reason = null) => this.Discord.ApiClient.EditChannelPermissionsAsync(this.ChannelId, this.Id, allow ?? this.Allowed, deny ?? this.Denied, this.Type.ToString().ToLowerInvariant(), reason); #endregion /// /// Gets the DiscordMember that is affected by this overwrite. /// /// The DiscordMember that is affected by this overwrite /// Thrown when the client does not have the permission. /// Thrown when the overwrite does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public async Task GetMemberAsync() - { - return this.Type != OverwriteType.Member + public async Task GetMemberAsync() => + this.Type != OverwriteType.Member ? throw new ArgumentException(nameof(this.Type), "This overwrite is for a role, not a member.") : await (await this.Discord.ApiClient.GetChannelAsync(this.ChannelId).ConfigureAwait(false)).Guild.GetMemberAsync(this.Id).ConfigureAwait(false); - } /// /// Gets the DiscordRole that is affected by this overwrite. /// /// The DiscordRole that is affected by this overwrite /// Thrown when the role does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public async Task GetRoleAsync() - { - return this.Type != OverwriteType.Role + public async Task GetRoleAsync() => + this.Type != OverwriteType.Role ? throw new ArgumentException(nameof(this.Type), "This overwrite is for a member, not a role.") : (await this.Discord.ApiClient.GetChannelAsync(this.ChannelId).ConfigureAwait(false)).Guild.GetRole(this.Id); - } /// /// Initializes a new instance of the class. /// internal DiscordOverwrite() { } /// /// Checks whether given permissions are allowed, denied, or not set. /// /// Permissions to check. /// Whether given permissions are allowed, denied, or not set. - public PermissionLevel CheckPermission(Permissions permission) - { - return (this.Allowed & permission) != 0 + public PermissionLevel CheckPermission(Permissions permission) => + (this.Allowed & permission) != 0 ? PermissionLevel.Allowed : (this.Denied & permission) != 0 ? PermissionLevel.Denied : PermissionLevel.Unset; - } } } diff --git a/DisCatSharp/Entities/Channel/Overwrite/DiscordOverwriteBuilder.cs b/DisCatSharp/Entities/Channel/Overwrite/DiscordOverwriteBuilder.cs index 1dd118a0f..37b4e50c1 100644 --- a/DisCatSharp/Entities/Channel/Overwrite/DiscordOverwriteBuilder.cs +++ b/DisCatSharp/Entities/Channel/Overwrite/DiscordOverwriteBuilder.cs @@ -1,183 +1,181 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Threading.Tasks; using Newtonsoft.Json; namespace DisCatSharp.Entities { /// /// Represents a Discord permission overwrite builder. /// public sealed class DiscordOverwriteBuilder { /// /// Gets or sets the allowed permissions for this overwrite. /// public Permissions Allowed { get; set; } /// /// Gets or sets the denied permissions for this overwrite. /// public Permissions Denied { get; set; } /// /// Gets the type of this overwrite's target. /// public OverwriteType Type { get; private set; } /// /// Gets the target for this overwrite. /// public SnowflakeObject Target { get; private set; } /// /// Creates a new Discord permission overwrite builder for a member. This class can be used to construct permission overwrites for guild channels, used when creating channels. /// public DiscordOverwriteBuilder(DiscordMember member) { this.Target = member; this.Type = OverwriteType.Member; } /// /// Creates a new Discord permission overwrite builder for a role. This class can be used to construct permission overwrites for guild channels, used when creating channels. /// public DiscordOverwriteBuilder(DiscordRole role) { this.Target = role; this.Type = OverwriteType.Role; } /// /// Creates a new Discord permission overwrite builder. This class can be used to construct permission overwrites for guild channels, used when creating channels. /// [Obsolete("Will be removed in 10.0. Use specialized constructors instead", false)] public DiscordOverwriteBuilder() { } /// /// Allows a permission for this overwrite. /// /// Permission or permission set to allow for this overwrite. /// This builder. public DiscordOverwriteBuilder Allow(Permissions permission) { this.Allowed |= permission; return this; } /// /// Denies a permission for this overwrite. /// /// Permission or permission set to deny for this overwrite. /// This builder. public DiscordOverwriteBuilder Deny(Permissions permission) { this.Denied |= permission; return this; } /// /// Sets the member to which this overwrite applies. /// /// Member to which apply this overwrite's permissions. /// This builder. public DiscordOverwriteBuilder For(DiscordMember member) { this.Target = member; this.Type = OverwriteType.Member; return this; } /// /// Sets the role to which this overwrite applies. /// /// Role to which apply this overwrite's permissions. /// This builder. public DiscordOverwriteBuilder For(DiscordRole role) { this.Target = role; this.Type = OverwriteType.Role; return this; } /// /// Populates this builder with data from another overwrite object. /// /// Overwrite from which data will be used. /// This builder. public async Task FromAsync(DiscordOverwrite other) { this.Allowed = other.Allowed; this.Denied = other.Denied; this.Type = other.Type; this.Target = this.Type == OverwriteType.Member ? await other.GetMemberAsync().ConfigureAwait(false) as SnowflakeObject : await other.GetRoleAsync().ConfigureAwait(false) as SnowflakeObject; return this; } /// /// Builds this DiscordOverwrite. /// /// Use this object for creation of new overwrites. - internal DiscordRestOverwrite Build() - { - return new DiscordRestOverwrite() + internal DiscordRestOverwrite Build() => + new DiscordRestOverwrite() { Allow = this.Allowed, Deny = this.Denied, Id = this.Target.Id, Type = this.Type, }; - } } internal struct DiscordRestOverwrite { /// /// Determines what is allowed. /// [JsonProperty("allow", NullValueHandling = NullValueHandling.Ignore)] internal Permissions Allow { get; set; } /// /// Determines what is denied. /// [JsonProperty("deny", NullValueHandling = NullValueHandling.Ignore)] internal Permissions Deny { get; set; } /// /// Gets or sets the id. /// [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)] internal ulong Id { get; set; } /// /// Gets or sets the overwrite type. /// [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] internal OverwriteType Type { get; set; } } } diff --git a/DisCatSharp/Entities/Color/DiscordColor.Colors.cs b/DisCatSharp/Entities/Color/DiscordColor.Colors.cs index f2c735785..375c3124d 100644 --- a/DisCatSharp/Entities/Color/DiscordColor.Colors.cs +++ b/DisCatSharp/Entities/Color/DiscordColor.Colors.cs @@ -1,264 +1,264 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. namespace DisCatSharp.Entities { public partial struct DiscordColor { #region Black and White /// /// Represents no color, or integer 0; /// - public static DiscordColor None { get; } = new DiscordColor(0); + public static DiscordColor None { get; } = new(0); /// /// A near-black color. Due to API limitations, the color is #010101, rather than #000000, as the latter is treated as no color. /// - public static DiscordColor Black { get; } = new DiscordColor(0x010101); + public static DiscordColor Black { get; } = new(0x010101); /// /// White, or #FFFFFF. /// - public static DiscordColor White { get; } = new DiscordColor(0xFFFFFF); + public static DiscordColor White { get; } = new(0xFFFFFF); /// /// Gray, or #808080. /// - public static DiscordColor Gray { get; } = new DiscordColor(0x808080); + public static DiscordColor Gray { get; } = new(0x808080); /// /// Dark gray, or #A9A9A9. /// - public static DiscordColor DarkGray { get; } = new DiscordColor(0xA9A9A9); + public static DiscordColor DarkGray { get; } = new(0xA9A9A9); /// /// Light gray, or #808080. /// - public static DiscordColor LightGray { get; } = new DiscordColor(0xD3D3D3); + public static DiscordColor LightGray { get; } = new(0xD3D3D3); // dev-approved /// /// Very dark gray, or #666666. /// - public static DiscordColor VeryDarkGray { get; } = new DiscordColor(0x666666); + public static DiscordColor VeryDarkGray { get; } = new(0x666666); #endregion #region Discord branding colors // https://discord.com/branding /// /// Discord Blurple, or #5865F2. /// - public static DiscordColor Blurple { get; } = new DiscordColor(0x5865F2); + public static DiscordColor Blurple { get; } = new(0x5865F2); /// /// Discord Fuchsia, or #EB459E. /// - public static DiscordColor Fuchsia { get; } = new DiscordColor(0xEB459E); + public static DiscordColor Fuchsia { get; } = new(0xEB459E); /// /// Discord Green, or #57F287. /// - public static DiscordColor Green { get; } = new DiscordColor(0x57F287); + public static DiscordColor Green { get; } = new(0x57F287); /// /// Discord Yellow, or #FEE75C. /// - public static DiscordColor Yellow { get; } = new DiscordColor(0xFEE75C); + public static DiscordColor Yellow { get; } = new(0xFEE75C); /// /// Discord Red, or #ED4245. /// - public static DiscordColor Red { get; } = new DiscordColor(0xED4245); + public static DiscordColor Red { get; } = new(0xED4245); #endregion #region Other colors /// /// Dark red, or #7F0000. /// - public static DiscordColor DarkRed { get; } = new DiscordColor(0x7F0000); + public static DiscordColor DarkRed { get; } = new(0x7F0000); /// /// Dark green, or #007F00. /// - public static DiscordColor DarkGreen { get; } = new DiscordColor(0x007F00); + public static DiscordColor DarkGreen { get; } = new(0x007F00); /// /// Blue, or #0000FF. /// - public static DiscordColor Blue { get; } = new DiscordColor(0x0000FF); + public static DiscordColor Blue { get; } = new(0x0000FF); /// /// Dark blue, or #00007F. /// - public static DiscordColor DarkBlue { get; } = new DiscordColor(0x00007F); + public static DiscordColor DarkBlue { get; } = new(0x00007F); /// /// Cyan, or #00FFFF. /// - public static DiscordColor Cyan { get; } = new DiscordColor(0x00FFFF); + public static DiscordColor Cyan { get; } = new(0x00FFFF); /// /// Magenta, or #FF00FF. /// - public static DiscordColor Magenta { get; } = new DiscordColor(0xFF00FF); + public static DiscordColor Magenta { get; } = new(0xFF00FF); /// /// Teal, or #008080. /// - public static DiscordColor Teal { get; } = new DiscordColor(0x008080); + public static DiscordColor Teal { get; } = new(0x008080); // meme /// /// Aquamarine, or #00FFBF. /// - public static DiscordColor Aquamarine { get; } = new DiscordColor(0x00FFBF); + public static DiscordColor Aquamarine { get; } = new(0x00FFBF); /// /// Gold, or #FFD700. /// - public static DiscordColor Gold { get; } = new DiscordColor(0xFFD700); + public static DiscordColor Gold { get; } = new(0xFFD700); // To be fair, you have to have a very high IQ to understand Goldenrod . // The tones are extremely subtle, and without a solid grasp of artistic // theory most of the beauty will go over a typical painter's head. // There's also the flower's nihilistic style, which is deftly woven // into its characterization - it's pollinated by the Bombus cryptarum // bumblebee, for instance. The fans understand this stuff; they have // the intellectual capacity to truly appreciate the depth of this // flower, to realize that it's not just a color - it says something // deep about LIFE. As a consequence people who dislike Goldenrod truly // ARE idiots - of course they wouldn't appreciate, for instance, the // beauty in the bumblebee species' complex presence in the British Isles, // which is cryptically explained by Turgenev's Russian epic Fathers and // Sons I'm blushing right now just imagining one of those addlepated // simpletons scratching their heads in confusion as nature's genius // unfolds itself on their computer screens. What fools... how I pity them. // 😂 And yes by the way, I DO have a goldenrod tattoo. And no, you cannot // see it. It's for the ladies' eyes only- And even they have to // demonstrate that they're within 5 IQ points of my own (preferably lower) beforehand. /// /// Goldenrod, or #DAA520. /// - public static DiscordColor Goldenrod { get; } = new DiscordColor(0xDAA520); + public static DiscordColor Goldenrod { get; } = new(0xDAA520); // emzi's favourite /// /// Azure, or #007FFF. /// - public static DiscordColor Azure { get; } = new DiscordColor(0x007FFF); + public static DiscordColor Azure { get; } = new(0x007FFF); /// /// Rose, or #FF007F. /// - public static DiscordColor Rose { get; } = new DiscordColor(0xFF007F); + public static DiscordColor Rose { get; } = new(0xFF007F); /// /// Spring green, or #00FF7F. /// - public static DiscordColor SpringGreen { get; } = new DiscordColor(0x00FF7F); + public static DiscordColor SpringGreen { get; } = new(0x00FF7F); /// /// Chartreuse, or #7FFF00. /// - public static DiscordColor Chartreuse { get; } = new DiscordColor(0x7FFF00); + public static DiscordColor Chartreuse { get; } = new(0x7FFF00); /// /// Orange, or #FFA500. /// - public static DiscordColor Orange { get; } = new DiscordColor(0xFFA500); + public static DiscordColor Orange { get; } = new(0xFFA500); /// /// Purple, or #800080. /// - public static DiscordColor Purple { get; } = new DiscordColor(0x800080); + public static DiscordColor Purple { get; } = new(0x800080); /// /// Violet, or #EE82EE. /// - public static DiscordColor Violet { get; } = new DiscordColor(0xEE82EE); + public static DiscordColor Violet { get; } = new(0xEE82EE); /// /// Brown, or #A52A2A. /// - public static DiscordColor Brown { get; } = new DiscordColor(0xA52A2A); + public static DiscordColor Brown { get; } = new(0xA52A2A); // meme /// /// Hot pink, or #FF69B4 /// - public static DiscordColor HotPink { get; } = new DiscordColor(0xFF69B4); + public static DiscordColor HotPink { get; } = new(0xFF69B4); /// /// Lilac, or #C8A2C8. /// - public static DiscordColor Lilac { get; } = new DiscordColor(0xC8A2C8); + public static DiscordColor Lilac { get; } = new(0xC8A2C8); /// /// Cornflower blue, or #6495ED. /// - public static DiscordColor CornflowerBlue { get; } = new DiscordColor(0x6495ED); + public static DiscordColor CornflowerBlue { get; } = new(0x6495ED); /// /// Midnight blue, or #191970. /// - public static DiscordColor MidnightBlue { get; } = new DiscordColor(0x191970); + public static DiscordColor MidnightBlue { get; } = new(0x191970); /// /// Wheat, or #F5DEB3. /// - public static DiscordColor Wheat { get; } = new DiscordColor(0xF5DEB3); + public static DiscordColor Wheat { get; } = new(0xF5DEB3); /// /// Indian red, or #CD5C5C. /// - public static DiscordColor IndianRed { get; } = new DiscordColor(0xCD5C5C); + public static DiscordColor IndianRed { get; } = new(0xCD5C5C); /// /// Turquoise, or #30D5C8. /// - public static DiscordColor Turquoise { get; } = new DiscordColor(0x30D5C8); + public static DiscordColor Turquoise { get; } = new(0x30D5C8); /// /// Sap green, or #507D2A. /// - public static DiscordColor SapGreen { get; } = new DiscordColor(0x507D2A); + public static DiscordColor SapGreen { get; } = new(0x507D2A); // meme, specifically bob ross /// /// Phthalo blue, or #000F89. /// - public static DiscordColor PhthaloBlue { get; } = new DiscordColor(0x000F89); + public static DiscordColor PhthaloBlue { get; } = new(0x000F89); // meme, specifically bob ross /// /// Phthalo green, or #123524. /// - public static DiscordColor PhthaloGreen { get; } = new DiscordColor(0x123524); + public static DiscordColor PhthaloGreen { get; } = new(0x123524); /// /// Sienna, or #882D17. /// - public static DiscordColor Sienna { get; } = new DiscordColor(0x882D17); + public static DiscordColor Sienna { get; } = new(0x882D17); #endregion } } diff --git a/DisCatSharp/Entities/Emoji/DiscordEmoji.cs b/DisCatSharp/Entities/Emoji/DiscordEmoji.cs index 23076792d..325e3bb76 100644 --- a/DisCatSharp/Entities/Emoji/DiscordEmoji.cs +++ b/DisCatSharp/Entities/Emoji/DiscordEmoji.cs @@ -1,380 +1,371 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Globalization; using System.Linq; using 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 - { - get - { - return this.Id == 0 - ? throw new InvalidOperationException("Cannot get URL of unicode emojis.") - : this.IsAnimated + 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 }:"; } /// /// Returns a string representation of this emoji. /// /// String representation of this emoji. - public override string ToString() - { - return this.Id != 0 + 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) - { - return !IsValidUnicode(unicodeEntity) + 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.cs b/DisCatSharp/Entities/Guild/DiscordGuild.cs index 2282d9cea..ff1ace8a4 100644 --- a/DisCatSharp/Entities/Guild/DiscordGuild.cs +++ b/DisCatSharp/Entities/Guild/DiscordGuild.cs @@ -1,3574 +1,3566 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. #pragma warning disable CS0618 using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Globalization; using System.IO; using System.Linq; using System.Threading.Tasks; using DisCatSharp.Enums; using DisCatSharp.Net; using DisCatSharp.Net.Abstractions; using DisCatSharp.Net.Models; using DisCatSharp.Net.Serialization; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace DisCatSharp.Entities { /// /// Represents a Discord guild. /// public class DiscordGuild : SnowflakeObject, IEquatable { /// /// Gets the guild's name. /// [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] public string Name { get; internal set; } /// /// Gets the guild icon's hash. /// [JsonProperty("icon", NullValueHandling = NullValueHandling.Ignore)] public string IconHash { get; internal set; } /// /// Gets the guild icon's url. /// [JsonIgnore] public string IconUrl => !string.IsNullOrWhiteSpace(this.IconHash) ? $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.ICONS}/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.IconHash}.{(this.IconHash.StartsWith("a_") ? "gif" : "png")}?size=1024" : null; /// /// Gets the guild splash's hash. /// [JsonProperty("splash", NullValueHandling = NullValueHandling.Ignore)] public string SplashHash { get; internal set; } /// /// Gets the guild splash's url. /// [JsonIgnore] public string SplashUrl => !string.IsNullOrWhiteSpace(this.SplashHash) ? $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.SPLASHES}/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.SplashHash}.png?size=1024" : null; /// /// Gets the guild discovery splash's hash. /// [JsonProperty("discovery_splash", NullValueHandling = NullValueHandling.Ignore)] public string DiscoverySplashHash { get; internal set; } /// /// Gets the guild discovery splash's url. /// [JsonIgnore] public string DiscoverySplashUrl => !string.IsNullOrWhiteSpace(this.DiscoverySplashHash) ? $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.GUILD_DISCOVERY_SPLASHES}/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.DiscoverySplashHash}.png?size=1024" : null; /// /// Gets the preferred locale of this guild. /// This is used for server discovery and notices from Discord. Defaults to en-US. /// [JsonProperty("preferred_locale", NullValueHandling = NullValueHandling.Ignore)] public string PreferredLocale { get; internal set; } /// /// Gets the ID of the guild's owner. /// [JsonProperty("owner_id", NullValueHandling = NullValueHandling.Ignore)] public ulong OwnerId { get; internal set; } /// /// Gets the guild's owner. /// [JsonIgnore] public DiscordMember Owner => this.Members.TryGetValue(this.OwnerId, out var owner) ? owner : this.Discord.ApiClient.GetGuildMemberAsync(this.Id, this.OwnerId).ConfigureAwait(false).GetAwaiter().GetResult(); /// /// Gets permissions for the user in the guild (does not include channel overrides) /// [JsonProperty("permissions", NullValueHandling = NullValueHandling.Ignore)] public Permissions? Permissions { get; set; } /// /// Gets the guild's voice region ID. /// [JsonProperty("region", NullValueHandling = NullValueHandling.Ignore)] internal string VoiceRegionId { get; set; } /// /// Gets the guild's voice region. /// [JsonIgnore] public DiscordVoiceRegion VoiceRegion => this.Discord.VoiceRegions[this.VoiceRegionId]; /// /// Gets the guild's AFK voice channel ID. /// [JsonProperty("afk_channel_id", NullValueHandling = NullValueHandling.Ignore)] - internal ulong AfkChannelId { get; set; } = 0; + internal ulong AfkChannelId { get; set; } /// /// Gets the guild's AFK voice channel. /// [JsonIgnore] public DiscordChannel AfkChannel => this.GetChannel(this.AfkChannelId); /// /// List of . /// Null if DisCatSharp.ApplicationCommands is not used or no guild commands are registered. /// [JsonIgnore] public ReadOnlyCollection RegisteredApplicationCommands => new(this.InternalRegisteredApplicationCommands); [JsonIgnore] internal List InternalRegisteredApplicationCommands { get; set; } = null; /// /// Gets the guild's AFK timeout. /// [JsonProperty("afk_timeout", NullValueHandling = NullValueHandling.Ignore)] public int AfkTimeout { get; internal set; } /// /// Gets the guild's verification level. /// [JsonProperty("verification_level", NullValueHandling = NullValueHandling.Ignore)] public VerificationLevel VerificationLevel { get; internal set; } /// /// Gets the guild's default notification settings. /// [JsonProperty("default_message_notifications", NullValueHandling = NullValueHandling.Ignore)] public DefaultMessageNotifications DefaultMessageNotifications { get; internal set; } /// /// Gets the guild's explicit content filter settings. /// [JsonProperty("explicit_content_filter")] public ExplicitContentFilter ExplicitContentFilter { get; internal set; } /// /// Gets the guild's nsfw level. /// [JsonProperty("nsfw_level")] public NsfwLevel NsfwLevel { get; internal set; } /// /// Gets the system channel id. /// [JsonProperty("system_channel_id", NullValueHandling = NullValueHandling.Include)] internal ulong? SystemChannelId { get; set; } /// /// Gets the channel where system messages (such as boost and welcome messages) are sent. /// [JsonIgnore] public DiscordChannel SystemChannel => this.SystemChannelId.HasValue ? this.GetChannel(this.SystemChannelId.Value) : null; /// /// Gets the settings for this guild's system channel. /// [JsonProperty("system_channel_flags")] public SystemChannelFlags SystemChannelFlags { get; internal set; } /// /// Gets whether this guild's widget is enabled. /// [JsonProperty("widget_enabled", NullValueHandling = NullValueHandling.Ignore)] public bool? WidgetEnabled { get; internal set; } /// /// Gets the widget channel id. /// [JsonProperty("widget_channel_id", NullValueHandling = NullValueHandling.Ignore)] internal ulong? WidgetChannelId { get; set; } /// /// Gets the widget channel for this guild. /// [JsonIgnore] public DiscordChannel WidgetChannel => this.WidgetChannelId.HasValue ? this.GetChannel(this.WidgetChannelId.Value) : null; /// /// Gets the rules channel id. /// [JsonProperty("rules_channel_id")] internal ulong? RulesChannelId { get; set; } /// /// Gets the rules channel for this guild. /// This is only available if the guild is considered "discoverable". /// [JsonIgnore] public DiscordChannel RulesChannel => this.RulesChannelId.HasValue ? this.GetChannel(this.RulesChannelId.Value) : null; /// /// Gets the public updates channel id. /// [JsonProperty("public_updates_channel_id")] internal ulong? PublicUpdatesChannelId { get; set; } /// /// Gets the public updates channel (where admins and moderators receive messages from Discord) for this guild. /// This is only available if the guild is considered "discoverable". /// [JsonIgnore] public DiscordChannel PublicUpdatesChannel => this.PublicUpdatesChannelId.HasValue ? this.GetChannel(this.PublicUpdatesChannelId.Value) : null; /// /// Gets the application id of this guild if it is bot created. /// [JsonProperty("application_id")] public ulong? ApplicationId { get; internal set; } /// /// Gets a collection of this guild's roles. /// [JsonIgnore] public IReadOnlyDictionary Roles => new ReadOnlyConcurrentDictionary(this.RolesInternal); [JsonProperty("roles", NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] internal ConcurrentDictionary RolesInternal; /// /// Gets a collection of this guild's stickers. /// [JsonIgnore] public IReadOnlyDictionary Stickers => new ReadOnlyConcurrentDictionary(this.StickersInternal); [JsonProperty("stickers", NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] internal ConcurrentDictionary StickersInternal; /// /// Gets a collection of this guild's emojis. /// [JsonIgnore] public IReadOnlyDictionary Emojis => new ReadOnlyConcurrentDictionary(this.EmojisInternal); [JsonProperty("emojis", NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] internal ConcurrentDictionary EmojisInternal; /// /// Gets a collection of this guild's features. /// [JsonProperty("features", NullValueHandling = NullValueHandling.Ignore)] public IReadOnlyList RawFeatures { get; internal set; } /// /// Gets the guild's features. /// [JsonIgnore] public GuildFeatures Features => new(this); /// /// Gets the required multi-factor authentication level for this guild. /// [JsonProperty("mfa_level", NullValueHandling = NullValueHandling.Ignore)] public MfaLevel MfaLevel { get; internal set; } /// /// Gets this guild's join date. /// [JsonProperty("joined_at", NullValueHandling = NullValueHandling.Ignore)] public DateTimeOffset JoinedAt { get; internal set; } /// /// Gets whether this guild is considered to be a large guild. /// [JsonProperty("large", NullValueHandling = NullValueHandling.Ignore)] public bool IsLarge { get; internal set; } /// /// Gets whether this guild is unavailable. /// [JsonProperty("unavailable", NullValueHandling = NullValueHandling.Ignore)] public bool IsUnavailable { get; internal set; } /// /// Gets the total number of members in this guild. /// [JsonProperty("member_count", NullValueHandling = NullValueHandling.Ignore)] public int MemberCount { get; internal set; } /// /// Gets the maximum amount of members allowed for this guild. /// [JsonProperty("max_members")] public int? MaxMembers { get; internal set; } /// /// Gets the maximum amount of presences allowed for this guild. /// [JsonProperty("max_presences")] public int? MaxPresences { get; internal set; } #pragma warning disable CS1734 /// /// Gets the approximate number of members in this guild, when using and having set to true. /// [JsonProperty("approximate_member_count", NullValueHandling = NullValueHandling.Ignore)] public int? ApproximateMemberCount { get; internal set; } /// /// Gets the approximate number of presences in this guild, when using and having set to true. /// [JsonProperty("approximate_presence_count", NullValueHandling = NullValueHandling.Ignore)] public int? ApproximatePresenceCount { get; internal set; } #pragma warning restore CS1734 /// /// Gets the maximum amount of users allowed per video channel. /// [JsonProperty("max_video_channel_users", NullValueHandling = NullValueHandling.Ignore)] public int? MaxVideoChannelUsers { get; internal set; } /// /// Gets a dictionary of all the voice states for this guilds. The key for this dictionary is the ID of the user /// the voice state corresponds to. /// [JsonIgnore] public IReadOnlyDictionary VoiceStates => new ReadOnlyConcurrentDictionary(this.VoiceStatesInternal); [JsonProperty("voice_states", NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] internal ConcurrentDictionary VoiceStatesInternal; /// /// Gets a dictionary of all the members that belong to this guild. The dictionary's key is the member ID. /// [JsonIgnore] // TODO overhead of => vs Lazy? it's a struct public IReadOnlyDictionary Members => new ReadOnlyConcurrentDictionary(this.MembersInternal); [JsonProperty("members", NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] internal ConcurrentDictionary MembersInternal; /// /// Gets a dictionary of all the channels associated with this guild. The dictionary's key is the channel ID. /// [JsonIgnore] public IReadOnlyDictionary Channels => new ReadOnlyConcurrentDictionary(this.ChannelsInternal); [JsonProperty("channels", NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] internal ConcurrentDictionary ChannelsInternal; internal ConcurrentDictionary Invites; /// /// Gets a dictionary of all the active threads associated with this guild the user has permission to view. The dictionary's key is the channel ID. /// [JsonIgnore] public IReadOnlyDictionary Threads { get; internal set; } [JsonProperty("threads", NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] internal ConcurrentDictionary ThreadsInternal = new(); /// /// Gets a dictionary of all active stage instances. The dictionary's key is the stage ID. /// [JsonIgnore] public IReadOnlyDictionary StageInstances { get; internal set; } [JsonProperty("stage_instances", NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] internal ConcurrentDictionary StageInstancesInternal = new(); /// /// Gets a dictionary of all scheduled events. /// [JsonIgnore] public IReadOnlyDictionary ScheduledEvents { get; internal set; } [JsonProperty("guild_scheduled_events", NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] internal ConcurrentDictionary ScheduledEventsInternal = new(); /// /// Gets the guild member for current user. /// [JsonIgnore] public DiscordMember CurrentMember => this._currentMemberLazy.Value; [JsonIgnore] private readonly Lazy _currentMemberLazy; /// /// Gets the @everyone role for this guild. /// [JsonIgnore] public DiscordRole EveryoneRole => this.GetRole(this.Id); [JsonIgnore] internal bool IsOwnerInternal; /// /// Gets whether the current user is the guild's owner. /// [JsonProperty("owner", NullValueHandling = NullValueHandling.Ignore)] public bool IsOwner { get => this.IsOwnerInternal || this.OwnerId == this.Discord.CurrentUser.Id; internal set => this.IsOwnerInternal = value; } /// /// Gets the vanity URL code for this guild, when applicable. /// [JsonProperty("vanity_url_code")] public string VanityUrlCode { get; internal set; } /// /// Gets the guild description, when applicable. /// [JsonProperty("description")] public string Description { get; internal set; } /// /// Gets this guild's banner hash, when applicable. /// [JsonProperty("banner")] public string BannerHash { get; internal set; } /// /// Gets this guild's banner in url form. /// [JsonIgnore] public string BannerUrl => !string.IsNullOrWhiteSpace(this.BannerHash) ? $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Uri}{Endpoints.BANNERS}/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.BannerHash}.{(this.BannerHash.StartsWith("a_") ? "gif" : "png")}" : null; /// /// Whether this guild has the community feature enabled. /// [JsonIgnore] public bool IsCommunity => this.Features.HasCommunityEnabled; /// /// Whether this guild has enabled the welcome screen. /// [JsonIgnore] public bool HasWelcomeScreen => this.Features.HasWelcomeScreenEnabled; /// /// Whether this guild has enabled membership screening. /// [JsonIgnore] public bool HasMemberVerificationGate => this.Features.HasMembershipScreeningEnabled; /// /// Gets this guild's premium tier (Nitro boosting). /// [JsonProperty("premium_tier")] public PremiumTier PremiumTier { get; internal set; } /// /// Gets the amount of members that boosted this guild. /// [JsonProperty("premium_subscription_count", NullValueHandling = NullValueHandling.Ignore)] public int? PremiumSubscriptionCount { get; internal set; } /// /// Whether the premium progress bar is enabled. /// [JsonProperty("premium_progress_bar_enabled", NullValueHandling = NullValueHandling.Ignore)] public bool PremiumProgressBarEnabled { get; internal set; } /// /// Gets whether this guild is designated as NSFW. /// [JsonProperty("nsfw", NullValueHandling = NullValueHandling.Ignore)] public bool IsNsfw { get; internal set; } /// /// Gets this guild's hub type, if applicable. /// [JsonProperty("hub_type", NullValueHandling = NullValueHandling.Ignore)] public HubType HubType { get; internal set; } /// /// Gets a dictionary of all by position ordered channels associated with this guild. The dictionary's key is the channel ID. /// [JsonIgnore] public IReadOnlyDictionary OrderedChannels => new ReadOnlyDictionary(this.InternalSortChannels()); /// /// Sorts the channels. /// private Dictionary InternalSortChannels() { Dictionary keyValuePairs = new(); var ochannels = this.GetOrderedChannels(); foreach (var ochan in ochannels) { if (ochan.Key != 0) keyValuePairs.Add(ochan.Key, this.GetChannel(ochan.Key)); foreach (var chan in ochan.Value) keyValuePairs.Add(chan.Id, chan); } return keyValuePairs; } /// /// Gets an ordered list out of the channel cache. /// Returns a Dictionary where the key is an ulong and can be mapped to s. /// Ignore the 0 key here, because that indicates that this is the "has no category" list. /// Each value contains a ordered list of text/news and voice/stage channels as . /// /// A ordered list of categories with its channels public Dictionary> GetOrderedChannels() { IReadOnlyList rawChannels = this.ChannelsInternal.Values.ToList(); Dictionary> orderedChannels = new(); orderedChannels.Add(0, new List()); foreach (var channel in rawChannels.Where(c => c.Type == ChannelType.Category).OrderBy(c => c.Position)) { orderedChannels.Add(channel.Id, new List()); } foreach (var channel in rawChannels.Where(c => c.ParentId.HasValue && (c.Type == ChannelType.Text || c.Type == ChannelType.News)).OrderBy(c => c.Position)) { orderedChannels[channel.ParentId.Value].Add(channel); } foreach (var channel in rawChannels.Where(c => c.ParentId.HasValue && (c.Type == ChannelType.Voice || c.Type == ChannelType.Stage)).OrderBy(c => c.Position)) { orderedChannels[channel.ParentId.Value].Add(channel); } foreach (var channel in rawChannels.Where(c => !c.ParentId.HasValue && c.Type != ChannelType.Category && (c.Type == ChannelType.Text || c.Type == ChannelType.News)).OrderBy(c => c.Position)) { orderedChannels[0].Add(channel); } foreach (var channel in rawChannels.Where(c => !c.ParentId.HasValue && c.Type != ChannelType.Category && (c.Type == ChannelType.Voice || c.Type == ChannelType.Stage)).OrderBy(c => c.Position)) { orderedChannels[0].Add(channel); } return orderedChannels; } /// /// Gets an ordered list. /// Returns a Dictionary where the key is an ulong and can be mapped to s. /// Ignore the 0 key here, because that indicates that this is the "has no category" list. /// Each value contains a ordered list of text/news and voice/stage channels as . /// /// A ordered list of categories with its channels public async Task>> GetOrderedChannelsAsync() { var rawChannels = await this.Discord.ApiClient.GetGuildChannelsAsync(this.Id); Dictionary> orderedChannels = new(); orderedChannels.Add(0, new List()); foreach (var channel in rawChannels.Where(c => c.Type == ChannelType.Category).OrderBy(c => c.Position)) { orderedChannels.Add(channel.Id, new List()); } foreach (var channel in rawChannels.Where(c => c.ParentId.HasValue && (c.Type == ChannelType.Text || c.Type == ChannelType.News)).OrderBy(c => c.Position)) { orderedChannels[channel.ParentId.Value].Add(channel); } foreach (var channel in rawChannels.Where(c => c.ParentId.HasValue && (c.Type == ChannelType.Voice || c.Type == ChannelType.Stage)).OrderBy(c => c.Position)) { orderedChannels[channel.ParentId.Value].Add(channel); } foreach (var channel in rawChannels.Where(c => !c.ParentId.HasValue && c.Type != ChannelType.Category && (c.Type == ChannelType.Text || c.Type == ChannelType.News)).OrderBy(c => c.Position)) { orderedChannels[0].Add(channel); } foreach (var channel in rawChannels.Where(c => !c.ParentId.HasValue && c.Type != ChannelType.Category && (c.Type == ChannelType.Voice || c.Type == ChannelType.Stage)).OrderBy(c => c.Position)) { orderedChannels[0].Add(channel); } return orderedChannels; } /// /// Whether it is synced. /// [JsonIgnore] internal bool IsSynced { get; set; } /// /// Initializes a new instance of the class. /// internal DiscordGuild() { - this._currentMemberLazy = new Lazy(() => (this.MembersInternal != null && this.MembersInternal.TryGetValue(this.Discord.CurrentUser.Id, out var member)) ? member : null); + this._currentMemberLazy = new Lazy(() => this.MembersInternal != null && this.MembersInternal.TryGetValue(this.Discord.CurrentUser.Id, out var member) ? member : null); this.Invites = new ConcurrentDictionary(); this.Threads = new ReadOnlyConcurrentDictionary(this.ThreadsInternal); this.StageInstances = new ReadOnlyConcurrentDictionary(this.StageInstancesInternal); this.ScheduledEvents = new ReadOnlyConcurrentDictionary(this.ScheduledEventsInternal); } #region Guild Methods /// /// Searches the current guild for members who's display name start with the specified name. /// /// The name to search for. /// The maximum amount of members to return. Max 1000. Defaults to 1. /// The members found, if any. public Task> SearchMembersAsync(string name, int? limit = 1) => this.Discord.ApiClient.SearchMembersAsync(this.Id, name, limit); /// /// Adds a new member to this guild /// /// User to add /// User's access token (OAuth2) /// new nickname /// new roles /// whether this user has to be muted /// whether this user has to be deafened /// /// Thrown when the client does not have the permission. /// Thrown when the or is not found. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task AddMemberAsync(DiscordUser user, string accessToken, string nickname = null, IEnumerable roles = null, bool muted = false, bool deaf = false) => this.Discord.ApiClient.AddGuildMemberAsync(this.Id, user.Id, accessToken, nickname, roles, muted, deaf); /// /// Deletes this guild. Requires the caller to be the owner of the guild. /// /// /// Thrown when the client is not the owner of the guild. /// Thrown when Discord is unable to process the request. public Task DeleteAsync() => this.Discord.ApiClient.DeleteGuildAsync(this.Id); /// /// Modifies this guild. /// /// Action to perform on this guild.. /// The modified guild object. /// Thrown when the client does not have the permission. /// Thrown when the guild does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task ModifyAsync(Action action) { var mdl = new GuildEditModel(); action(mdl); var afkChannelId = Optional.FromNoValue(); if (mdl.AfkChannel.HasValue && mdl.AfkChannel.Value.Type != ChannelType.Voice && mdl.AfkChannel.Value != null) throw new ArgumentException("AFK channel needs to be a voice channel."); else if (mdl.AfkChannel.HasValue && mdl.AfkChannel.Value != null) afkChannelId = mdl.AfkChannel.Value.Id; else if (mdl.AfkChannel.HasValue) afkChannelId = null; var rulesChannelId = Optional.FromNoValue(); if (mdl.RulesChannel.HasValue && mdl.RulesChannel.Value != null && mdl.RulesChannel.Value.Type != ChannelType.Text && mdl.RulesChannel.Value.Type != ChannelType.News) throw new ArgumentException("Rules channel needs to be a text channel."); else if (mdl.RulesChannel.HasValue && mdl.RulesChannel.Value != null) rulesChannelId = mdl.RulesChannel.Value.Id; else if (mdl.RulesChannel.HasValue) rulesChannelId = null; var publicUpdatesChannelId = Optional.FromNoValue(); if (mdl.PublicUpdatesChannel.HasValue && mdl.PublicUpdatesChannel.Value != null && mdl.PublicUpdatesChannel.Value.Type != ChannelType.Text && mdl.PublicUpdatesChannel.Value.Type != ChannelType.News) throw new ArgumentException("Public updates channel needs to be a text channel."); else if (mdl.PublicUpdatesChannel.HasValue && mdl.PublicUpdatesChannel.Value != null) publicUpdatesChannelId = mdl.PublicUpdatesChannel.Value.Id; else if (mdl.PublicUpdatesChannel.HasValue) publicUpdatesChannelId = null; var systemChannelId = Optional.FromNoValue(); if (mdl.SystemChannel.HasValue && mdl.SystemChannel.Value != null && mdl.SystemChannel.Value.Type != ChannelType.Text && mdl.SystemChannel.Value.Type != ChannelType.News) throw new ArgumentException("Public updates channel needs to be a text channel."); else if (mdl.SystemChannel.HasValue && mdl.SystemChannel.Value != null) systemChannelId = mdl.SystemChannel.Value.Id; else if (mdl.SystemChannel.HasValue) systemChannelId = null; var iconb64 = Optional.FromNoValue(); if (mdl.Icon.HasValue && mdl.Icon.Value != null) using (var imgtool = new ImageTool(mdl.Icon.Value)) iconb64 = imgtool.GetBase64(); else if (mdl.Icon.HasValue) iconb64 = null; var splashb64 = Optional.FromNoValue(); if (mdl.Splash.HasValue && mdl.Splash.Value != null) using (var imgtool = new ImageTool(mdl.Splash.Value)) splashb64 = imgtool.GetBase64(); else if (mdl.Splash.HasValue) splashb64 = null; var bannerb64 = Optional.FromNoValue(); if (mdl.Banner.HasValue && mdl.Banner.Value != null) using (var imgtool = new ImageTool(mdl.Banner.Value)) bannerb64 = imgtool.GetBase64(); else if (mdl.Banner.HasValue) bannerb64 = null; var discoverySplash64 = Optional.FromNoValue(); if (mdl.DiscoverySplash.HasValue && mdl.DiscoverySplash.Value != null) using (var imgtool = new ImageTool(mdl.DiscoverySplash.Value)) discoverySplash64 = imgtool.GetBase64(); else if (mdl.DiscoverySplash.HasValue) discoverySplash64 = null; var description = Optional.FromNoValue(); if (mdl.Description.HasValue && mdl.Description.Value != null) description = mdl.Description; else if (mdl.Description.HasValue) description = null; return await this.Discord.ApiClient.ModifyGuildAsync(this.Id, mdl.Name, mdl.VerificationLevel, mdl.DefaultMessageNotifications, mdl.MfaLevel, mdl.ExplicitContentFilter, afkChannelId, mdl.AfkTimeout, iconb64, mdl.Owner.IfPresent(e => e.Id), splashb64, systemChannelId, mdl.SystemChannelFlags, publicUpdatesChannelId, rulesChannelId, description, bannerb64, discoverySplash64, mdl.PreferredLocale, mdl.PremiumProgressBarEnabled, mdl.AuditLogReason).ConfigureAwait(false); } /// /// Modifies the community settings async. /// This sets if not highest and . /// /// If true, enable . /// The rules channel. /// The public updates channel. /// The preferred locale. Defaults to en-US. /// The description. /// The default message notifications. Defaults to /// The auditlog reason. /// Thrown when the client does not have the permission. /// Thrown when the guild does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task ModifyCommunitySettingsAsync(bool enabled, DiscordChannel rulesChannel = null, DiscordChannel publicUpdatesChannel = null, string preferredLocale = "en-US", string description = null, DefaultMessageNotifications defaultMessageNotifications = DefaultMessageNotifications.MentionsOnly, string reason = null) { var verificationLevel = this.VerificationLevel; if (this.VerificationLevel != VerificationLevel.Highest) { verificationLevel = VerificationLevel.High; } var explicitContentFilter = ExplicitContentFilter.AllMembers; var rulesChannelId = Optional.FromNoValue(); if (rulesChannel != null && rulesChannel.Type != ChannelType.Text && rulesChannel.Type != ChannelType.News) throw new ArgumentException("Rules channel needs to be a text channel."); else if (rulesChannel != null) rulesChannelId = rulesChannel.Id; else if (rulesChannel == null) rulesChannelId = null; var publicUpdatesChannelId = Optional.FromNoValue(); if (publicUpdatesChannel != null && publicUpdatesChannel.Type != ChannelType.Text && publicUpdatesChannel.Type != ChannelType.News) throw new ArgumentException("Public updates channel needs to be a text channel."); else if (publicUpdatesChannel != null) publicUpdatesChannelId = publicUpdatesChannel.Id; else if (publicUpdatesChannel == null) publicUpdatesChannelId = null; List features = new(); var rfeatures = this.RawFeatures.ToList(); if (this.RawFeatures.Contains("COMMUNITY") && enabled) { features = rfeatures; } else if (!this.RawFeatures.Contains("COMMUNITY") && enabled) { rfeatures.Add("COMMUNITY"); features = rfeatures; } else if (this.RawFeatures.Contains("COMMUNITY") && !enabled) { rfeatures.Remove("COMMUNITY"); features = rfeatures; } else if (!this.RawFeatures.Contains("COMMUNITY") && !enabled) { features = rfeatures; } return await this.Discord.ApiClient.ModifyGuildCommunitySettingsAsync(this.Id, features, rulesChannelId, publicUpdatesChannelId, preferredLocale, description, defaultMessageNotifications, explicitContentFilter, verificationLevel, reason).ConfigureAwait(false); } /// /// Timeout a specified member in this guild. /// /// Member to timeout. /// The datetime offset to time out the user. Up to 28 days. /// Reason for audit logs. /// /// Thrown when the client does not have the permission. /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task TimeoutAsync(ulong memberId, DateTimeOffset until, string reason = null) => until.Subtract(DateTimeOffset.UtcNow).Days > 28 ? throw new ArgumentException("Timeout can not be longer than 28 days") : this.Discord.ApiClient.ModifyTimeoutAsync(this.Id, memberId, until, reason); /// /// Timeout a specified member in this guild. /// /// Member to timeout. /// The timespan to time out the user. Up to 28 days. /// Reason for audit logs. /// /// Thrown when the client does not have the permission. /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task TimeoutAsync(ulong memberId, TimeSpan until, string reason = null) => this.TimeoutAsync(memberId, DateTimeOffset.UtcNow + until, reason); /// /// Timeout a specified member in this guild. /// /// Member to timeout. /// The datetime to time out the user. Up to 28 days. /// Reason for audit logs. /// /// Thrown when the client does not have the permission. /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task TimeoutAsync(ulong memberId, DateTime until, string reason = null) => this.TimeoutAsync(memberId, until.ToUniversalTime() - DateTime.UtcNow, reason); /// /// Removes the timeout from a specified member in this guild. /// /// Member to remove the timeout from. /// Reason for audit logs. /// /// Thrown when the client does not have the permission. /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task RemoveTimeoutAsync(ulong memberId, string reason = null) => this.Discord.ApiClient.ModifyTimeoutAsync(this.Id, memberId, null, reason); /// /// Bans a specified member from this guild. /// /// Member to ban. /// How many days to remove messages from. /// Reason for audit logs. /// /// Thrown when the client does not have the permission. /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task BanMemberAsync(DiscordMember member, int deleteMessageDays = 0, string reason = null) => this.Discord.ApiClient.CreateGuildBanAsync(this.Id, member.Id, deleteMessageDays, reason); /// /// Bans a specified user by ID. This doesn't require the user to be in this guild. /// /// ID of the user to ban. /// How many days to remove messages from. /// Reason for audit logs. /// /// Thrown when the client does not have the permission. /// Thrown when the member does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task BanMemberAsync(ulong userId, int deleteMessageDays = 0, string reason = null) => this.Discord.ApiClient.CreateGuildBanAsync(this.Id, userId, deleteMessageDays, reason); /// /// Unbans a user from this guild. /// /// User to unban. /// Reason for audit logs. /// /// Thrown when the client does not have the permission. /// Thrown when the user does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task UnbanMemberAsync(DiscordUser user, string reason = null) => this.Discord.ApiClient.RemoveGuildBanAsync(this.Id, user.Id, reason); /// /// Unbans a user by ID. /// /// ID of the user to unban. /// Reason for audit logs. /// /// Thrown when the client does not have the permission. /// Thrown when the user does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task UnbanMemberAsync(ulong userId, string reason = null) => this.Discord.ApiClient.RemoveGuildBanAsync(this.Id, userId, reason); /// /// Leaves this guild. /// /// /// Thrown when Discord is unable to process the request. public Task LeaveAsync() => this.Discord.ApiClient.LeaveGuildAsync(this.Id); /// /// Gets the bans for this guild. /// /// Collection of bans in this guild. /// Thrown when the client does not have the permission. /// Thrown when Discord is unable to process the request. public Task> GetBansAsync() => this.Discord.ApiClient.GetGuildBansAsync(this.Id); /// /// Gets a ban for a specific user. /// /// The Id of the user to get the ban for. /// Thrown when the specified user is not banned. /// The requested ban object. public Task GetBanAsync(ulong userId) => this.Discord.ApiClient.GetGuildBanAsync(this.Id, userId); /// /// Gets a ban for a specific user. /// /// The user to get the ban for. /// Thrown when the specified user is not banned. /// The requested ban object. public Task GetBanAsync(DiscordUser user) => this.GetBanAsync(user.Id); #region Sheduled Events /// /// Creates a scheduled event. /// /// The name. /// The scheduled start time. /// The scheduled end time. /// The channel. /// The metadata. /// The description. /// The type. /// The reason. /// A scheduled event. /// Thrown when the guild does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task CreateScheduledEventAsync(string name, DateTimeOffset scheduledStartTime, DateTimeOffset? scheduledEndTime = null, DiscordChannel channel = null, DiscordScheduledEventEntityMetadata metadata = null, string description = null, ScheduledEventEntityType type = ScheduledEventEntityType.StageInstance, string reason = null) => await this.Discord.ApiClient.CreateGuildScheduledEventAsync(this.Id, type == ScheduledEventEntityType.External ? null : channel?.Id, type == ScheduledEventEntityType.External ? metadata : null, name, scheduledStartTime, scheduledEndTime.HasValue && type == ScheduledEventEntityType.External ? scheduledEndTime.Value : null, description, type, reason); /// /// Creates a scheduled event with type . /// /// The name. /// The scheduled start time. /// The scheduled end time. /// The location of the external event. /// The description. /// The reason. /// A scheduled event. /// Thrown when the guild does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task CreateExternalScheduledEventAsync(string name, DateTimeOffset scheduledStartTime, DateTimeOffset scheduledEndTime, string location, string description = null, string reason = null) => await this.Discord.ApiClient.CreateGuildScheduledEventAsync(this.Id, null, new DiscordScheduledEventEntityMetadata(location), name, scheduledStartTime, scheduledEndTime, description, ScheduledEventEntityType.External, reason); /// /// Gets a specific scheduled events. /// /// The Id of the event to get. /// Whether to include user count. /// A scheduled event. /// Thrown when the guild does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task GetScheduledEventAsync(ulong scheduledEventId, bool? withUserCount = null) => this.ScheduledEventsInternal.TryGetValue(scheduledEventId, out var ev) ? ev : await this.Discord.ApiClient.GetGuildScheduledEventAsync(this.Id, scheduledEventId, withUserCount); /// /// Gets a specific scheduled events. /// /// The event to get. /// Whether to include user count. /// A sheduled event. /// Thrown when the guild does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task GetScheduledEventAsync(DiscordScheduledEvent scheduledEvent, bool? withUserCount = null) => await this.GetScheduledEventAsync(scheduledEvent.Id, withUserCount); /// /// Gets the guilds scheduled events. /// /// Whether to include user count. /// A list of the guilds scheduled events. /// Thrown when the guild does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public async Task> GetScheduledEventsAsync(bool? withUserCount = null) => await this.Discord.ApiClient.ListGuildScheduledEventsAsync(this.Id, withUserCount); #endregion /// /// Creates a new text channel in this guild. /// /// Name of the new channel. /// Category to put this channel in. /// Topic of the channel. /// Permission overwrites for this channel. /// Whether the channel is to be flagged as not safe for work. /// Reason for audit logs. /// Slow mode timeout for users. /// The newly-created channel. /// Thrown when the client does not have the permission. /// Thrown when the guild does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task CreateTextChannelAsync(string name, DiscordChannel parent = null, Optional topic = default, IEnumerable overwrites = null, bool? nsfw = null, Optional perUserRateLimit = default, string reason = null) => this.CreateChannelAsync(name, ChannelType.Text, parent, topic, null, null, overwrites, nsfw, perUserRateLimit, null, reason); /// /// Creates a new channel category in this guild. /// /// Name of the new category. /// Permission overwrites for this category. /// Reason for audit logs. /// The newly-created channel category. /// Thrown when the client does not have the permission. /// Thrown when the guild does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task CreateChannelCategoryAsync(string name, IEnumerable overwrites = null, string reason = null) => this.CreateChannelAsync(name, ChannelType.Category, null, Optional.FromNoValue(), null, null, overwrites, null, Optional.FromNoValue(), null, reason); /// /// Creates a new stage channel in this guild. /// /// Name of the new stage channel. /// Permission overwrites for this stage channel. /// Reason for audit logs. /// The newly-created stage channel. /// Thrown when the client does not have the . /// Thrown when the guild does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. /// Thrown when the guilds has not enabled community. public Task CreateStageChannelAsync(string name, IEnumerable overwrites = null, string reason = null) => this.Features.HasCommunityEnabled ? this.CreateChannelAsync(name, ChannelType.Stage, null, Optional.FromNoValue(), null, null, overwrites, null, Optional.FromNoValue(), null, reason) : throw new NotSupportedException("Guild has not enabled community. Can not create a stage channel."); /// /// Creates a new news channel in this guild. /// /// Name of the new stage channel. /// Permission overwrites for this news channel. /// Reason for audit logs. /// The newly-created news channel. /// Thrown when the client does not have the . /// Thrown when the guild does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. /// Thrown when the guilds has not enabled community. public Task CreateNewsChannelAsync(string name, IEnumerable overwrites = null, string reason = null) => this.Features.HasCommunityEnabled ? this.CreateChannelAsync(name, ChannelType.News, null, Optional.FromNoValue(), null, null, overwrites, null, Optional.FromNoValue(), null, reason) : throw new NotSupportedException("Guild has not enabled community. Can not create a news channel."); /// /// Creates a new voice channel in this guild. /// /// Name of the new channel. /// Category to put this channel in. /// Bitrate of the channel. /// Maximum number of users in the channel. /// Permission overwrites for this channel. /// Video quality mode of the channel. /// Reason for audit logs. /// The newly-created channel. /// Thrown when the client does not have the permission. /// Thrown when the guild does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task CreateVoiceChannelAsync(string name, DiscordChannel parent = null, int? bitrate = null, int? userLimit = null, IEnumerable overwrites = null, VideoQualityMode? qualityMode = null, string reason = null) => this.CreateChannelAsync(name, ChannelType.Voice, parent, Optional.FromNoValue(), bitrate, userLimit, overwrites, null, Optional.FromNoValue(), qualityMode, reason); /// /// Creates a new channel in this guild. /// /// Name of the new channel. /// Type of the new channel. /// Category to put this channel in. /// Topic of the channel. /// Bitrate of the channel. Applies to voice only. /// Maximum number of users in the channel. Applies to voice only. /// Permission overwrites for this channel. /// Whether the channel is to be flagged as not safe for work. Applies to text only. /// Slow mode timeout for users. /// Video quality mode of the channel. Applies to voice only. /// Reason for audit logs. /// The newly-created channel. /// Thrown when the client does not have the permission. /// Thrown when the guild does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. - public Task CreateChannelAsync(string name, ChannelType type, DiscordChannel parent = null, Optional topic = default, int? bitrate = null, int? userLimit = null, IEnumerable overwrites = null, bool? nsfw = null, Optional perUserRateLimit = default, VideoQualityMode? qualityMode = null, string reason = null) - { + public Task CreateChannelAsync(string name, ChannelType type, DiscordChannel parent = null, Optional topic = default, int? bitrate = null, int? userLimit = null, IEnumerable overwrites = null, bool? nsfw = null, Optional perUserRateLimit = default, VideoQualityMode? qualityMode = null, string reason = null) => // technically you can create news/store channels but not always - return type != ChannelType.Text && type != ChannelType.Voice && type != ChannelType.Category && type != ChannelType.News && type != ChannelType.Store && type != ChannelType.Stage + type != ChannelType.Text && type != ChannelType.Voice && type != ChannelType.Category && type != ChannelType.News && type != ChannelType.Store && type != ChannelType.Stage ? throw new ArgumentException("Channel type must be text, voice, stage, or category.", nameof(type)) : type == ChannelType.Category && parent != null - ? throw new ArgumentException("Cannot specify parent of a channel category.", nameof(parent)) - : this.Discord.ApiClient.CreateGuildChannelAsync(this.Id, name, type, parent?.Id, topic, bitrate, userLimit, overwrites, nsfw, perUserRateLimit, qualityMode, reason); - } + ? throw new ArgumentException("Cannot specify parent of a channel category.", nameof(parent)) + : this.Discord.ApiClient.CreateGuildChannelAsync(this.Id, name, type, parent?.Id, topic, bitrate, userLimit, overwrites, nsfw, perUserRateLimit, qualityMode, reason); /// /// Gets active threads. Can contain more threads. /// If the result's value 'HasMore' is true, you need to recall this function to get older threads. /// /// Thrown when the thread does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task GetActiveThreadsAsync() => this.Discord.ApiClient.GetActiveThreadsAsync(this.Id); // this is to commemorate the Great DAPI Channel Massacre of 2017-11-19. /// /// Deletes all channels in this guild. /// Note that this is irreversible. Use carefully! /// /// public Task DeleteAllChannelsAsync() { var tasks = this.Channels.Values.Select(xc => xc.DeleteAsync()); return Task.WhenAll(tasks); } /// /// Estimates the number of users to be pruned. /// /// Minimum number of inactivity days required for users to be pruned. Defaults to 7. /// The roles to be included in the prune. /// Number of users that will be pruned. /// Thrown when the client does not have the permission. /// Thrown when the guild does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task GetPruneCountAsync(int days = 7, IEnumerable includedRoles = null) { if (includedRoles != null) { includedRoles = includedRoles.Where(r => r != null); var roleCount = includedRoles.Count(); var roleArr = includedRoles.ToArray(); var rawRoleIds = new List(); for (var i = 0; i < roleCount; i++) { if (this.RolesInternal.ContainsKey(roleArr[i].Id)) rawRoleIds.Add(roleArr[i].Id); } return this.Discord.ApiClient.GetGuildPruneCountAsync(this.Id, days, rawRoleIds); } return this.Discord.ApiClient.GetGuildPruneCountAsync(this.Id, days, null); } /// /// Prunes inactive users from this guild. /// /// Minimum number of inactivity days required for users to be pruned. Defaults to 7. /// Whether to return the prune count after this method completes. This is discouraged for larger guilds. /// The roles to be included in the prune. /// Reason for audit logs. /// Number of users pruned. /// Thrown when the client does not have the permission. /// Thrown when the guild does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task PruneAsync(int days = 7, bool computePruneCount = true, IEnumerable includedRoles = null, string reason = null) { if (includedRoles != null) { includedRoles = includedRoles.Where(r => r != null); var roleCount = includedRoles.Count(); var roleArr = includedRoles.ToArray(); var rawRoleIds = new List(); for (var i = 0; i < roleCount; i++) { if (this.RolesInternal.ContainsKey(roleArr[i].Id)) rawRoleIds.Add(roleArr[i].Id); } return this.Discord.ApiClient.BeginGuildPruneAsync(this.Id, days, computePruneCount, rawRoleIds, reason); } return this.Discord.ApiClient.BeginGuildPruneAsync(this.Id, days, computePruneCount, null, reason); } /// /// Gets integrations attached to this guild. /// /// Collection of integrations attached to this guild. /// Thrown when the client does not have the permission. /// Thrown when the guild does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task> GetIntegrationsAsync() => this.Discord.ApiClient.GetGuildIntegrationsAsync(this.Id); /// /// Attaches an integration from current user to this guild. /// /// Integration to attach. /// The integration after being attached to the guild. /// Thrown when the client does not have the permission. /// Thrown when the guild does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task AttachUserIntegrationAsync(DiscordIntegration integration) => this.Discord.ApiClient.CreateGuildIntegrationAsync(this.Id, integration.Type, integration.Id); /// /// Modifies an integration in this guild. /// /// Integration to modify. /// Number of days after which the integration expires. /// Length of grace period which allows for renewing the integration. /// Whether emotes should be synced from this integration. /// The modified integration. /// Thrown when the client does not have the permission. /// Thrown when the guild does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task ModifyIntegrationAsync(DiscordIntegration integration, int expireBehaviour, int expireGracePeriod, bool enableEmoticons) => this.Discord.ApiClient.ModifyGuildIntegrationAsync(this.Id, integration.Id, expireBehaviour, expireGracePeriod, enableEmoticons); /// /// Removes an integration from this guild. /// /// Integration to remove. /// /// Thrown when the client does not have the permission. /// Thrown when the guild does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task DeleteIntegrationAsync(DiscordIntegration integration) => this.Discord.ApiClient.DeleteGuildIntegrationAsync(this.Id, integration); /// /// Forces re-synchronization of an integration for this guild. /// /// Integration to synchronize. /// /// Thrown when the client does not have the permission. /// Thrown when the guild does not exist. /// Thrown when an invalid parameter was provided. /// Thrown when Discord is unable to process the request. public Task SyncIntegrationAsync(DiscordIntegration integration) => this.Discord.ApiClient.SyncGuildIntegrationAsync(this.Id, integration.Id); /// /// Gets the voice regions for this guild. /// /// Voice regions available for this guild. /// Thrown when Discord is unable to process the request. public async Task> ListVoiceRegionsAsync() { var vrs = await this.Discord.ApiClient.GetGuildVoiceRegionsAsync(this.Id).ConfigureAwait(false); foreach (var xvr in vrs) this.Discord.InternalVoiceRegions.TryAdd(xvr.Id, xvr); return vrs; } /// /// Gets an invite from this guild from an invite code. /// /// The invite code /// An invite, or null if not in cache. public DiscordInvite GetInvite(string code) => this.Invites.TryGetValue(code, out var invite) ? invite : null; /// /// Gets all the invites created for all the channels in this guild. /// /// A collection of invites. /// Thrown when Discord is unable to process the request. public async Task> GetInvitesAsync() { var res = await this.Discord.ApiClient.GetGuildInvitesAsync(this.Id).ConfigureAwait(false); var intents = this.Discord.Configuration.Intents; if (!intents.HasIntent(DiscordIntents.GuildInvites)) { for (var i = 0; i < res.Count; i++) this.Invites[res[i].Code] = res[i]; } return res; } /// /// Gets the vanity invite for this guild. /// /// A partial vanity invite. /// Thrown when the client does not have the permission. /// Thrown when Discord is unable to process the request. public Task GetVanityInviteAsync() => this.Discord.ApiClient.GetGuildVanityUrlAsync(this.Id); /// /// Gets all the webhooks created for all the channels in this guild. /// /// A collection of webhooks this guild has. /// Thrown when the client does not have the permission. /// Thrown when Discord is unable to process the request. public Task> GetWebhooksAsync() => this.Discord.ApiClient.GetGuildWebhooksAsync(this.Id); /// /// Gets this guild's widget image. /// /// The format of the widget. /// The URL of the widget image. public string GetWidgetImage(WidgetType bannerType = WidgetType.Shield) { var param = bannerType switch { WidgetType.Banner1 => "banner1", WidgetType.Banner2 => "banner2", WidgetType.Banner3 => "banner3", WidgetType.Banner4 => "banner4", _ => "shield", }; return $"{Endpoints.BASE_URI}{Endpoints.GUILDS}/{this.Id}{Endpoints.WIDGET_PNG}?style={param}"; } /// /// Gets a member of this guild by their user ID. /// /// ID of the member to get. /// The requested member. /// Thrown when Discord is unable to process the request. public async Task GetMemberAsync(ulong userId) { if (this.MembersInternal != null && this.MembersInternal.TryGetValue(userId, out var mbr)) return mbr; mbr = await this.Discord.ApiClient.GetGuildMemberAsync(this.Id, userId).ConfigureAwait(false); var intents = this.Discord.Configuration.Intents; if (intents.HasIntent(DiscordIntents.GuildMembers)) { if (this.MembersInternal != null) { this.MembersInternal[userId] = mbr; } } return mbr; } /// /// Retrieves a full list of members from Discord. This method will bypass cache. /// /// A collection of all members in this guild. /// Thrown when Discord is unable to process the request. public async Task> GetAllMembersAsync() { var recmbr = new HashSet(); var recd = 1000; var last = 0ul; while (recd > 0) { var tms = await this.Discord.ApiClient.ListGuildMembersAsync(this.Id, 1000, last == 0 ? null : (ulong?)last).ConfigureAwait(false); recd = tms.Count; foreach (var xtm in tms) { var usr = new DiscordUser(xtm.User) { Discord = this.Discord }; usr = this.Discord.UserCache.AddOrUpdate(xtm.User.Id, usr, (id, old) => { old.Username = usr.Username; old.Discord = usr.Discord; old.AvatarHash = usr.AvatarHash; return old; }); recmbr.Add(new DiscordMember(xtm) { Discord = this.Discord, GuildId = this.Id }); } var tm = tms.LastOrDefault(); last = tm?.User.Id ?? 0; } return new ReadOnlySet(recmbr); } /// /// Requests that Discord send a list of guild members based on the specified arguments. This method will fire the event. /// If no arguments aside from and are specified, this will request all guild members. /// /// Filters the returned members based on what the username starts with. Either this or must not be null. /// The must also be greater than 0 if this is specified. /// Total number of members to request. This must be greater than 0 if is specified. /// Whether to include the associated with the fetched members. /// Whether to limit the request to the specified user ids. Either this or must not be null. /// The unique string to identify the response. public async Task RequestMembersAsync(string query = "", int limit = 0, bool? presences = null, IEnumerable userIds = null, string nonce = null) { if (this.Discord is not DiscordClient client) throw new InvalidOperationException("This operation is only valid for regular Discord clients."); if (query == null && userIds == null) throw new ArgumentException("The query and user IDs cannot both be null."); if (query != null && userIds != null) query = null; var grgm = new GatewayRequestGuildMembers(this) { Query = query, Limit = limit >= 0 ? limit : 0, Presences = presences, UserIds = userIds, Nonce = nonce }; var payload = new GatewayPayload { OpCode = GatewayOpCode.RequestGuildMembers, Data = grgm }; var payloadStr = JsonConvert.SerializeObject(payload, Formatting.None); await client.WsSendAsync(payloadStr).ConfigureAwait(false); } /// /// Gets all the channels this guild has. /// /// A collection of this guild's channels. /// Thrown when Discord is unable to process the request. public Task> GetChannelsAsync() => this.Discord.ApiClient.GetGuildChannelsAsync(this.Id); /// /// Creates a new role in this guild. /// /// Name of the role. /// Permissions for the role. /// Color for the role. /// Whether the role is to be hoisted. /// Whether the role is to be mentionable. /// Reason for audit logs. /// The newly-created role. /// Thrown when the client does not have the permission. /// Thrown when Discord is unable to process the request. public Task CreateRoleAsync(string name = null, Permissions? permissions = null, DiscordColor? color = null, bool? hoist = null, bool? mentionable = null, string reason = null) => this.Discord.ApiClient.CreateGuildRoleAsync(this.Id, name, permissions, color?.Value, hoist, mentionable, reason); /// /// Gets a role from this guild by its ID. /// /// ID of the role to get. /// Requested role. /// Thrown when Discord is unable to process the request. public DiscordRole GetRole(ulong id) => this.RolesInternal.TryGetValue(id, out var role) ? role : null; /// /// Gets a channel from this guild by its ID. /// /// ID of the channel to get. /// Requested channel. /// Thrown when Discord is unable to process the request. public DiscordChannel GetChannel(ulong id) - => (this.ChannelsInternal != null && this.ChannelsInternal.TryGetValue(id, out var channel)) ? channel : null; + => this.ChannelsInternal != null && this.ChannelsInternal.TryGetValue(id, out var channel) ? channel : null; /// /// Gets a thread from this guild by its ID. /// /// ID of the thread to get. /// Requested thread. /// Thrown when Discord is unable to process the request. public DiscordThreadChannel GetThread(ulong id) - => (this.ThreadsInternal != null && this.ThreadsInternal.TryGetValue(id, out var thread)) ? thread : null; + => this.ThreadsInternal != null && this.ThreadsInternal.TryGetValue(id, out var thread) ? thread : null; /// /// 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 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.GuildUpdate: entry = new DiscordAuditLogGuildEntry { Target = this }; var entrygld = entry as DiscordAuditLogGuildEntry; foreach (var xc in xac.Changes) { 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) + 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": ulong.TryParse(xc.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t1); ulong.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t2); entrygld.AfkChannelChange = 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 } }; break; case "widget_channel_id": ulong.TryParse(xc.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t1); ulong.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t2); entrygld.EmbedChannelChange = 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 } }; 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": ulong.TryParse(xc.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t1); ulong.TryParse(xc.OldValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out t2); entrygld.SystemChannelChange = 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 } }; 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 "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.OldValue != null ? xc.OldValueString : null, After = xc.NewValue != null ? xc.NewValueString : null }; 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 "bitrate": entrychn.BitrateChange = 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; 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.OldValueString, After = xc.NewValueString }; 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; 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 "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 "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.OldValue != null ? xc.OldValueString : null, After = xc.NewValue != null ? xc.NewValueString : null }; 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 "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 "description": entryse.DescriptionChange = new PropertyChange { Before = xc.OldValue != null ? xc.OldValueString : null, After = xc.NewValue != null ? xc.NewValueString : null }; break; case "location": entryse.LocationChange = new PropertyChange { Before = xc.OldValue != null ? xc.OldValueString : null, After = xc.NewValue != null ? xc.NewValueString : null }; 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); } /// /// Gets all of this guild's custom emojis. /// /// All of this guild's custom emojis. /// Thrown when Discord is unable to process the request. public Task> GetEmojisAsync() => this.Discord.ApiClient.GetGuildEmojisAsync(this.Id); /// /// Gets this guild's specified custom emoji. /// /// ID of the emoji to get. /// The requested custom emoji. /// Thrown when Discord is unable to process the request. public Task GetEmojiAsync(ulong id) => this.Discord.ApiClient.GetGuildEmojiAsync(this.Id, id); /// /// Creates a new custom emoji for this guild. /// /// Name of the new emoji. /// Image to use as the emoji. /// Roles for which the emoji will be available. This works only if your application is whitelisted as integration. /// Reason for audit log. /// The newly-created emoji. /// Thrown when the client does not have the permission. /// Thrown when Discord is unable to process the request. public Task CreateEmojiAsync(string name, Stream image, IEnumerable roles = null, string reason = null) { if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullException(nameof(name)); name = name.Trim(); if (name.Length < 2 || name.Length > 50) throw new ArgumentException("Emoji name needs to be between 2 and 50 characters long."); if (image == null) throw new ArgumentNullException(nameof(image)); string image64 = null; using (var imgtool = new ImageTool(image)) image64 = imgtool.GetBase64(); return this.Discord.ApiClient.CreateGuildEmojiAsync(this.Id, name, image64, roles?.Select(xr => xr.Id), reason); } /// /// Modifies a this guild's custom emoji. /// /// Emoji to modify. /// New name for the emoji. /// Roles for which the emoji will be available. This works only if your application is whitelisted as integration. /// Reason for audit log. /// The modified emoji. /// Thrown when the client does not have the permission. /// Thrown when Discord is unable to process the request. public Task ModifyEmojiAsync(DiscordGuildEmoji emoji, string name, IEnumerable roles = null, string reason = null) { if (emoji == null) throw new ArgumentNullException(nameof(emoji)); if (emoji.Guild.Id != this.Id) throw new ArgumentException("This emoji does not belong to this guild."); if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullException(nameof(name)); name = name.Trim(); return name.Length < 2 || name.Length > 50 ? throw new ArgumentException("Emoji name needs to be between 2 and 50 characters long.") : this.Discord.ApiClient.ModifyGuildEmojiAsync(this.Id, emoji.Id, name, roles?.Select(xr => xr.Id), reason); } /// /// Deletes this guild's custom emoji. /// /// Emoji to delete. /// Reason for audit log. /// /// Thrown when the client does not have the permission. /// Thrown when Discord is unable to process the request. - public Task DeleteEmojiAsync(DiscordGuildEmoji emoji, string reason = null) - { - return emoji == null + public Task DeleteEmojiAsync(DiscordGuildEmoji emoji, string reason = null) => + emoji == null ? throw new ArgumentNullException(nameof(emoji)) : emoji.Guild.Id != this.Id - ? throw new ArgumentException("This emoji does not belong to this guild.") - : this.Discord.ApiClient.DeleteGuildEmojiAsync(this.Id, emoji.Id, reason); - } + ? throw new ArgumentException("This emoji does not belong to this guild.") + : this.Discord.ApiClient.DeleteGuildEmojiAsync(this.Id, emoji.Id, reason); /// /// Gets all of this guild's custom stickers. /// /// All of this guild's custom stickers. /// Thrown when Discord is unable to process the request. public async Task> GetStickersAsync() { var stickers = await this.Discord.ApiClient.GetGuildStickersAsync(this.Id); foreach (var xstr in stickers) { this.StickersInternal.AddOrUpdate(xstr.Id, xstr, (id, old) => { old.Name = xstr.Name; old.Description = xstr.Description; old.InternalTags = xstr.InternalTags; return old; }); } return stickers; } /// /// Gets a sticker /// /// Thrown when the sticker could not be found. /// Thrown when the client does not have the permission. /// Thrown when Discord is unable to process the request. /// Sticker does not belong to a guild. public Task GetStickerAsync(ulong stickerId) => this.Discord.ApiClient.GetGuildStickerAsync(this.Id, stickerId); /// /// Creates a sticker /// /// The name of the sticker. /// The optional description of the sticker. /// The emoji to associate the sticker with. /// The file format the sticker is written in. /// The sticker. /// Audit log reason /// Thrown when the client does not have the permission. /// Thrown when Discord is unable to process the request. public Task CreateStickerAsync(string name, string description, DiscordEmoji emoji, Stream file, StickerFormat format, string reason = null) { var fileExt = format switch { StickerFormat.Png => "png", StickerFormat.Apng => "png", StickerFormat.Lottie => "json", _ => throw new InvalidOperationException("This format is not supported.") }; var contentType = format switch { StickerFormat.Png => "image/png", StickerFormat.Apng => "image/png", StickerFormat.Lottie => "application/json", _ => throw new InvalidOperationException("This format is not supported.") }; return emoji.Id is not 0 ? throw new InvalidOperationException("Only unicode emoji can be used for stickers.") : name.Length < 2 || name.Length > 30 ? throw new ArgumentOutOfRangeException(nameof(name), "Sticker name needs to be between 2 and 30 characters long.") : description.Length < 1 || description.Length > 100 ? throw new ArgumentOutOfRangeException(nameof(description), "Sticker description needs to be between 1 and 100 characters long.") - : this.Discord.ApiClient.CreateGuildStickerAsync(this.Id, name, description, emoji.GetDiscordName().Replace(":", ""), new("sticker", file, null, fileExt, contentType), reason); + : this.Discord.ApiClient.CreateGuildStickerAsync(this.Id, name, description, emoji.GetDiscordName().Replace(":", ""), new DiscordMessageFile("sticker", file, null, fileExt, contentType), reason); } /// /// Modifies a sticker /// /// The id of the sticker to modify /// The name of the sticker /// The description of the sticker /// The emoji to associate with this sticker. /// Audit log reason /// A sticker object /// Thrown when the sticker could not be found. /// Thrown when the client does not have the permission. /// Thrown when Discord is unable to process the request. /// Sticker does not belong to a guild. public async Task ModifyStickerAsync(ulong sticker, Optional name, Optional description, Optional emoji, string reason = null) { string uemoji = null; if (!this.StickersInternal.TryGetValue(sticker, out var stickerobj) || stickerobj.Guild.Id != this.Id) throw new ArgumentException("This sticker does not belong to this guild."); if (name.HasValue && (name.Value.Length < 2 || name.Value.Length > 30)) throw new ArgumentException("Sticker name needs to be between 2 and 30 characters long."); if (description.HasValue && (description.Value.Length < 1 || description.Value.Length > 100)) throw new ArgumentException("Sticker description needs to be between 1 and 100 characters long."); if (emoji.HasValue && emoji.Value.Id > 0) throw new ArgumentException("Only unicode emojis can be used with stickers."); else if (emoji.HasValue) uemoji = emoji.Value.GetDiscordName().Replace(":", ""); var usticker = await this.Discord.ApiClient.ModifyGuildStickerAsync(this.Id, sticker, name, description, uemoji, reason).ConfigureAwait(false); if (this.StickersInternal.TryGetValue(usticker.Id, out var old)) this.StickersInternal.TryUpdate(usticker.Id, usticker, old); return usticker; } /// /// Modifies a sticker /// /// The sticker to modify /// The name of the sticker /// The description of the sticker /// The emoji to associate with this sticker. /// Audit log reason /// A sticker object /// Thrown when the sticker could not be found. /// Thrown when the client does not have the permission. /// Thrown when Discord is unable to process the request. /// Sticker does not belong to a guild. public Task ModifyStickerAsync(DiscordSticker sticker, Optional name, Optional description, Optional emoji, string reason = null) => this.ModifyStickerAsync(sticker.Id, name, description, emoji, reason); /// /// Deletes a sticker /// /// Id of sticker to delete /// Audit log reason /// Thrown when the sticker could not be found. /// Thrown when the client does not have the permission. /// Thrown when Discord is unable to process the request. /// Sticker does not belong to a guild. - public Task DeleteStickerAsync(ulong sticker, string reason = null) - { - return !this.StickersInternal.TryGetValue(sticker, out var stickerobj) + public Task DeleteStickerAsync(ulong sticker, string reason = null) => + !this.StickersInternal.TryGetValue(sticker, out var stickerobj) ? throw new ArgumentNullException(nameof(sticker)) : stickerobj.Guild.Id != this.Id - ? throw new ArgumentException("This sticker does not belong to this guild.") - : this.Discord.ApiClient.DeleteGuildStickerAsync(this.Id, sticker, reason); - } + ? throw new ArgumentException("This sticker does not belong to this guild.") + : this.Discord.ApiClient.DeleteGuildStickerAsync(this.Id, sticker, reason); /// /// Deletes a sticker /// /// Sticker to delete /// Audit log reason /// Thrown when the sticker could not be found. /// Thrown when the client does not have the permission. /// Thrown when Discord is unable to process the request. /// Sticker does not belong to a guild. public Task DeleteStickerAsync(DiscordSticker sticker, string reason = null) => this.DeleteStickerAsync(sticker.Id, reason); /// /// Gets the default channel for this guild. /// Default channel is the first channel current member can see. /// /// This member's default guild. /// Thrown when Discord is unable to process the request. - public DiscordChannel GetDefaultChannel() - { - return this.ChannelsInternal?.Values.Where(xc => xc.Type == ChannelType.Text) - .OrderBy(xc => xc.Position) - .FirstOrDefault(xc => (xc.PermissionsFor(this.CurrentMember) & DisCatSharp.Permissions.AccessChannels) == DisCatSharp.Permissions.AccessChannels); - } + public DiscordChannel GetDefaultChannel() => + this.ChannelsInternal?.Values.Where(xc => xc.Type == ChannelType.Text) + .OrderBy(xc => xc.Position) + .FirstOrDefault(xc => (xc.PermissionsFor(this.CurrentMember) & DisCatSharp.Permissions.AccessChannels) == DisCatSharp.Permissions.AccessChannels); /// /// Gets the guild's widget /// /// The guild's widget public Task GetWidgetAsync() => this.Discord.ApiClient.GetGuildWidgetAsync(this.Id); /// /// Gets the guild's widget settings /// /// The guild's widget settings public Task GetWidgetSettingsAsync() => this.Discord.ApiClient.GetGuildWidgetSettingsAsync(this.Id); /// /// Modifies the guild's widget settings /// /// If the widget is enabled or not /// Widget channel /// Reason the widget settings were modified /// The newly modified widget settings public Task ModifyWidgetSettingsAsync(bool? isEnabled = null, DiscordChannel channel = null, string reason = null) => this.Discord.ApiClient.ModifyGuildWidgetSettingsAsync(this.Id, isEnabled, channel?.Id, reason); /// /// Gets all of this guild's templates. /// /// All of the guild's templates. /// Throws when the client does not have the permission. /// Thrown when Discord is unable to process the request. public Task> GetTemplatesAsync() => this.Discord.ApiClient.GetGuildTemplatesAsync(this.Id); /// /// Creates a guild template. /// /// Name of the template. /// Description of the template. /// The template created. /// Throws when a template already exists for the guild or a null parameter is provided for the name. /// Throws when the client does not have the permission. /// Thrown when Discord is unable to process the request. public Task CreateTemplateAsync(string name, string description = null) => this.Discord.ApiClient.CreateGuildTemplateAsync(this.Id, name, description); /// /// Syncs the template to the current guild's state. /// /// The code of the template to sync. /// The template synced. /// Throws when the template for the code cannot be found /// Throws when the client does not have the permission. /// Thrown when Discord is unable to process the request. public Task SyncTemplateAsync(string code) => this.Discord.ApiClient.SyncGuildTemplateAsync(this.Id, code); /// /// Modifies the template's metadata. /// /// The template's code. /// Name of the template. /// Description of the template. /// The template modified. /// Throws when the template for the code cannot be found /// Throws when the client does not have the permission. /// Thrown when Discord is unable to process the request. public Task ModifyTemplateAsync(string code, string name = null, string description = null) => this.Discord.ApiClient.ModifyGuildTemplateAsync(this.Id, code, name, description); /// /// Deletes the template. /// /// The code of the template to delete. /// The deleted template. /// Throws when the template for the code cannot be found /// Throws when the client does not have the permission. /// Thrown when Discord is unable to process the request. public Task DeleteTemplateAsync(string code) => this.Discord.ApiClient.DeleteGuildTemplateAsync(this.Id, code); /// /// Gets this guild's membership screening form. /// /// This guild's membership screening form. /// Thrown when Discord is unable to process the request. public Task GetMembershipScreeningFormAsync() => this.Discord.ApiClient.GetGuildMembershipScreeningFormAsync(this.Id); /// /// Modifies this guild's membership screening form. /// /// Action to perform /// The modified screening form. /// Thrown when the client doesn't have the permission, or community is not enabled on this guild. /// Thrown when Discord is unable to process the request. public async Task ModifyMembershipScreeningFormAsync(Action action) { var mdl = new MembershipScreeningEditModel(); action(mdl); return await this.Discord.ApiClient.ModifyGuildMembershipScreeningFormAsync(this.Id, mdl.Enabled, mdl.Fields, mdl.Description); } /// /// Gets all the application commands in this guild. /// /// A list of application commands in this guild. public Task> GetApplicationCommandsAsync() => this.Discord.ApiClient.GetGuildApplicationCommandsAsync(this.Discord.CurrentApplication.Id, this.Id); /// /// Overwrites the existing application commands in this guild. New commands are automatically created and missing commands are automatically delete /// /// The list of commands to overwrite with. /// The list of guild commands public Task> BulkOverwriteApplicationCommandsAsync(IEnumerable commands) => this.Discord.ApiClient.BulkOverwriteGuildApplicationCommandsAsync(this.Discord.CurrentApplication.Id, this.Id, commands); /// /// Creates or overwrites a application command in this guild. /// /// The command to create. /// The created command. public Task CreateApplicationCommandAsync(DiscordApplicationCommand command) => this.Discord.ApiClient.CreateGuildApplicationCommandAsync(this.Discord.CurrentApplication.Id, this.Id, command); /// /// Edits a application command in this guild. /// /// The id of the command to edit. /// Action to perform. /// The edit command. public async Task EditApplicationCommandAsync(ulong commandId, Action action) { var mdl = new ApplicationCommandEditModel(); action(mdl); return await this.Discord.ApiClient.EditGuildApplicationCommandAsync(this.Discord.CurrentApplication.Id, this.Id, commandId, mdl.Name, mdl.Description, mdl.Options, mdl.DefaultPermission, mdl.NameLocalizations, mdl.DescriptionLocalizations).ConfigureAwait(false); } /// /// Gets this guild's welcome screen. /// /// This guild's welcome screen object. /// Thrown when Discord is unable to process the request. public Task GetWelcomeScreenAsync() => this.Discord.ApiClient.GetGuildWelcomeScreenAsync(this.Id); /// /// Modifies this guild's welcome screen. /// /// Action to perform. /// The modified welcome screen. /// Thrown when the client doesn't have the permission, or community is not enabled on this guild. /// Thrown when Discord is unable to process the request. public async Task ModifyWelcomeScreenAsync(Action action) { var mdl = new WelcomeScreenEditModel(); action(mdl); return await this.Discord.ApiClient.ModifyGuildWelcomeScreenAsync(this.Id, mdl.Enabled, mdl.WelcomeChannels, mdl.Description).ConfigureAwait(false); } #endregion /// /// Returns a string representation of this guild. /// /// String representation of this guild. public override string ToString() => $"Guild {this.Id}; {this.Name}"; /// /// Checks whether this is equal to another object. /// /// Object to compare to. /// Whether the object is equal to this . public override bool Equals(object obj) => this.Equals(obj as DiscordGuild); /// /// Checks whether this is equal to another . /// /// to compare to. /// Whether the is equal to this . public bool Equals(DiscordGuild e) => e is not null && (ReferenceEquals(this, e) || this.Id == e.Id); /// /// Gets the hash code for this . /// /// The hash code for this . public override int GetHashCode() => this.Id.GetHashCode(); /// /// Gets whether the two objects are equal. /// /// First guild to compare. /// Second guild to compare. /// Whether the two guilds are equal. public static bool operator ==(DiscordGuild e1, DiscordGuild e2) { var o1 = e1 as object; var o2 = e2 as object; return (o1 != null || o2 == null) && (o1 == null || o2 != null) && ((o1 == null && o2 == null) || e1.Id == e2.Id); } /// /// Gets whether the two objects are not equal. /// /// First guild to compare. /// Second guild to compare. /// Whether the two guilds are not equal. public static bool operator !=(DiscordGuild e1, DiscordGuild e2) => !(e1 == e2); } /// /// Represents guild verification level. /// public enum VerificationLevel : int { /// /// No verification. Anyone can join and chat right away. /// None = 0, /// /// Low verification level. Users are required to have a verified email attached to their account in order to be able to chat. /// Low = 1, /// /// Medium verification level. Users are required to have a verified email attached to their account, and account age need to be at least 5 minutes in order to be able to chat. /// Medium = 2, /// /// High verification level. Users are required to have a verified email attached to their account, account age need to be at least 5 minutes, and they need to be in the server for at least 10 minutes in order to be able to chat. /// High = 3, /// /// Highest verification level. Users are required to have a verified phone number attached to their account. /// Highest = 4 } /// /// Represents default notification level for a guild. /// public enum DefaultMessageNotifications : int { /// /// All messages will trigger push notifications. /// AllMessages = 0, /// /// Only messages that mention the user (or a role he's in) will trigger push notifications. /// MentionsOnly = 1 } /// /// Represents multi-factor authentication level required by a guild to use administrator functionality. /// public enum MfaLevel : int { /// /// Multi-factor authentication is not required to use administrator functionality. /// Disabled = 0, /// /// Multi-factor authentication is required to use administrator functionality. /// Enabled = 1 } /// /// Represents the value of explicit content filter in a guild. /// public enum ExplicitContentFilter : int { /// /// Explicit content filter is disabled. /// Disabled = 0, /// /// Only messages from members without any roles are scanned. /// MembersWithoutRoles = 1, /// /// Messages from all members are scanned. /// AllMembers = 2 } /// /// Represents the formats for a guild widget. /// public enum WidgetType : int { /// /// The widget is represented in shield format. /// This is the default widget type. /// Shield = 0, /// /// The widget is represented as the first banner type. /// Banner1 = 1, /// /// The widget is represented as the second banner type. /// Banner2 = 2, /// /// The widget is represented as the third banner type. /// Banner3 = 3, /// /// The widget is represented in the fourth banner type. /// Banner4 = 4 } /// /// Represents the guild features. /// public class GuildFeatures { /// /// Guild has access to set an animated guild icon. /// public bool CanSetAnimatedIcon { get; } /// /// Guild has access to set a guild banner image. /// public bool CanSetBanner { get; } /// /// Guild has access to use commerce features (i.e. create store channels) /// public bool CanCreateStoreChannels { get; } /// /// Guild can enable Welcome Screen, Membership Screening, Stage Channels, News Channels and receives community updates. /// Furthermore the guild can apply as a partner and for the discovery (if the prerequisites are given). /// and is usable. /// public bool HasCommunityEnabled { get; } /// /// Guild is able to be discovered in the discovery. /// public bool IsDiscoverable { get; } /// /// Guild is able to be featured in the discovery. /// public bool IsFeatureable { get; } /// /// Guild has access to set an invite splash background. /// public bool CanSetInviteSplash { get; } /// /// Guild has enabled Membership Screening. /// public bool HasMembershipScreeningEnabled { get; } /// /// Guild has access to create news channels. /// is usable. /// public bool CanCreateNewsChannels { get; } /// /// Guild is partnered. /// public bool IsPartnered { get; } /// /// Guild has increased custom emoji slots. /// public bool CanUploadMoreEmojis { get; } /// /// Guild can be previewed before joining via Membership Screening or the discovery. /// public bool HasPreviewEnabled { get; } /// /// Guild has access to set a vanity URL. /// public bool CanSetVanityUrl { get; } /// /// Guild is verified. /// public bool IsVerified { get; } /// /// Guild has access to set 384kbps bitrate in voice (previously VIP voice servers). /// public bool CanAccessVipRegions { get; } /// /// Guild has enabled the welcome screen. /// public bool HasWelcomeScreenEnabled { get; } /// /// Guild has enabled ticketed events. /// public bool HasTicketedEventsEnabled { get; } /// /// Guild has enabled monetization. /// public bool HasMonetizationEnabled { get; } /// /// Guild has increased custom sticker slots. /// public bool CanUploadMoreStickers { get; } /// /// Guild has access to the three day archive time for threads. /// Needs Premium Tier 1 (). /// public bool CanSetThreadArchiveDurationThreeDays { get; } /// /// Guild has access to the seven day archive time for threads. /// Needs Premium Tier 2 (). /// public bool CanSetThreadArchiveDurationSevenDays { get; } /// /// Guild has access to create private threads. /// Needs Premium Tier 2 (). /// public bool CanCreatePrivateThreads { get; } /// /// Guild is a hub. /// is usable. /// public bool IsHub { get; } /// /// Guild is in a hub. /// https://github.com/discord/discord-api-docs/pull/3757/commits/4932d92c9d0c783861bc715bf7ebbabb15114e34 /// public bool HasDirectoryEntry { get; } /// /// Guild is linked to a hub. /// public bool IsLinkedToHub { get; } /// /// Guild has full access to threads. /// Old Feature. /// public bool HasThreadTestingEnabled { get; } /// /// Guild has access to threads. /// public bool HasThreadsEnabled { get; } /// /// Guild can set role icons. /// public bool CanSetRoleIcons { get; } /// /// Guild has the new thread permissions. /// Old Feature. /// public bool HasNewThreadPermissions { get; } /// /// Guild can set thread default auto archive duration. /// Old Feature. /// public bool CanSetThreadDefaultAutoArchiveDuration { get; } /// /// Guild has enabled role subsriptions. /// public bool HasRoleSubscriptionsEnabled { get; } /// /// Guild role subsriptions as purchaseable. /// public bool RoleSubscriptionsIsAvaiableForPurchase { get; } /// /// Guild has premium tier 3 override. /// public bool PremiumTierThreeOverride { get; } /// /// Guild has access to text in voice. /// Restricted to . /// public bool TextInVoiceEnabled { get; } /// /// Guild can set an animated banner. /// Needs Premium Tier 3 (). /// public bool CanSetAnimatedBanner { get; } /// /// Guild can set an animated banner. /// Needs Premium Tier 3 (). /// public bool CanSetChannelBanner { get; } /// /// Allows members to customize their avatar, banner and bio for that server. /// public bool HasMemberProfiles { get; } /// /// Guild is restricted to users with the badge. /// public bool IsStaffOnly { get; } /// /// String of guild features. /// public string FeatureString { get; } /// /// Checks the guild features and constructs a new object. /// /// Guild to check public GuildFeatures(DiscordGuild guild) { this.CanSetAnimatedIcon = guild.RawFeatures.Contains("ANIMATED_ICON"); this.CanSetAnimatedBanner = guild.RawFeatures.Contains("ANIMATED_BANNER"); this.CanSetBanner = guild.RawFeatures.Contains("BANNER"); this.CanSetChannelBanner = guild.RawFeatures.Contains("CHANNEL_BANNER"); this.CanCreateStoreChannels = guild.RawFeatures.Contains("COMMERCE"); this.HasCommunityEnabled = guild.RawFeatures.Contains("COMMUNITY"); this.IsDiscoverable = !guild.RawFeatures.Contains("DISCOVERABLE_DISABLED") && guild.RawFeatures.Contains("DISCOVERABLE"); this.IsFeatureable = guild.RawFeatures.Contains("FEATURABLE"); this.CanSetInviteSplash = guild.RawFeatures.Contains("INVITE_SPLASH"); this.HasMembershipScreeningEnabled = guild.RawFeatures.Contains("MEMBER_VERIFICATION_GATE_ENABLED"); this.CanCreateNewsChannels = guild.RawFeatures.Contains("NEWS"); this.IsPartnered = guild.RawFeatures.Contains("PARTNERED"); this.CanUploadMoreEmojis = guild.RawFeatures.Contains("MORE_EMOJI"); this.HasPreviewEnabled = guild.RawFeatures.Contains("PREVIEW_ENABLED"); this.CanSetVanityUrl = guild.RawFeatures.Contains("VANITY_URL"); this.IsVerified = guild.RawFeatures.Contains("VERIFIED"); this.CanAccessVipRegions = guild.RawFeatures.Contains("VIP_REGIONS"); this.HasWelcomeScreenEnabled = guild.RawFeatures.Contains("WELCOME_SCREEN_ENABLED"); this.HasTicketedEventsEnabled = guild.RawFeatures.Contains("TICKETED_EVENTS_ENABLED"); this.HasMonetizationEnabled = guild.RawFeatures.Contains("MONETIZATION_ENABLED"); this.CanUploadMoreStickers = guild.RawFeatures.Contains("MORE_STICKERS"); this.CanSetThreadArchiveDurationThreeDays = guild.RawFeatures.Contains("THREE_DAY_THREAD_ARCHIVE"); this.CanSetThreadArchiveDurationSevenDays = guild.RawFeatures.Contains("SEVEN_DAY_THREAD_ARCHIVE"); this.CanCreatePrivateThreads = guild.RawFeatures.Contains("PRIVATE_THREADS"); this.IsHub = guild.RawFeatures.Contains("HUB"); this.HasThreadTestingEnabled = guild.RawFeatures.Contains("THREADS_ENABLED_TESTING"); this.HasThreadsEnabled = guild.RawFeatures.Contains("THREADS_ENABLED"); this.CanSetRoleIcons = guild.RawFeatures.Contains("ROLE_ICONS"); this.HasNewThreadPermissions = guild.RawFeatures.Contains("NEW_THREAD_PERMISSIONS"); this.HasRoleSubscriptionsEnabled = guild.RawFeatures.Contains("ROLE_SUBSCRIPTIONS_ENABLED"); this.PremiumTierThreeOverride = guild.RawFeatures.Contains("PREMIUM_TIER_3_OVERRIDE"); this.CanSetThreadDefaultAutoArchiveDuration = guild.RawFeatures.Contains("THREAD_DEFAULT_AUTO_ARCHIVE_DURATION"); this.TextInVoiceEnabled = guild.RawFeatures.Contains("TEXT_IN_VOICE_ENABLED"); this.HasDirectoryEntry = guild.RawFeatures.Contains("HAS_DIRECTORY_ENTRY"); this.IsLinkedToHub = guild.RawFeatures.Contains("LINKED_TO_HUB"); this.HasMemberProfiles = guild.RawFeatures.Contains("MEMBER_PROFILES"); this.IsStaffOnly = guild.RawFeatures.Contains("INTERNAL_EMPLOYEE_ONLY"); this.RoleSubscriptionsIsAvaiableForPurchase = guild.RawFeatures.Contains("ROLE_SUBSCRIPTIONS_AVAILABLE_FOR_PURCHASE"); var features = guild.RawFeatures.Any() ? "" : "None"; foreach (var feature in guild.RawFeatures) { features += feature + " "; } this.FeatureString = features; } } } diff --git a/DisCatSharp/Entities/Interaction/DiscordInteractionDataOption.cs b/DisCatSharp/Entities/Interaction/DiscordInteractionDataOption.cs index f6ae1e3b3..5ddcd3746 100644 --- a/DisCatSharp/Entities/Interaction/DiscordInteractionDataOption.cs +++ b/DisCatSharp/Entities/Interaction/DiscordInteractionDataOption.cs @@ -1,92 +1,87 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System.Collections.Generic; using Newtonsoft.Json; namespace DisCatSharp.Entities { /// /// Represents parameters for interaction commands. /// public sealed class DiscordInteractionDataOption { /// /// Gets the name of this interaction parameter. /// [JsonProperty("name")] public string Name { get; internal set; } /// /// Gets the type of this interaction parameter. /// [JsonProperty("type")] public ApplicationCommandOptionType Type { get; internal set; } /// /// Whether this option is currently focused by the user. /// Only applicable for autocomplete option choices. /// [JsonProperty("focused")] public bool Focused { get; internal set; } /// /// Gets the value of this interaction parameter. /// [JsonProperty("value")] internal string RawValue { get; set; } /// /// Gets the value of this interaction parameter. /// This can be cast to a , , , or depending on the /// [JsonIgnore] - public object Value - { - get - { - return this.Type == ApplicationCommandOptionType.Integer && int.TryParse(this.RawValue, out var raw) - ? raw - : this.Type == ApplicationCommandOptionType.Integer - ? long.Parse(this.RawValue) - : this.Type switch - { - ApplicationCommandOptionType.Boolean => bool.Parse(this.RawValue), - ApplicationCommandOptionType.String => this.RawValue, - ApplicationCommandOptionType.Channel => ulong.Parse(this.RawValue), - ApplicationCommandOptionType.User => ulong.Parse(this.RawValue), - ApplicationCommandOptionType.Role => ulong.Parse(this.RawValue), - ApplicationCommandOptionType.Mentionable => ulong.Parse(this.RawValue), - ApplicationCommandOptionType.Number => double.Parse(this.RawValue), - ApplicationCommandOptionType.Attachment => int.Parse(this.RawValue), - _ => this.RawValue, - }; - } - } + public object Value => + this.Type == ApplicationCommandOptionType.Integer && int.TryParse(this.RawValue, out var raw) + ? raw + : this.Type == ApplicationCommandOptionType.Integer + ? long.Parse(this.RawValue) + : this.Type switch + { + ApplicationCommandOptionType.Boolean => bool.Parse(this.RawValue), + ApplicationCommandOptionType.String => this.RawValue, + ApplicationCommandOptionType.Channel => ulong.Parse(this.RawValue), + ApplicationCommandOptionType.User => ulong.Parse(this.RawValue), + ApplicationCommandOptionType.Role => ulong.Parse(this.RawValue), + ApplicationCommandOptionType.Mentionable => ulong.Parse(this.RawValue), + ApplicationCommandOptionType.Number => double.Parse(this.RawValue), + ApplicationCommandOptionType.Attachment => int.Parse(this.RawValue), + _ => this.RawValue, + }; /// /// Gets the additional parameters if this parameter is a subcommand. /// [JsonProperty("options", NullValueHandling = NullValueHandling.Ignore)] public IEnumerable Options { get; internal set; } } } diff --git a/DisCatSharp/Entities/Message/DiscordMessage.cs b/DisCatSharp/Entities/Message/DiscordMessage.cs index cc67e59dc..cb5acb60e 100644 --- a/DisCatSharp/Entities/Message/DiscordMessage.cs +++ b/DisCatSharp/Entities/Message/DiscordMessage.cs @@ -1,888 +1,886 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Globalization; using System.Linq; using System.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(() => { var gid = this.Channel != null ? this.Channel is DiscordDmChannel ? "@me" : this.Channel.GuildId.Value.ToString(CultureInfo.InvariantCulture) : this.INTERNAL_THREAD.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" : "discord.com")}/channels/{gid}/{cid}/{mid}"); }); } /// /// Initializes a new instance of the class. /// /// The other. 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 ulong? 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()); // Return null to allow all mentions 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.HasValue ? new[] { embed.Value } : 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.HasValue ? new[] { embed.Value } : 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, new Optional>(builder.Embeds), builder.Mentions, builder.Components, builder.Suppressed, builder.Files, builder.Attachments.Count > 0 ? new Optional>(builder.Attachments) : builder.KeepAttachmentsInternal.HasValue ? builder.KeepAttachmentsInternal.Value ? new Optional>(this.Attachments) : 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, this.GetMentions(), 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, new Optional>(builder.Embeds), builder.Mentions, builder.Components, builder.Suppressed, builder.Files, builder.Attachments.Count > 0 ? new Optional>(builder.Attachments) : builder.KeepAttachmentsInternal.HasValue ? builder.KeepAttachmentsInternal.Value ? new Optional>(this.Attachments) : 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) - { - return Utilities.CheckThreadAutoArchiveDurationFeature(this.Channel.Guild, autoArchiveDuration) - ? await this.Discord.ApiClient.CreateThreadWithMessageAsync(this.ChannelId, this.Id, name, autoArchiveDuration, rateLimitPerUser, reason) - : throw new NotSupportedException($"Cannot modify ThreadAutoArchiveDuration. Guild needs boost tier {(autoArchiveDuration == ThreadAutoArchiveDuration.ThreeDays ? "one" : "two")}."); - } + 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.CreateThreadWithMessageAsync(this.ChannelId, this.Id, name, autoArchiveDuration, 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/Message/DiscordMessageBuilder.cs b/DisCatSharp/Entities/Message/DiscordMessageBuilder.cs index 74a202877..7820f76a4 100644 --- a/DisCatSharp/Entities/Message/DiscordMessageBuilder.cs +++ b/DisCatSharp/Entities/Message/DiscordMessageBuilder.cs @@ -1,460 +1,460 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; namespace DisCatSharp.Entities { /// /// Constructs a Message to be sent. /// public sealed class DiscordMessageBuilder { /// /// Gets or Sets the Message to be sent. /// public string Content { get => this._content; set { if (value != null && value.Length > 2000) throw new ArgumentException("Content cannot exceed 2000 characters.", nameof(value)); this._content = value; } } private string _content; /// /// Gets or sets the embed for the builder. This will always set the builder to have one embed. /// public DiscordEmbed Embed { get => this._embeds.Count > 0 ? this._embeds[0] : null; set { this._embeds.Clear(); this._embeds.Add(value); } } /// /// Gets the Sticker to be send. /// public DiscordSticker Sticker { get; set; } /// /// Gets the Embeds to be sent. /// public IReadOnlyList Embeds => this._embeds; private readonly List _embeds = new(); /// /// Gets or Sets if the message should be TTS. /// - public bool IsTts { get; set; } = false; + public bool IsTts { get; set; } /// /// Whether to keep previous attachments. /// - internal bool? KeepAttachmentsInternal = null; + internal bool? KeepAttachmentsInternal; /// /// Gets the Allowed Mentions for the message to be sent. /// - public List Mentions { get; private set; } = null; + public List Mentions { get; private set; } /// /// Gets the Files to be sent in the Message. /// public IReadOnlyCollection Files => this.FilesInternal; internal readonly List FilesInternal = new(); /// /// Gets the components that will be attached to the message. /// public IReadOnlyList Components => this.ComponentsInternal; internal readonly List ComponentsInternal = new(5); /// /// Gets the Attachments to be sent in the Message. /// public IReadOnlyList Attachments => this.AttachmentsInternal; internal readonly List AttachmentsInternal = new(); /// /// Gets the Reply Message ID. /// - public ulong? ReplyId { get; private set; } = null; + public ulong? ReplyId { get; private set; } /// /// Gets if the Reply should mention the user. /// - public bool MentionOnReply { get; private set; } = false; + public bool MentionOnReply { get; private set; } /// /// Gets if the embeds should be suppressed. /// - public bool Suppressed { get; private set; } = false; + public bool Suppressed { get; private set; } /// /// Gets if the Reply will error if the Reply Message Id does not reference a valid message. /// If set to false, invalid replies are send as a regular message. /// Defaults to false. /// public bool FailOnInvalidReply { get; set; } /// /// Sets the Content of the Message. /// /// The content to be set. /// The current builder to be chained. public DiscordMessageBuilder WithContent(string content) { this.Content = content; return this; } /// /// Adds a sticker to the message. Sticker must be from current guild. /// /// The sticker to add. /// The current builder to be chained. public DiscordMessageBuilder WithSticker(DiscordSticker sticker) { this.Sticker = sticker; return this; } /// /// Adds a row of components to a message, up to 5 components per row, and up to 5 rows per message. /// /// The components to add to the message. /// The current builder to be chained. /// No components were passed. public DiscordMessageBuilder AddComponents(params DiscordComponent[] components) => this.AddComponents((IEnumerable)components); /// /// Appends several rows of components to the message /// /// The rows of components to add, holding up to five each. /// public DiscordMessageBuilder AddComponents(IEnumerable components) { var ara = components.ToArray(); if (ara.Length + this.ComponentsInternal.Count > 5) throw new ArgumentException("ActionRow count exceeds maximum of five."); foreach (var ar in ara) this.ComponentsInternal.Add(ar); return this; } /// /// Adds a row of components to a message, up to 5 components per row, and up to 5 rows per message. /// /// The components to add to the message. /// The current builder to be chained. /// No components were passed. public DiscordMessageBuilder AddComponents(IEnumerable components) { var cmpArr = components.ToArray(); var count = cmpArr.Length; if (!cmpArr.Any()) throw new ArgumentOutOfRangeException(nameof(components), "You must provide at least one component"); if (count > 5) throw new ArgumentException("Cannot add more than 5 components per action row!"); var comp = new DiscordActionRowComponent(cmpArr); this.ComponentsInternal.Add(comp); return this; } /// /// Sets if the message should be TTS. /// /// If TTS should be set. /// The current builder to be chained. public DiscordMessageBuilder HasTts(bool isTts) { this.IsTts = isTts; return this; } /// /// Sets the embed for the current builder. /// /// The embed that should be set. /// The current builder to be chained. public DiscordMessageBuilder WithEmbed(DiscordEmbed embed) { if (embed == null) return this; this.Embed = embed; return this; } /// /// Appends an embed to the current builder. /// /// The embed that should be appended. /// The current builder to be chained. public DiscordMessageBuilder AddEmbed(DiscordEmbed embed) { if (embed == null) return this; //Providing null embeds will produce a 400 response from Discord.// this._embeds.Add(embed); return this; } /// /// Appends several embeds to the current builder. /// /// The embeds that should be appended. /// The current builder to be chained. public DiscordMessageBuilder AddEmbeds(IEnumerable embeds) { this._embeds.AddRange(embeds); return this; } /// /// Sets if the message has allowed mentions. /// /// The allowed Mention that should be sent. /// The current builder to be chained. public DiscordMessageBuilder WithAllowedMention(IMention allowedMention) { if (this.Mentions != null) this.Mentions.Add(allowedMention); else this.Mentions = new List { allowedMention }; return this; } /// /// Sets if the message has allowed mentions. /// /// The allowed Mentions that should be sent. /// The current builder to be chained. public DiscordMessageBuilder WithAllowedMentions(IEnumerable allowedMentions) { if (this.Mentions != null) this.Mentions.AddRange(allowedMentions); else this.Mentions = allowedMentions.ToList(); return this; } /// /// Sets if the message has files to be sent. /// /// The fileName that the file should be sent as. /// The Stream to the file. /// Tells the API Client to reset the stream position to what it was after the file is sent. /// Description of the file. /// The current builder to be chained. public DiscordMessageBuilder WithFile(string fileName, Stream stream, bool resetStreamPosition = false, string description = null) { if (this.Files.Count > 10) throw new ArgumentException("Cannot send more than 10 files with a single message."); if (this.FilesInternal.Any(x => x.FileName == fileName)) throw new ArgumentException("A File with that filename already exists"); if (resetStreamPosition) this.FilesInternal.Add(new DiscordMessageFile(fileName, stream, stream.Position, description: description)); else this.FilesInternal.Add(new DiscordMessageFile(fileName, stream, null, description: description)); return this; } /// /// Sets if the message has files to be sent. /// /// The Stream to the file. /// Tells the API Client to reset the stream position to what it was after the file is sent. /// Description of the file. /// The current builder to be chained. public DiscordMessageBuilder WithFile(FileStream stream, bool resetStreamPosition = false, string description = null) { if (this.Files.Count > 10) throw new ArgumentException("Cannot send more than 10 files with a single message."); if (this.FilesInternal.Any(x => x.FileName == stream.Name)) throw new ArgumentException("A File with that filename already exists"); if (resetStreamPosition) this.FilesInternal.Add(new DiscordMessageFile(stream.Name, stream, stream.Position, description: description)); else this.FilesInternal.Add(new DiscordMessageFile(stream.Name, stream, null, description: description)); return this; } /// /// Sets if the message has files to be sent. /// /// The Files that should be sent. /// Tells the API Client to reset the stream position to what it was after the file is sent. /// The current builder to be chained. public DiscordMessageBuilder WithFiles(Dictionary files, bool resetStreamPosition = false) { if (this.Files.Count + files.Count > 10) throw new ArgumentException("Cannot send more than 10 files with a single message."); foreach (var file in files) { if (this.FilesInternal.Any(x => x.FileName == file.Key)) throw new ArgumentException("A File with that filename already exists"); if (resetStreamPosition) this.FilesInternal.Add(new DiscordMessageFile(file.Key, file.Value, file.Value.Position)); else this.FilesInternal.Add(new DiscordMessageFile(file.Key, file.Value, null)); } return this; } /// /// Modifies the given attachments on edit. /// /// Attachments to edit. /// public DiscordMessageBuilder ModifyAttachments(IEnumerable attachments) { this.AttachmentsInternal.AddRange(attachments); return this; } /// /// Whether to keep the message attachments, if new ones are added. /// /// public DiscordMessageBuilder KeepAttachments(bool keep) { this.KeepAttachmentsInternal = keep; return this; } /// /// Sets if the message is a reply /// /// The ID of the message to reply to. /// If we should mention the user in the reply. /// Whether sending a reply that references an invalid message should be /// The current builder to be chained. public DiscordMessageBuilder WithReply(ulong messageId, bool mention = false, bool failOnInvalidReply = false) { this.ReplyId = messageId; this.MentionOnReply = mention; this.FailOnInvalidReply = failOnInvalidReply; if (mention) { this.Mentions ??= new List(); this.Mentions.Add(new RepliedUserMention()); } return this; } /// /// Sends the Message to a specific channel /// /// The channel the message should be sent to. /// The current builder to be chained. public Task SendAsync(DiscordChannel channel) => channel.SendMessageAsync(this); /// /// Sends the modified message. /// Note: Message replies cannot be modified. To clear the reply, simply pass to . /// /// The original Message to modify. /// The current builder to be chained. public Task ModifyAsync(DiscordMessage msg) => msg.ModifyAsync(this); /// /// Clears all message components on this builder. /// public void ClearComponents() => this.ComponentsInternal.Clear(); /// /// Allows for clearing the Message Builder so that it can be used again to send a new message. /// public void Clear() { this.Content = ""; this._embeds.Clear(); this.IsTts = false; this.Mentions = null; this.FilesInternal.Clear(); this.ReplyId = null; this.MentionOnReply = false; this.ComponentsInternal.Clear(); this.Suppressed = false; this.Sticker = null; this.AttachmentsInternal.Clear(); this.KeepAttachmentsInternal = false; } /// /// Does the validation before we send a the Create/Modify request. /// /// Tells the method to perform the Modify Validation or Create Validation. internal void Validate(bool isModify = false) { if (this._embeds.Count > 10) throw new ArgumentException("A message can only have up to 10 embeds."); if (!isModify) { if (this.Files?.Count == 0 && string.IsNullOrEmpty(this.Content) && (!this.Embeds?.Any() ?? true) && this.Sticker is null) throw new ArgumentException("You must specify content, an embed, a sticker or at least one file."); if (this.Components.Count > 5) throw new InvalidOperationException("You can only have 5 action rows per message."); if (this.Components.Any(c => c.Components.Count > 5)) throw new InvalidOperationException("Action rows can only have 5 components"); } } } } diff --git a/DisCatSharp/Entities/Optional.cs b/DisCatSharp/Entities/Optional.cs index a23cc654f..34d2a7b0a 100644 --- a/DisCatSharp/Entities/Optional.cs +++ b/DisCatSharp/Entities/Optional.cs @@ -1,291 +1,289 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Linq; using System.Reflection; using DisCatSharp.Net.Serialization; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Newtonsoft.Json.Serialization; namespace DisCatSharp.Entities { /// /// Helper methods for instantiating an . /// /// /// This class only serves to allow type parameter inference on calls to or /// . /// public static class Optional { /// /// Creates a new with specified value and valid state. /// /// Value to populate the optional with. /// Type of the value. /// Created optional. public static Optional FromValue(T value) => new(value); /// /// Creates a new empty with no value and invalid state. /// /// The type that the created instance is wrapping around. /// Created optional. public static Optional FromNoValue() => default; } // used internally to make serialization more convenient, do NOT change this, do NOT implement this yourself /// /// Represents a IOptional interface. /// internal interface IOptional { /// /// Gets a whether it has a value. /// bool HasValue { get; } /// /// Gets the raw value. /// object RawValue { get; } // must NOT throw InvalidOperationException } /// /// Represents a wrapper which may or may not have a value. /// /// Type of the value. [JsonConverter(typeof(OptionalJsonConverter))] public readonly struct Optional : IEquatable>, IEquatable, IOptional { /// /// Gets whether this has a value. /// public bool HasValue { get; } /// /// Gets the value of this . /// /// If this has no value. public T Value => this.HasValue ? this._val : throw new InvalidOperationException("Value is not set."); /// /// Gets the raw value. /// object IOptional.RawValue => this._val; private readonly T _val; /// /// Creates a new with specified value. /// /// Value of this option. public Optional(T value) { this._val = value; this.HasValue = true; } /// /// Returns a string representation of this optional value. /// /// String representation of this optional value. public override string ToString() => $"Optional<{typeof(T)}> ({(this.HasValue ? this.Value.ToString() : "")})"; /// /// Checks whether this (or its value) are equal to another object. /// /// Object to compare to. /// Whether the object is equal to this or its value. - public override bool Equals(object obj) - { - return obj switch + public override bool Equals(object obj) => + obj switch { T t => this.Equals(t), Optional opt => this.Equals(opt), _ => false, }; - } /// /// Checks whether this is equal to another . /// /// to compare to. /// Whether the is equal to this . public bool Equals(Optional e) => (!this.HasValue && !e.HasValue) || (this.HasValue == e.HasValue && this.Value.Equals(e.Value)); /// /// Checks whether the value of this is equal to specified object. /// /// Object to compare to. /// Whether the object is equal to the value of this . public bool Equals(T e) => this.HasValue && ReferenceEquals(this.Value, e); /// /// Gets the hash code for this . /// /// The hash code for this . public override int GetHashCode() => this.HasValue ? this.Value.GetHashCode() : 0; public static implicit operator Optional(T val) => new(val); public static explicit operator T(Optional opt) => opt.Value; public static bool operator ==(Optional opt1, Optional opt2) => opt1.Equals(opt2); public static bool operator !=(Optional opt1, Optional opt2) => !opt1.Equals(opt2); public static bool operator ==(Optional opt, T t) => opt.Equals(t); public static bool operator !=(Optional opt, T t) => !opt.Equals(t); /// /// Performs a mapping operation on the current , turning it into an Optional holding a /// instance if the source optional contains a value; otherwise, returns an /// of that same type with no value. /// /// The mapping function to apply on the current value if it exists /// The type of the target value returned by /// /// An containing a value denoted by calling if the current /// contains a value; otherwise, an empty of the target /// type. /// public Optional IfPresent(Func mapper) => this.HasValue ? new Optional(mapper(this.Value)) : default; } /// /// Represents an optional json contract resolver. /// /// internal sealed class OptionalJsonContractResolver : DefaultContractResolver { /// /// Creates the property. /// /// The member. /// The member serialization. protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) { var property = base.CreateProperty(member, memberSerialization); var type = property.PropertyType; if (!type.GetTypeInfo().ImplementedInterfaces.Contains(typeof(IOptional))) return property; // we cache the PropertyInfo object here (it's captured in closure). we don't have direct // access to the property value so we have to reflect into it from the parent instance // we use UnderlyingName instead of PropertyName in case the C# name is different from the Json name. var declaringMember = property.DeclaringType.GetTypeInfo().DeclaredMembers .FirstOrDefault(e => e.Name == property.UnderlyingName); switch (declaringMember) { case PropertyInfo declaringProp: property.ShouldSerialize = instance => // instance here is the declaring (parent) type { var optionalValue = declaringProp.GetValue(instance); return (optionalValue as IOptional).HasValue; }; return property; case FieldInfo declaringField: property.ShouldSerialize = instance => // instance here is the declaring (parent) type { var optionalValue = declaringField.GetValue(instance); return (optionalValue as IOptional).HasValue; }; return property; default: throw new InvalidOperationException( "Can only serialize Optional members that are fields or properties"); } } } /// /// Represents an optional json converter. /// internal sealed class OptionalJsonConverter : JsonConverter { /// /// Writes the json. /// /// The writer. /// The value. /// The serializer. public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { // we don't check for HasValue here since it's checked in OptionalJsonContractResolver var val = (value as IOptional).RawValue; // JToken.FromObject will throw if `null` so we manually write a null value. if (val == null) { // you can read serializer.NullValueHandling here, but unfortunately you can **not** skip serialization // here, or else you will get a nasty JsonWriterException, so we just ignore its value and manually // write the null. writer.WriteToken(JsonToken.Null); } else { // convert the value to a JSON object and write it to the property value. JToken.FromObject(val).WriteTo(writer); } } /// /// 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) { var genericType = objectType.GenericTypeArguments[0]; var constructor = objectType.GetTypeInfo().DeclaredConstructors .FirstOrDefault(e => e.GetParameters()[0].ParameterType == genericType); return constructor.Invoke(new[] { serializer.Deserialize(reader, genericType) }); } /// /// Whether it can convert. /// /// The object type. public override bool CanConvert(Type objectType) => objectType.GetTypeInfo().ImplementedInterfaces.Contains(typeof(IOptional)); } } diff --git a/DisCatSharp/Entities/Sticker/DiscordSticker.cs b/DisCatSharp/Entities/Sticker/DiscordSticker.cs index 07fe85d55..e834eec68 100644 --- a/DisCatSharp/Entities/Sticker/DiscordSticker.cs +++ b/DisCatSharp/Entities/Sticker/DiscordSticker.cs @@ -1,215 +1,213 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Generic; using System.Threading.Tasks; using DisCatSharp.Enums; using DisCatSharp.Net; using Newtonsoft.Json; namespace DisCatSharp.Entities { /// /// Represents a Discord Sticker. /// public class DiscordSticker : SnowflakeObject, IEquatable { /// /// Gets the Pack ID of this sticker. /// [JsonProperty("pack_id", NullValueHandling = NullValueHandling.Ignore)] public ulong? PackId { get; internal set; } /// /// Gets the Name of the sticker. /// [JsonProperty("name")] public string Name { get; internal set; } /// /// Gets the Description of the sticker. /// [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] public string Description { get; internal set; } /// /// Gets the type of sticker. /// [JsonProperty("type")] public StickerType Type { get; internal set; } /// /// For guild stickers, gets the user that made the sticker. /// [JsonProperty("user", NullValueHandling = NullValueHandling.Ignore)] public DiscordUser User { get; internal set; } /// /// Gets the guild associated with this sticker, if any. /// public DiscordGuild Guild => (this.Discord as DiscordClient).InternalGetCachedGuild(this.GuildId); /// /// Gets the guild id the sticker belongs too. /// [JsonProperty("guild_id", NullValueHandling = NullValueHandling.Ignore)] public ulong? GuildId { get; internal set; } /// /// Gets whether this sticker is available. Only applicable to guild stickers. /// [JsonProperty("available", NullValueHandling = NullValueHandling.Ignore)] public bool Available { get; internal set; } /// /// Gets the sticker's sort order, if it's in a pack. /// [JsonProperty("sort_value", NullValueHandling = NullValueHandling.Ignore)] public int? SortValue { get; internal set; } /// /// Gets the list of tags for the sticker. /// [JsonIgnore] public IEnumerable Tags => this.InternalTags != null ? this.InternalTags.Split(',') : Array.Empty(); /// /// Gets the asset hash of the sticker. /// [JsonProperty("asset", NullValueHandling = NullValueHandling.Ignore)] public string Asset { get; internal set; } /// /// Gets the preview asset hash of the sticker. /// [JsonProperty("preview_asset", NullValueHandling = NullValueHandling.Ignore)] public string PreviewAsset { get; internal set; } /// /// Gets the Format type of the sticker. /// [JsonProperty("format_type")] public StickerFormat FormatType { get; internal set; } /// /// Gets the tags of the sticker. /// [JsonProperty("tags", NullValueHandling = NullValueHandling.Ignore)] [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "")] internal string InternalTags { get; set; } /// /// Gets the url of the sticker. /// [JsonIgnore] public string Url => $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.STICKERS}/{this.Id}.{(this.FormatType == StickerFormat.Lottie ? "json" : "png")}"; /// /// Initializes a new instance of the class. /// internal DiscordSticker() { } /// /// Whether to stickers are equal. /// /// DiscordSticker /// public bool Equals(DiscordSticker other) => this.Id == other.Id; /// /// Gets the sticker in readable format. /// public override string ToString() => $"Sticker {this.Id}; {this.Name}; {this.FormatType}"; /// /// Modifies the sticker /// /// The name of the sticker /// The description of the sticker /// The name of a unicode emoji representing the sticker's expression /// Audit log reason /// A sticker object /// Thrown when the sticker could not be found. /// Thrown when the client does not have the permission. /// Thrown when Discord is unable to process the request. /// Sticker does not belong to a guild. - public Task ModifyAsync(Optional name, Optional description, Optional tags, string reason = null) - { - return !this.GuildId.HasValue + public Task ModifyAsync(Optional name, Optional description, Optional tags, string reason = null) => + !this.GuildId.HasValue ? throw new ArgumentException("This sticker does not belong to a guild.") : name.HasValue && (name.Value.Length < 2 || name.Value.Length > 30) - ? throw new ArgumentException("Sticker name needs to be between 2 and 30 characters long.") - : description.HasValue && (description.Value.Length < 1 || description.Value.Length > 100) - ? throw new ArgumentException("Sticker description needs to be between 1 and 100 characters long.") - : tags.HasValue && !DiscordEmoji.TryFromUnicode(this.Discord, tags.Value, out var emoji) - ? throw new ArgumentException("Sticker tags needs to be a unicode emoji.") - : this.Discord.ApiClient.ModifyGuildStickerAsync(this.GuildId.Value, this.Id, name, description, tags, reason); - } + ? throw new ArgumentException("Sticker name needs to be between 2 and 30 characters long.") + : description.HasValue && (description.Value.Length < 1 || description.Value.Length > 100) + ? throw new ArgumentException("Sticker description needs to be between 1 and 100 characters long.") + : tags.HasValue && !DiscordEmoji.TryFromUnicode(this.Discord, tags.Value, out var emoji) + ? throw new ArgumentException("Sticker tags needs to be a unicode emoji.") + : this.Discord.ApiClient.ModifyGuildStickerAsync(this.GuildId.Value, this.Id, name, description, tags, reason); /// /// Deletes the sticker /// /// Audit log reason /// Thrown when the sticker could not be found. /// Thrown when the client does not have the permission. /// Thrown when Discord is unable to process the request. /// Sticker does not belong to a guild. public Task DeleteAsync(string reason = null) => this.GuildId.HasValue ? this.Discord.ApiClient.DeleteGuildStickerAsync(this.GuildId.Value, this.Id, reason) : throw new ArgumentException("The requested sticker is no guild sticker."); } /// /// The sticker type /// public enum StickerType : long { /// /// Standard nitro sticker /// Standard = 1, /// /// Custom guild sticker /// Guild = 2 } /// /// The sticker type /// public enum StickerFormat : long { /// /// Sticker is a png /// Png = 1, /// /// Sticker is a animated png /// Apng = 2, /// /// Sticker is lottie /// Lottie = 3 } } diff --git a/DisCatSharp/Entities/Thread/DiscordThreadChannel.cs b/DisCatSharp/Entities/Thread/DiscordThreadChannel.cs index 6691d1631..c8c2f9089 100644 --- a/DisCatSharp/Entities/Thread/DiscordThreadChannel.cs +++ b/DisCatSharp/Entities/Thread/DiscordThreadChannel.cs @@ -1,660 +1,648 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Globalization; using System.Linq; using System.Threading.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.AutoArchiveDuration, mdl.PerUserRateLimit, 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) - { - return !this.IsWriteable() + public new Task SendMessageAsync(string content) => + !this.IsWriteable() ? 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) - { - return !this.IsWriteable() + public new Task SendMessageAsync(DiscordEmbed embed) => + !this.IsWriteable() ? 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) - { - return !this.IsWriteable() + public new Task SendMessageAsync(string content, DiscordEmbed embed) => + !this.IsWriteable() ? 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.IsWriteable() ? 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) - { - return 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) + 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.Count() < 2) { await this.Discord.ApiClient.DeleteMessageAsync(this.Id, msgs.Single(), reason).ConfigureAwait(false); return; } for (var i = 0; i < msgs.Count(); 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() - { - return this.Type != ChannelType.PublicThread && this.Type != ChannelType.PrivateThread && this.Type != ChannelType.NewsThread + 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() - { - return this.Type != ChannelType.PublicThread && this.Type != ChannelType.PrivateThread && this.Type != ChannelType.News + 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() { var threadchannel = (object)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})", }; return threadchannel; } #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/Thread/DiscordThreadChannelMember.cs b/DisCatSharp/Entities/Thread/DiscordThreadChannelMember.cs index 8ca671212..d4c0bc959 100644 --- a/DisCatSharp/Entities/Thread/DiscordThreadChannelMember.cs +++ b/DisCatSharp/Entities/Thread/DiscordThreadChannelMember.cs @@ -1,137 +1,137 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Globalization; using Newtonsoft.Json; namespace DisCatSharp.Entities { /// /// Represents a discord thread member object. /// public class DiscordThreadChannelMember : SnowflakeObject, IEquatable { /// /// Gets the id of the user. /// [JsonProperty("user_id", NullValueHandling = NullValueHandling.Ignore)] public ulong UserId { get; internal set; } /// /// Gets the member object of the user. /// [JsonProperty("member", NullValueHandling = NullValueHandling.Ignore)] public DiscordMember Member { get; internal set; } /// /// Gets the presence of the user. /// [JsonProperty("presence", NullValueHandling = NullValueHandling.Ignore)] public DiscordPresence Presence { get; internal set; } /// /// Gets the timestamp when the user joined the thread. /// [JsonIgnore] public DateTimeOffset? JoinTimeStamp => !string.IsNullOrWhiteSpace(this.JoinTimeStampRaw) && DateTimeOffset.TryParse(this.JoinTimeStampRaw, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dto) ? dto : null; /// /// Gets the timestamp when the user joined the thread as raw string. /// [JsonProperty("join_timestamp", NullValueHandling = NullValueHandling.Ignore)] internal string JoinTimeStampRaw { get; set; } /// /// Gets the thread member flags. /// [JsonProperty("flags", NullValueHandling = NullValueHandling.Ignore)] public ThreadMemberFlags Flags { get; internal set; } /// /// Gets the category that contains this channel. For threads, gets the channel this thread was created in. /// [JsonIgnore] public DiscordChannel Thread - => this.Guild != null ? (this.Guild.ThreadsInternal.TryGetValue(this.Id, out var thread) ? thread : null) : null; + => this.Guild != null ? this.Guild.ThreadsInternal.TryGetValue(this.Id, out var thread) ? thread : null : null; /// /// Gets the guild to which this channel belongs. /// [JsonIgnore] public DiscordGuild Guild => this.Discord.Guilds.TryGetValue(this.GuildId, out var guild) ? guild : null; [JsonIgnore] internal ulong GuildId; /// /// 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 DiscordThreadChannelMember); /// /// Checks whether this is equal to another . /// /// to compare to. /// Whether the is equal to this . public bool Equals(DiscordThreadChannelMember e) => e is not null && (ReferenceEquals(this, e) || (this.Id == e.Id && this.UserId == e.UserId)); /// /// Gets the hash code for this . /// /// The hash code for this . public override int GetHashCode() => HashCode.Combine(this.Id.GetHashCode(), this.UserId.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 ==(DiscordThreadChannelMember e1, DiscordThreadChannelMember 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.UserId == e2.UserId)); } /// /// 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 !=(DiscordThreadChannelMember e1, DiscordThreadChannelMember e2) => !(e1 == e2); /// /// Initializes a new instance of the class. /// internal DiscordThreadChannelMember() { } } } diff --git a/DisCatSharp/Entities/User/DiscordActivity.cs b/DisCatSharp/Entities/User/DiscordActivity.cs index 59888101e..0560cefb7 100644 --- a/DisCatSharp/Entities/User/DiscordActivity.cs +++ b/DisCatSharp/Entities/User/DiscordActivity.cs @@ -1,528 +1,526 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Generic; using System.Globalization; using DisCatSharp.Net.Abstractions; using Newtonsoft.Json; namespace DisCatSharp.Entities { /// /// Represents user status. /// [JsonConverter(typeof(UserStatusConverter))] public enum UserStatus { /// /// User is offline. /// Offline = 0, /// /// User is online. /// Online = 1, /// /// User is idle. /// Idle = 2, /// /// User asked not to be disturbed. /// DoNotDisturb = 4, /// /// User is invisible. They will appear as Offline to anyone but themselves. /// Invisible = 5, /// /// User is streaming. /// Streaming = 6 } /// /// Represents a user status converter. /// internal sealed class UserStatusConverter : JsonConverter { /// /// Writes the json. /// /// The writer. /// The value. /// The serializer. public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { if (value is UserStatus status) { switch (status) // reader.Value can be a string, DateTime or DateTimeOffset (yes, it's weird) { case UserStatus.Online: writer.WriteValue("online"); return; case UserStatus.Idle: writer.WriteValue("idle"); return; case UserStatus.DoNotDisturb: writer.WriteValue("dnd"); return; case UserStatus.Invisible: writer.WriteValue("invisible"); return; case UserStatus.Streaming: writer.WriteValue("streaming"); return; case UserStatus.Offline: default: writer.WriteValue("offline"); return; } } } /// /// 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) - { + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) => // Active sessions are indicated with an "online", "idle", or "dnd" string per platform. If a user is // offline or invisible, the corresponding field is not present. - return (reader.Value?.ToString().ToLowerInvariant()) switch // reader.Value can be a string, DateTime or DateTimeOffset (yes, it's weird) + reader.Value?.ToString().ToLowerInvariant() switch // reader.Value can be a string, DateTime or DateTimeOffset (yes, it's weird) { "online" => UserStatus.Online, "idle" => UserStatus.Idle, "dnd" => UserStatus.DoNotDisturb, "invisible" => UserStatus.Invisible, "streaming" => UserStatus.Streaming, _ => UserStatus.Offline, }; - } /// /// Whether this user5 status can be converted. /// /// The object type. /// A bool. public override bool CanConvert(Type objectType) => objectType == typeof(UserStatus); } /// /// Represents a game that a user is playing. /// public sealed class DiscordActivity { /// /// Gets or sets the id of user's activity. /// public string Id { get; set; } /// /// Gets or sets the name of user's activity. /// public string Name { get; set; } /// /// Gets or sets the stream URL, if applicable. /// public string StreamUrl { get; set; } /// /// Gets or sets platform in this rich presence. /// public string Platform { get; set; } /// /// Gets or sets sync_id in this rich presence. /// public string SyncId { get; set; } /// /// Gets or sets session_id in this rich presence. /// public string SessionId { get; set; } /// /// Gets or sets the activity type. /// public ActivityType ActivityType { get; set; } /// /// Gets the rich presence details, if present. /// public DiscordRichPresence RichPresence { get; internal set; } /// /// Gets the custom status of this activity, if present. /// public DiscordCustomStatus CustomStatus { get; internal set; } /// /// Creates a new, empty instance of a . /// public DiscordActivity() { this.ActivityType = ActivityType.Playing; } /// /// Creates a new instance of a with specified name. /// /// Name of the activity. public DiscordActivity(string name) { this.Name = name; this.ActivityType = ActivityType.Playing; } /// /// Creates a new instance of a with specified name. /// /// Name of the activity. /// Type of the activity. public DiscordActivity(string name, ActivityType type) { if (type == ActivityType.Custom) throw new InvalidOperationException("Bots cannot use a custom status."); this.Name = name; this.ActivityType = type; } /// /// Initializes a new instance of the class. /// /// The raw activity. internal DiscordActivity(TransportActivity rawActivity) { this.UpdateWith(rawActivity); } /// /// Initializes a new instance of the class. /// /// The other. internal DiscordActivity(DiscordActivity other) { this.Name = other.Name; this.ActivityType = other.ActivityType; this.StreamUrl = other.StreamUrl; this.SessionId = other.SessionId; this.SyncId = other.SyncId; this.Platform = other.Platform; this.RichPresence = new DiscordRichPresence(other.RichPresence); this.CustomStatus = new DiscordCustomStatus(other.CustomStatus); } /// /// Updates a activity with an transport activity. /// /// The raw activity. internal void UpdateWith(TransportActivity rawActivity) { this.Name = rawActivity?.Name; this.ActivityType = rawActivity != null ? rawActivity.ActivityType : ActivityType.Playing; this.StreamUrl = rawActivity?.StreamUrl; this.SessionId = rawActivity?.SessionId; this.SyncId = rawActivity?.SyncId; this.Platform = rawActivity?.Platform; if (rawActivity?.IsRichPresence() == true && this.RichPresence != null) this.RichPresence.UpdateWith(rawActivity); else this.RichPresence = rawActivity?.IsRichPresence() == true ? new DiscordRichPresence(rawActivity) : null; if (rawActivity?.IsCustomStatus() == true && this.CustomStatus != null) this.CustomStatus.UpdateWith(rawActivity.State, rawActivity.Emoji); else this.CustomStatus = rawActivity?.IsCustomStatus() == true ? new DiscordCustomStatus { Name = rawActivity.State, Emoji = rawActivity.Emoji } : null; } } /// /// Represents details for a custom status activity, attached to a . /// public sealed class DiscordCustomStatus { /// /// Gets the name of this custom status. /// public string Name { get; internal set; } /// /// Gets the emoji of this custom status, if any. /// public DiscordEmoji Emoji { get; internal set; } /// /// Initializes a new instance of the class. /// internal DiscordCustomStatus() { } /// /// Initializes a new instance of the class. /// /// The other. internal DiscordCustomStatus(DiscordCustomStatus other) { this.Name = other.Name; this.Emoji = other.Emoji; } /// /// Updates a discord status. /// /// The state. /// The emoji. internal void UpdateWith(string state, DiscordEmoji emoji) { this.Name = state; this.Emoji = emoji; } } /// /// Represents details for Discord rich presence, attached to a . /// public sealed class DiscordRichPresence { /// /// Gets the details of this presence. /// public string Details { get; internal set; } /// /// Gets the game state. /// public string State { get; internal set; } /// /// Gets the application for which the rich presence is for. /// public DiscordApplication Application { get; internal set; } /// /// Gets the instance status. /// public bool? Instance { get; internal set; } /// /// Gets the large image for the rich presence. /// public DiscordAsset LargeImage { get; internal set; } /// /// Gets the hovertext for large image. /// public string LargeImageText { get; internal set; } /// /// Gets the small image for the rich presence. /// public DiscordAsset SmallImage { get; internal set; } /// /// Gets the hovertext for small image. /// public string SmallImageText { get; internal set; } /// /// Gets the current party size. /// public long? CurrentPartySize { get; internal set; } /// /// Gets the maximum party size. /// public long? MaximumPartySize { get; internal set; } /// /// Gets the party ID. /// public ulong? PartyId { get; internal set; } /// /// Gets the buttons. /// public IReadOnlyList Buttons { get; internal set; } /// /// Gets the game start timestamp. /// public DateTimeOffset? StartTimestamp { get; internal set; } /// /// Gets the game end timestamp. /// public DateTimeOffset? EndTimestamp { get; internal set; } /// /// Gets the secret value enabling users to join your game. /// public string JoinSecret { get; internal set; } /// /// Gets the secret value enabling users to receive notifications whenever your game state changes. /// public string MatchSecret { get; internal set; } /// /// Gets the secret value enabling users to spectate your game. /// public string SpectateSecret { get; internal set; } /// /// Initializes a new instance of the class. /// internal DiscordRichPresence() { } /// /// Initializes a new instance of the class. /// /// The raw game. internal DiscordRichPresence(TransportActivity rawGame) { this.UpdateWith(rawGame); } /// /// Initializes a new instance of the class. /// /// The other. internal DiscordRichPresence(DiscordRichPresence other) { this.Details = other.Details; this.State = other.State; this.Application = other.Application; this.Instance = other.Instance; this.LargeImageText = other.LargeImageText; this.SmallImageText = other.SmallImageText; this.LargeImage = other.LargeImage; this.SmallImage = other.SmallImage; this.CurrentPartySize = other.CurrentPartySize; this.MaximumPartySize = other.MaximumPartySize; this.PartyId = other.PartyId; this.Buttons = other.Buttons; this.StartTimestamp = other.StartTimestamp; this.EndTimestamp = other.EndTimestamp; this.JoinSecret = other.JoinSecret; this.MatchSecret = other.MatchSecret; this.SpectateSecret = other.SpectateSecret; } /// /// Updates a game activity with an transport activity. /// /// The raw game. internal void UpdateWith(TransportActivity rawGame) { this.Details = rawGame?.Details; this.State = rawGame?.State; this.Application = rawGame?.ApplicationId != null ? new DiscordApplication { Id = rawGame.ApplicationId.Value } : null; this.Instance = rawGame?.Instance; this.LargeImageText = rawGame?.Assets?.LargeImageText; this.SmallImageText = rawGame?.Assets?.SmallImageText; this.CurrentPartySize = rawGame?.Party?.Size?.Current; this.MaximumPartySize = rawGame?.Party?.Size?.Maximum; if (rawGame?.Party != null && ulong.TryParse(rawGame.Party.Id, NumberStyles.Number, CultureInfo.InvariantCulture, out var partyId)) this.PartyId = partyId; this.Buttons = rawGame?.Buttons; this.StartTimestamp = rawGame?.Timestamps?.Start; this.EndTimestamp = rawGame?.Timestamps?.End; this.JoinSecret = rawGame?.Secrets?.Join; this.MatchSecret = rawGame?.Secrets?.Match; this.SpectateSecret = rawGame?.Secrets?.Spectate; var lid = rawGame?.Assets?.LargeImage; if (lid != null) { if (lid.StartsWith("spotify:")) this.LargeImage = new DiscordSpotifyAsset { Id = lid }; else if (ulong.TryParse(lid, NumberStyles.Number, CultureInfo.InvariantCulture, out var ulid)) this.LargeImage = new DiscordApplicationAsset { Id = lid, Application = this.Application, Type = ApplicationAssetType.LargeImage }; } var sid = rawGame?.Assets?.SmallImage; if (sid != null) { if (sid.StartsWith("spotify:")) this.SmallImage = new DiscordSpotifyAsset { Id = sid }; else if (ulong.TryParse(sid, NumberStyles.Number, CultureInfo.InvariantCulture, out var usid)) this.SmallImage = new DiscordApplicationAsset { Id = sid, Application = this.Application, Type = ApplicationAssetType.LargeImage }; } } } /// /// Determines the type of a user activity. /// public enum ActivityType { /// /// Indicates the user is playing a game. /// Playing = 0, /// /// Indicates the user is streaming a game. /// Streaming = 1, /// /// Indicates the user is listening to something. /// ListeningTo = 2, /// /// Indicates the user is watching something. /// Watching = 3, /// /// Indicates the current activity is a custom status. /// Custom = 4, /// /// Indicates the user is competing in something. /// Competing = 5 } } diff --git a/DisCatSharp/Entities/User/DiscordUser.cs b/DisCatSharp/Entities/User/DiscordUser.cs index 661a7b1b2..cbdd1307f 100644 --- a/DisCatSharp/Entities/User/DiscordUser.cs +++ b/DisCatSharp/Entities/User/DiscordUser.cs @@ -1,433 +1,433 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Generic; using System.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. 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); [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. /// 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. 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 /// /// 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 /// /// /// [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "")] public async Task IsNotInGuild(DiscordGuild guild) => !await this.IsInGuild(guild); /// /// 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 : 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", + 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/Entities/Webhook/DiscordWebhookBuilder.cs b/DisCatSharp/Entities/Webhook/DiscordWebhookBuilder.cs index f2170ef18..c3f49ed87 100644 --- a/DisCatSharp/Entities/Webhook/DiscordWebhookBuilder.cs +++ b/DisCatSharp/Entities/Webhook/DiscordWebhookBuilder.cs @@ -1,442 +1,442 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; namespace DisCatSharp.Entities { /// /// Constructs ready-to-send webhook requests. /// public sealed class DiscordWebhookBuilder { /// /// Username to use for this webhook request. /// public Optional Username { get; set; } /// /// Avatar url to use for this webhook request. /// public Optional AvatarUrl { get; set; } /// /// Whether this webhook request is text-to-speech. /// public bool IsTts { get; set; } /// /// Message to send on this webhook request. /// public string Content { get => this._content; set { if (value != null && value.Length > 2000) throw new ArgumentException("Content length cannot exceed 2000 characters.", nameof(value)); this._content = value; } } private string _content; /// /// Whether to keep previous attachments. /// - internal bool? KeepAttachmentsInternal = null; + internal bool? KeepAttachmentsInternal; /// /// Embeds to send on this webhook request. /// public IReadOnlyList Embeds => this._embeds; private readonly List _embeds = new(); /// /// Files to send on this webhook request. /// public IReadOnlyList Files => this._files; private readonly List _files = new(); /// /// Mentions to send on this webhook request. /// public IReadOnlyList Mentions => this._mentions; private readonly List _mentions = new(); /// /// Gets the components. /// public IReadOnlyList Components => this._components; private readonly List _components = new(); /// /// Attachments to keep on this webhook request. /// public IEnumerable Attachments => this.AttachmentsInternal; internal readonly List AttachmentsInternal = new(); /// /// Constructs a new empty webhook request builder. /// public DiscordWebhookBuilder() { } // I still see no point in initializing collections with empty collections. // /// /// Adds a row of components to the builder, up to 5 components per row, and up to 5 rows per message. /// /// The components to add to the builder. /// The current builder to be chained. /// No components were passed. public DiscordWebhookBuilder AddComponents(params DiscordComponent[] components) => this.AddComponents((IEnumerable)components); /// /// Appends several rows of components to the builder /// /// The rows of components to add, holding up to five each. /// public DiscordWebhookBuilder AddComponents(IEnumerable components) { var ara = components.ToArray(); if (ara.Length + this._components.Count > 5) throw new ArgumentException("ActionRow count exceeds maximum of five."); foreach (var ar in ara) this._components.Add(ar); return this; } /// /// Adds a row of components to the builder, up to 5 components per row, and up to 5 rows per message. /// /// The components to add to the builder. /// The current builder to be chained. /// No components were passed. public DiscordWebhookBuilder AddComponents(IEnumerable components) { var cmpArr = components.ToArray(); var count = cmpArr.Length; if (!cmpArr.Any()) throw new ArgumentOutOfRangeException(nameof(components), "You must provide at least one component"); if (count > 5) throw new ArgumentException("Cannot add more than 5 components per action row!"); var comp = new DiscordActionRowComponent(cmpArr); this._components.Add(comp); return this; } /// /// Sets the username for this webhook builder. /// /// Username of the webhook public DiscordWebhookBuilder WithUsername(string username) { this.Username = username; return this; } /// /// Sets the avatar of this webhook builder from its url. /// /// Avatar url of the webhook public DiscordWebhookBuilder WithAvatarUrl(string avatarUrl) { this.AvatarUrl = avatarUrl; return this; } /// /// Indicates if the webhook must use text-to-speech. /// /// Text-to-speech public DiscordWebhookBuilder WithTts(bool tts) { this.IsTts = tts; return this; } /// /// Sets the message to send at the execution of the webhook. /// /// Message to send. public DiscordWebhookBuilder WithContent(string content) { this.Content = content; return this; } /// /// Adds an embed to send at the execution of the webhook. /// /// Embed to add. public DiscordWebhookBuilder AddEmbed(DiscordEmbed embed) { if (embed != null) this._embeds.Add(embed); return this; } /// /// Adds the given embeds to send at the execution of the webhook. /// /// Embeds to add. public DiscordWebhookBuilder AddEmbeds(IEnumerable embeds) { this._embeds.AddRange(embeds); return this; } /// /// Adds a file to send at the execution of the webhook. /// /// Name of the file. /// File data. /// Tells the API Client to reset the stream position to what it was after the file is sent. /// Description of the file. public DiscordWebhookBuilder AddFile(string filename, Stream data, bool resetStreamPosition = false, string description = null) { if (this.Files.Count() > 10) throw new ArgumentException("Cannot send more than 10 files with a single message."); if (this._files.Any(x => x.FileName == filename)) throw new ArgumentException("A File with that filename already exists"); if (resetStreamPosition) this._files.Add(new DiscordMessageFile(filename, data, data.Position, description: description)); else this._files.Add(new DiscordMessageFile(filename, data, null, description: description)); return this; } /// /// Sets if the message has files to be sent. /// /// The Stream to the file. /// Tells the API Client to reset the stream position to what it was after the file is sent. /// Description of the file. /// public DiscordWebhookBuilder AddFile(FileStream stream, bool resetStreamPosition = false, string description = null) { if (this.Files.Count() > 10) throw new ArgumentException("Cannot send more than 10 files with a single message."); if (this._files.Any(x => x.FileName == stream.Name)) throw new ArgumentException("A File with that filename already exists"); if (resetStreamPosition) this._files.Add(new DiscordMessageFile(stream.Name, stream, stream.Position, description: description)); else this._files.Add(new DiscordMessageFile(stream.Name, stream, null, description: description)); return this; } /// /// Adds the given files to send at the execution of the webhook. /// /// Dictionary of file name and file data. /// Tells the API Client to reset the stream position to what it was after the file is sent. public DiscordWebhookBuilder AddFiles(Dictionary files, bool resetStreamPosition = false) { if (this.Files.Count() + files.Count() > 10) throw new ArgumentException("Cannot send more than 10 files with a single message."); foreach (var file in files) { if (this._files.Any(x => x.FileName == file.Key)) throw new ArgumentException("A File with that filename already exists"); if (resetStreamPosition) this._files.Add(new DiscordMessageFile(file.Key, file.Value, file.Value.Position)); else this._files.Add(new DiscordMessageFile(file.Key, file.Value, null)); } return this; } /// /// Modifies the given attachments on edit. /// /// Attachments to edit. /// public DiscordWebhookBuilder ModifyAttachments(IEnumerable attachments) { this.AttachmentsInternal.AddRange(attachments); return this; } /// /// Whether to keep the message attachments, if new ones are added. /// /// public DiscordWebhookBuilder KeepAttachments(bool keep) { this.KeepAttachmentsInternal = keep; return this; } /// /// Adds the mention to the mentions to parse, etc. at the execution of the webhook. /// /// Mention to add. public DiscordWebhookBuilder AddMention(IMention mention) { this._mentions.Add(mention); return this; } /// /// Adds the mentions to the mentions to parse, etc. at the execution of the webhook. /// /// Mentions to add. public DiscordWebhookBuilder AddMentions(IEnumerable mentions) { this._mentions.AddRange(mentions); return this; } /// /// Executes a webhook. /// /// The webhook that should be executed. /// The message sent public async Task SendAsync(DiscordWebhook webhook) => await webhook.ExecuteAsync(this).ConfigureAwait(false); /// /// Executes a webhook. /// /// The webhook that should be executed. /// Target thread id. /// The message sent public async Task SendAsync(DiscordWebhook webhook, ulong threadId) => await webhook.ExecuteAsync(this, threadId.ToString()).ConfigureAwait(false); /// /// Sends the modified webhook message. /// /// The webhook that should be executed. /// The message to modify. /// The modified message public async Task ModifyAsync(DiscordWebhook webhook, DiscordMessage message) => await this.ModifyAsync(webhook, message.Id).ConfigureAwait(false); /// /// Sends the modified webhook message. /// /// The webhook that should be executed. /// The id of the message to modify. /// The modified message public async Task ModifyAsync(DiscordWebhook webhook, ulong messageId) => await webhook.EditMessageAsync(messageId, this).ConfigureAwait(false); /// /// Sends the modified webhook message. /// /// The webhook that should be executed. /// The message to modify. /// Target thread. /// The modified message public async Task ModifyAsync(DiscordWebhook webhook, DiscordMessage message, DiscordThreadChannel thread) => await this.ModifyAsync(webhook, message.Id, thread.Id).ConfigureAwait(false); /// /// Sends the modified webhook message. /// /// The webhook that should be executed. /// The id of the message to modify. /// Target thread id. /// The modified message public async Task ModifyAsync(DiscordWebhook webhook, ulong messageId, ulong threadId) => await webhook.EditMessageAsync(messageId, this, threadId.ToString()).ConfigureAwait(false); /// /// Clears all message components on this builder. /// public void ClearComponents() => this._components.Clear(); /// /// Allows for clearing the Webhook Builder so that it can be used again to send a new message. /// public void Clear() { this.Content = ""; this._embeds.Clear(); this.IsTts = false; this._mentions.Clear(); this._files.Clear(); this.AttachmentsInternal.Clear(); this._components.Clear(); this.KeepAttachmentsInternal = false; } /// /// Does the validation before we send a the Create/Modify request. /// /// Tells the method to perform the Modify Validation or Create Validation. /// Tells the method to perform the follow up message validation. /// Tells the method to perform the interaction response validation. internal void Validate(bool isModify = false, bool isFollowup = false, bool isInteractionResponse = false) { if (isModify) { if (this.Username.HasValue) throw new ArgumentException("You cannot change the username of a message."); if (this.AvatarUrl.HasValue) throw new ArgumentException("You cannot change the avatar of a message."); } else if (isFollowup) { if (this.Username.HasValue) throw new ArgumentException("You cannot change the username of a follow up message."); if (this.AvatarUrl.HasValue) throw new ArgumentException("You cannot change the avatar of a follow up message."); } else if (isInteractionResponse) { if (this.Username.HasValue) throw new ArgumentException("You cannot change the username of an interaction response."); if (this.AvatarUrl.HasValue) throw new ArgumentException("You cannot change the avatar of an interaction response."); } else { if (this.Files?.Count == 0 && string.IsNullOrEmpty(this.Content) && !this.Embeds.Any()) throw new ArgumentException("You must specify content, an embed, or at least one file."); } } } } diff --git a/DisCatSharp/Enums/Discord/DiscordDomain.cs b/DisCatSharp/Enums/Discord/DiscordDomain.cs index 50263b720..104e22fad 100644 --- a/DisCatSharp/Enums/Discord/DiscordDomain.cs +++ b/DisCatSharp/Enums/Discord/DiscordDomain.cs @@ -1,269 +1,269 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Linq; namespace DisCatSharp.Enums { /// /// Core Domains /// public enum CoreDomain { /// /// dis.gd /// [DomainHelp("Marketing URL shortner", "dis.gd")] DiscordMarketing = 1, /// /// discord.co /// [DomainHelp("Admin panel, internal tools", "discord.co")] DiscordAdmin = 2, /// /// discord.com /// [DomainHelp("New app, marketing website, API host", "discord.com")] Discord = 3, /// /// discord.design /// [DomainHelp("Dribbble profile shortlink", "discord.design")] DiscordDesign = 4, /// /// discord.dev /// [DomainHelp("Developer site shortlinks", "discord.dev")] DiscordDev = 5, /// /// discord.gg /// [DomainHelp("Invite shortlinks", "discord.gg")] DiscordShortlink = 6, /// /// discord.gift /// [DomainHelp("Gift shortlinks", "discord.gift")] DiscordGift = 7, /// /// discord.media /// [DomainHelp("Voice servers", "discord.media")] DiscordMedia = 8, /// /// discord.new /// [DomainHelp("Template shortlinks", "discord.new")] DiscordTemplate = 9, /// /// discord.store /// [DomainHelp("Merch store", "discord.store")] DiscordMerch = 10, /// /// discord.tools /// [DomainHelp("Internal tools", "discord.tools")] DiscordTools = 11, /// /// discordapp.com /// [DomainHelp("Old app, marketing website, and API; CDN", "discordapp.com")] DiscordAppOld = 12, /// /// discordapp.net /// [DomainHelp("Media Proxy", "discordapp.net")] DiscordAppMediaProxy = 13, /// /// discordmerch.com /// [DomainHelp("Merch store", "discordmerch.com")] DiscordMerchOld = 14, /// /// discordpartygames.com /// [DomainHelp("Voice channel activity API host", "discordpartygames.com")] DiscordActivityAlt = 15, /// /// discord-activities.com /// [DomainHelp("Voice channel activity API host", "discord-activities.com")] DiscordActivityAlt2 = 16, /// /// discordsays.com /// [DomainHelp("Voice channel activity host", "discordsays.com")] DiscordActivity = 17, /// /// discordstatus.com /// [DomainHelp("Status page", "discordstatus.com")] DiscordStatus = 18, /// /// cdn.discordapp.com /// [DomainHelp("CDN", "cdn.discordapp.com")] DiscordCdn = 19, } /// /// Other Domains /// public enum OtherDomain { /// /// airhorn.solutions /// [DomainHelp("API implementation example", "airhorn.solutions")] Airhorn = 1, /// /// airhornbot.com /// [DomainHelp("API implementation example", "airhornbot.com")] AirhornAlt = 2, /// /// bigbeans.solutions /// [DomainHelp("April Fools 2017", "bigbeans.solutions")] AprilFools = 3, /// /// watchanimeattheoffice.com /// [DomainHelp("HypeSquad form placeholder/meme", "watchanimeattheoffice.com")] HypeSquadMeme = 4 } /// /// Core Domains /// public enum UnusedDomain { /// /// discordapp.io /// [Obsolete("Not in use", false)] [DomainHelp("IO domain for discord", "discordapp.io")] DiscordAppIo = 1, /// /// discordcdn.com /// [Obsolete("Not in use", false)] [DomainHelp("Alternative CDN domain", "discordcdn.com")] DiscordCdnCom = 2 } /// /// Represents a discord domain. /// public static class DiscordDomain { /// /// Gets a domain. /// Valid types: , and . /// /// The domain type. /// A DomainHelpAttribute. public static DomainHelpAttribute GetDomain(Enum domainEnum) { if (domainEnum is not CoreDomain && domainEnum is not OtherDomain && domainEnum is not UnusedDomain) throw new NotSupportedException($"Invalid type. Found: {domainEnum.GetType()} Expected: CoreDomain or OtherDomain or UnusedDomain"); if (domainEnum is CoreDomain domain && (domain == CoreDomain.DiscordAdmin || domain == CoreDomain.DiscordTools)) throw new UnauthorizedAccessException("You don't have access to this domains"); var memberInfo = domainEnum.GetType().GetMember(domainEnum.ToString()).FirstOrDefault(); if (memberInfo != null) { var attribute = (DomainHelpAttribute)memberInfo.GetCustomAttributes(typeof(DomainHelpAttribute), false).FirstOrDefault(); return attribute; } return null; } } /// /// Defines a description and url for this domain. /// [AttributeUsage(AttributeTargets.All, AllowMultiple = false)] public class DomainHelpAttribute : Attribute { /// /// Gets the Description for this domain. /// public string Description { get; } /// /// Gets the Uri for this domain. /// public Uri Uri { get; } /// /// Gets the Domain for this domain. /// public string Domain { get; } /// /// Gets the Url for this domain. /// public string Url { get; } /// /// Defines a description and URIs for this domain. /// /// Description for this domain. /// Url for this domain. public DomainHelpAttribute(string desc, string domain) { this.Description = desc; this.Domain = domain; var url = $"https://{domain}"; this.Url = url; - this.Uri = new(url); + this.Uri = new Uri(url); } } } diff --git a/DisCatSharp/Enums/OAuth.cs b/DisCatSharp/Enums/OAuth.cs index f0606566f..8642c175f 100644 --- a/DisCatSharp/Enums/OAuth.cs +++ b/DisCatSharp/Enums/OAuth.cs @@ -1,120 +1,118 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // ReSharper disable InconsistentNaming namespace DisCatSharp.Enums { /// /// The oauth scopes. /// public static class OAuth { /// /// The default scopes for bots. /// private const string BOT_DEFAULT = "bot applications.commands applications.commands.permissions.update"; /// /// The bot minimal scopes. /// private const string BOT_MINIMAL = "bot applications.commands"; /// /// The bot only scope. /// private const string BOT_ONLY = "bot"; /// /// The basic identify scopes. /// private const string IDENTIFY_BASIC = "identify email"; /// /// The extended identify scopes. /// private const string IDENTIFY_EXTENDED = "identify email guilds connections"; /// /// All scopes for bots and identify. /// private const string ALL = BOT_DEFAULT + " " + IDENTIFY_EXTENDED; /// /// The oauth scope. /// /// /// Resolves the scopes. /// /// The scope. /// A string representing the scopes. - public static string ResolveScopes(OAuthScopes scope) - { - return scope switch + public static string ResolveScopes(OAuthScopes scope) => + scope switch { OAuthScopes.BOT_DEFAULT => BOT_DEFAULT, OAuthScopes.BOT_MINIMAL => BOT_MINIMAL, OAuthScopes.BOT_ONLY => BOT_ONLY, OAuthScopes.IDENTIFY_BASIC => IDENTIFY_BASIC, OAuthScopes.IDENTIFY_EXTENDED => IDENTIFY_EXTENDED, OAuthScopes.ALL => ALL, _ => BOT_DEFAULT, }; - } } /// /// The oauth scopes. /// public enum OAuthScopes { /// /// Scopes: bot applications.commands applications.commands.permissions.update /// BOT_DEFAULT = 0, /// /// Scopes: bot applications.commands /// BOT_MINIMAL = 1, /// /// Scopes: bot /// BOT_ONLY = 2, /// /// Scopes: identify email /// IDENTIFY_BASIC = 3, /// /// Scopes: identify email guilds connections /// IDENTIFY_EXTENDED = 4, /// /// Scopes: bot applications.commands applications.commands.permissions.update identify email guilds connections /// ALL = 5 } } diff --git a/DisCatSharp/Formatter.cs b/DisCatSharp/Formatter.cs index 9efca18d2..398adba10 100644 --- a/DisCatSharp/Formatter.cs +++ b/DisCatSharp/Formatter.cs @@ -1,205 +1,205 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Globalization; using System.Text.RegularExpressions; using DisCatSharp.Entities; namespace DisCatSharp { /// /// Contains markdown formatting helpers. /// public static class Formatter { /// /// Gets the md sanitize regex. /// - private static Regex s_mdSanitizeRegex { get; } = new Regex(@"([`\*_~<>\[\]\(\)""@\!\&#:\|])", RegexOptions.ECMAScript); + private static Regex s_mdSanitizeRegex { get; } = new(@"([`\*_~<>\[\]\(\)""@\!\&#:\|])", RegexOptions.ECMAScript); /// /// Gets the md strip regex. /// - private static Regex s_mdStripRegex { get; } = new Regex(@"([`\*_~\[\]\(\)""\|]|<@\!?\d+>|<#\d+>|<@\&\d+>|<:[a-zA-Z0-9_\-]:\d+>)", RegexOptions.ECMAScript); + private static Regex s_mdStripRegex { get; } = new(@"([`\*_~\[\]\(\)""\|]|<@\!?\d+>|<#\d+>|<@\&\d+>|<:[a-zA-Z0-9_\-]:\d+>)", RegexOptions.ECMAScript); /// /// Creates a block of code. /// /// Contents of the block. /// Language to use for highlighting. /// Formatted block of code. public static string BlockCode(string content, string language = "") => $"```{language}\n{content}\n```"; /// /// Creates inline code snippet. /// /// Contents of the snippet. /// Formatted inline code snippet. public static string InlineCode(string content) => $"`{content}`"; /// /// Creates a rendered timestamp. /// /// The time from now. /// The format to render the timestamp in. Defaults to relative. /// A formatted timestamp. public static string Timestamp(TimeSpan time, TimestampFormat format = TimestampFormat.RelativeTime) => Timestamp(DateTimeOffset.UtcNow + time, format); /// /// Creates a rendered timestamp. /// /// Timestamp to format. /// The format to render the timestamp in. Defaults to relative. /// A formatted timestamp. public static string Timestamp(DateTimeOffset time, TimestampFormat format = TimestampFormat.RelativeTime) => $""; /// /// Creates a rendered timestamp. /// /// The time from now. /// The format to render the timestamp in. Defaults to relative. /// A formatted timestamp relative to now. public static string Timestamp(DateTime time, TimestampFormat format = TimestampFormat.RelativeTime) => Timestamp(time.ToUniversalTime() - DateTime.UtcNow, format); /// /// Creates bold text. /// /// Text to bolden. /// Formatted text. public static string Bold(string content) => $"**{content}**"; /// /// Creates italicized text. /// /// Text to italicize. /// Formatted text. public static string Italic(string content) => $"*{content}*"; /// /// Creates spoiler from text. /// /// Text to spoilerize. /// Formatted text. public static string Spoiler(string content) => $"||{content}||"; /// /// Creates underlined text. /// /// Text to underline. /// Formatted text. public static string Underline(string content) => $"__{content}__"; /// /// Creates strikethrough text. /// /// Text to strikethrough. /// Formatted text. public static string Strike(string content) => $"~~{content}~~"; /// /// Creates a URL that won't create a link preview. /// /// Url to prevent from being previewed. /// Formatted url. public static string EmbedlessUrl(Uri url) => $"<{url}>"; /// /// Creates a masked link. This link will display as specified text, and alternatively provided alt text. This can only be used in embeds. /// /// Text to display the link as. /// Url that the link will lead to. /// Alt text to display on hover. /// Formatted url. public static string MaskedUrl(string text, Uri url, string altText = "") => $"[{text}]({url}{(!string.IsNullOrWhiteSpace(altText) ? $" \"{altText}\"" : "")})"; /// /// Escapes all markdown formatting from specified text. /// /// Text to sanitize. /// Sanitized text. public static string Sanitize(string text) => s_mdSanitizeRegex.Replace(text, m => $"\\{m.Groups[1].Value}"); /// /// Removes all markdown formatting from specified text. /// /// Text to strip of formatting. /// Formatting-stripped text. public static string Strip(string text) => s_mdStripRegex.Replace(text, m => string.Empty); /// /// Creates a mention for specified user or member. Can optionally specify to resolve nicknames. /// /// User to create mention for. /// Whether the mention should resolve nicknames or not. /// Formatted mention. public static string Mention(DiscordUser user, bool nickname = false) => nickname ? $"<@!{user.Id.ToString(CultureInfo.InvariantCulture)}>" : $"<@{user.Id.ToString(CultureInfo.InvariantCulture)}>"; /// /// Creates a mention for specified channel. /// /// Channel to mention. /// Formatted mention. public static string Mention(DiscordChannel channel) => $"<#{channel.Id.ToString(CultureInfo.InvariantCulture)}>"; /// /// Creates a mention for specified role. /// /// Role to mention. /// Formatted mention. public static string Mention(DiscordRole role) => $"<@&{role.Id.ToString(CultureInfo.InvariantCulture)}>"; /// /// Creates a custom emoji string. /// /// Emoji to display. /// Formatted emoji. public static string Emoji(DiscordEmoji emoji) => $"<:{emoji.Name}:{emoji.Id.ToString(CultureInfo.InvariantCulture)}>"; /// /// Creates a url for using attachments in embeds. This can only be used as an Image URL, Thumbnail URL, Author icon URL or Footer icon URL. /// /// Name of attached image to display /// public static string AttachedImageUrl(string filename) => $"attachment://{filename}"; } } diff --git a/DisCatSharp/Logging/DefaultLoggerFactory.cs b/DisCatSharp/Logging/DefaultLoggerFactory.cs index 222e7fc30..b616c9d7b 100644 --- a/DisCatSharp/Logging/DefaultLoggerFactory.cs +++ b/DisCatSharp/Logging/DefaultLoggerFactory.cs @@ -1,74 +1,72 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Generic; using Microsoft.Extensions.Logging; namespace DisCatSharp { /// /// Represents a default logger factory. /// internal class DefaultLoggerFactory : ILoggerFactory { /// /// Gets the providers. /// private readonly List _providers = new(); - private bool _isDisposed = false; + private bool _isDisposed; /// /// Adds a provider. /// /// The provider to be added. public void AddProvider(ILoggerProvider provider) => this._providers.Add(provider); /// /// Creates the logger. /// /// The category name. - public ILogger CreateLogger(string categoryName) - { - return this._isDisposed + public ILogger CreateLogger(string categoryName) => + this._isDisposed ? throw new InvalidOperationException("This logger factory is already disposed.") : categoryName != typeof(BaseDiscordClient).FullName && categoryName != typeof(DiscordWebhookClient).FullName - ? throw new ArgumentException($"This factory can only provide instances of loggers for {typeof(BaseDiscordClient).FullName} or {typeof(DiscordWebhookClient).FullName}.", nameof(categoryName)) - : new CompositeDefaultLogger(this._providers); - } + ? throw new ArgumentException($"This factory can only provide instances of loggers for {typeof(BaseDiscordClient).FullName} or {typeof(DiscordWebhookClient).FullName}.", nameof(categoryName)) + : new CompositeDefaultLogger(this._providers); /// /// Disposes the logger. /// public void Dispose() { if (this._isDisposed) return; this._isDisposed = true; foreach (var provider in this._providers) provider.Dispose(); this._providers.Clear(); } } } diff --git a/DisCatSharp/Logging/DefaultLoggerProvider.cs b/DisCatSharp/Logging/DefaultLoggerProvider.cs index 94ccb120d..08dc086fb 100644 --- a/DisCatSharp/Logging/DefaultLoggerProvider.cs +++ b/DisCatSharp/Logging/DefaultLoggerProvider.cs @@ -1,90 +1,88 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using Microsoft.Extensions.Logging; namespace DisCatSharp { /// /// Represents a default logger provider. /// internal class DefaultLoggerProvider : ILoggerProvider { /// /// Gets the minimum log level. /// private readonly LogLevel _minimumLevel; /// /// Gets the timestamp format. /// private readonly string _timestampFormat; - private bool _isDisposed = false; + private bool _isDisposed; /// /// Initializes a new instance of the class. /// /// The client. internal DefaultLoggerProvider(BaseDiscordClient client) : this(client.Configuration.MinimumLogLevel, client.Configuration.LogTimestampFormat) { } /// /// Initializes a new instance of the class. /// /// The client. internal DefaultLoggerProvider(DiscordWebhookClient client) : this(client.MinimumLogLevel, client.LogTimestampFormat) { } /// /// Initializes a new instance of the class. /// /// The min level. /// The timestamp format. internal DefaultLoggerProvider(LogLevel minLevel = LogLevel.Information, string timestampFormat = "yyyy-MM-dd HH:mm:ss zzz") { this._minimumLevel = minLevel; this._timestampFormat = timestampFormat; } /// /// Creates the logger. /// /// The category name. - public ILogger CreateLogger(string categoryName) - { - return this._isDisposed + public ILogger CreateLogger(string categoryName) => + this._isDisposed ? throw new InvalidOperationException("This logger provider is already disposed.") : categoryName != typeof(BaseDiscordClient).FullName && categoryName != typeof(DiscordWebhookClient).FullName - ? throw new ArgumentException($"This provider can only provide instances of loggers for {typeof(BaseDiscordClient).FullName} or {typeof(DiscordWebhookClient).FullName}.", nameof(categoryName)) - : new DefaultLogger(this._minimumLevel, this._timestampFormat); - } + ? throw new ArgumentException($"This provider can only provide instances of loggers for {typeof(BaseDiscordClient).FullName} or {typeof(DiscordWebhookClient).FullName}.", nameof(categoryName)) + : new DefaultLogger(this._minimumLevel, this._timestampFormat); /// /// Disposes the logger. /// public void Dispose() => this._isDisposed = true; } } diff --git a/DisCatSharp/Logging/LoggerEvents.cs b/DisCatSharp/Logging/LoggerEvents.cs index ea9420b0a..5d940a8f8 100644 --- a/DisCatSharp/Logging/LoggerEvents.cs +++ b/DisCatSharp/Logging/LoggerEvents.cs @@ -1,172 +1,172 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using Microsoft.Extensions.Logging; namespace DisCatSharp { /// /// Contains well-defined event IDs used by core of DisCatSharp. /// public static class LoggerEvents { /// /// Miscellaneous events, that do not fit in any other category. /// - public static EventId Misc { get; } = new EventId(100, "DisCatSharp"); + public static EventId Misc { get; } = new(100, "DisCatSharp"); /// /// Events pertaining to startup tasks. /// - public static EventId Startup { get; } = new EventId(101, nameof(Startup)); + public static EventId Startup { get; } = new(101, nameof(Startup)); /// /// Events typically emitted whenever WebSocket connections fail or are terminated. /// - public static EventId ConnectionFailure { get; } = new EventId(102, nameof(ConnectionFailure)); + public static EventId ConnectionFailure { get; } = new(102, nameof(ConnectionFailure)); /// /// Events pertaining to Discord-issued session state updates. /// - public static EventId SessionUpdate { get; } = new EventId(103, nameof(SessionUpdate)); + public static EventId SessionUpdate { get; } = new(103, nameof(SessionUpdate)); /// /// Events emitted when exceptions are thrown in handlers attached to async events. /// - public static EventId EventHandlerException { get; } = new EventId(104, nameof(EventHandlerException)); + public static EventId EventHandlerException { get; } = new(104, nameof(EventHandlerException)); /// /// Events emitted for various high-level WebSocket receive events. /// - public static EventId WebSocketReceive { get; } = new EventId(105, nameof(WebSocketReceive)); + public static EventId WebSocketReceive { get; } = new(105, nameof(WebSocketReceive)); /// /// Events emitted for various low-level WebSocket receive events. /// - public static EventId WebSocketReceiveRaw { get; } = new EventId(106, nameof(WebSocketReceiveRaw)); + public static EventId WebSocketReceiveRaw { get; } = new(106, nameof(WebSocketReceiveRaw)); /// /// Events emitted for various low-level WebSocket send events. /// - public static EventId WebSocketSendRaw { get; } = new EventId(107, nameof(WebSocketSendRaw)); + public static EventId WebSocketSendRaw { get; } = new(107, nameof(WebSocketSendRaw)); /// /// Events emitted for various WebSocket payload processing failures, typically when deserialization or decoding fails. /// - public static EventId WebSocketReceiveFailure { get; } = new EventId(108, nameof(WebSocketReceiveFailure)); + public static EventId WebSocketReceiveFailure { get; } = new(108, nameof(WebSocketReceiveFailure)); /// /// Events pertaining to connection lifecycle, specifically, heartbeats. /// - public static EventId Heartbeat { get; } = new EventId(109, nameof(Heartbeat)); + public static EventId Heartbeat { get; } = new(109, nameof(Heartbeat)); /// /// Events pertaining to various heartbeat failures, typically fatal. /// - public static EventId HeartbeatFailure { get; } = new EventId(110, nameof(HeartbeatFailure)); + public static EventId HeartbeatFailure { get; } = new(110, nameof(HeartbeatFailure)); /// /// Events pertaining to clean connection closes. /// - public static EventId ConnectionClose { get; } = new EventId(111, nameof(ConnectionClose)); + public static EventId ConnectionClose { get; } = new(111, nameof(ConnectionClose)); /// /// Events emitted when REST processing fails for any reason. /// - public static EventId RestError { get; } = new EventId(112, nameof(RestError)); + public static EventId RestError { get; } = new(112, nameof(RestError)); /// /// Events pertaining to the shard startup. /// - public static EventId ShardStartup { get; } = new EventId(113, nameof(ShardStartup)); + public static EventId ShardStartup { get; } = new(113, nameof(ShardStartup)); /// /// Events pertaining to ratelimit exhaustion. /// - public static EventId RatelimitHit { get; } = new EventId(114, nameof(RatelimitHit)); + public static EventId RatelimitHit { get; } = new(114, nameof(RatelimitHit)); /// /// Events pertaining to ratelimit diagnostics. Typically contain raw bucket info. /// - public static EventId RatelimitDiag { get; } = new EventId(115, nameof(RatelimitDiag)); + public static EventId RatelimitDiag { get; } = new(115, nameof(RatelimitDiag)); /// /// Events emitted when a ratelimit is exhausted and a request is preemtively blocked. /// - public static EventId RatelimitPreemptive { get; } = new EventId(116, nameof(RatelimitPreemptive)); + public static EventId RatelimitPreemptive { get; } = new(116, nameof(RatelimitPreemptive)); /// /// Events pertaining to audit log processing. /// - public static EventId AuditLog { get; } = new EventId(117, nameof(AuditLog)); + public static EventId AuditLog { get; } = new(117, nameof(AuditLog)); /// /// Events containing raw (but decompressed) payloads, received from Discord Gateway. /// - public static EventId GatewayWsRx { get; } = new EventId(118, "Gateway ↓"); + public static EventId GatewayWsRx { get; } = new(118, "Gateway ↓"); /// /// Events containing raw payloads, as they're being sent to Discord Gateway. /// - public static EventId GatewayWsTx { get; } = new EventId(119, "Gateway ↑"); + public static EventId GatewayWsTx { get; } = new(119, "Gateway ↑"); /// /// Events pertaining to Gateway Intents. Typically diagnostic information. /// - public static EventId Intents { get; } = new EventId(120, nameof(Intents)); + public static EventId Intents { get; } = new(120, nameof(Intents)); /// /// Events pertaining to autosharded client shard shutdown, clean or otherwise. /// - public static EventId ShardShutdown { get; } = new EventId(121, nameof(ShardShutdown)); + public static EventId ShardShutdown { get; } = new(121, nameof(ShardShutdown)); /// /// Events pertaining to the 's shards not initializing correctly. /// - public static EventId ShardClientError { get; } = new EventId(122, nameof(ShardClientError)); + public static EventId ShardClientError { get; } = new(122, nameof(ShardClientError)); /// /// Events containing raw payloads, as they're received from Discord's REST API. /// - public static EventId RestRx { get; } = new EventId(123, "REST ↓"); + public static EventId RestRx { get; } = new(123, "REST ↓"); /// /// Events containing raw payloads, as they're sent to Discord's REST API. /// - public static EventId RestTx { get; } = new EventId(124, "REST ↑"); + public static EventId RestTx { get; } = new(124, "REST ↑"); /// /// Event is rest cleaner. /// - public static EventId RestCleaner { get; } = new EventId(125, nameof(RestCleaner)); + public static EventId RestCleaner { get; } = new(125, nameof(RestCleaner)); /// /// Event is rest hash mover. /// - public static EventId RestHashMover { get; } = new EventId(126, nameof(RestHashMover)); + public static EventId RestHashMover { get; } = new(126, nameof(RestHashMover)); /// /// Events pertaining to Discord API requests from the . /// - public static EventId ShardRest { get; } = new EventId(127, nameof(ShardRest)); + public static EventId ShardRest { get; } = new(127, nameof(ShardRest)); } } diff --git a/DisCatSharp/Logging/ShardedLoggerFactory.cs b/DisCatSharp/Logging/ShardedLoggerFactory.cs index 162d402c8..14db9db60 100644 --- a/DisCatSharp/Logging/ShardedLoggerFactory.cs +++ b/DisCatSharp/Logging/ShardedLoggerFactory.cs @@ -1,70 +1,68 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using Microsoft.Extensions.Logging; namespace DisCatSharp { /// /// Represents a sharded logger factory. /// internal class ShardedLoggerFactory : ILoggerFactory { /// /// Gets the logger. /// private readonly ILogger _logger; /// /// Initializes a new instance of the class. /// /// The instance. public ShardedLoggerFactory(ILogger instance) { this._logger = instance; } /// /// Adds a provider. /// /// The provider to be added. public void AddProvider(ILoggerProvider provider) => throw new InvalidOperationException("This is a passthrough logger container, it cannot register new providers."); /// /// Creates a logger. /// /// The category name. - public ILogger CreateLogger(string categoryName) - { - return categoryName != typeof(BaseDiscordClient).FullName + public ILogger CreateLogger(string categoryName) => + categoryName != typeof(BaseDiscordClient).FullName ? throw new ArgumentException($"This factory can only provide instances of loggers for {typeof(BaseDiscordClient).FullName}.", nameof(categoryName)) : this._logger; - } /// /// Disposes the logger. /// public void Dispose() { } } } diff --git a/DisCatSharp/Net/Abstractions/Gateway/GatewayIdentifyResume.cs b/DisCatSharp/Net/Abstractions/Gateway/GatewayIdentifyResume.cs index 0b88b2a5d..70d6636ec 100644 --- a/DisCatSharp/Net/Abstractions/Gateway/GatewayIdentifyResume.cs +++ b/DisCatSharp/Net/Abstractions/Gateway/GatewayIdentifyResume.cs @@ -1,105 +1,105 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using Newtonsoft.Json; namespace DisCatSharp.Net.Abstractions { /// /// Represents data for websocket identify payload. /// internal sealed class GatewayIdentify { /// /// Gets or sets the discord client. /// [JsonIgnore] public BaseDiscordClient Discord { get; set; } /// /// Gets or sets the token used to identify the client to Discord. /// [JsonProperty("token")] public string Token { get; set; } /// /// Gets or sets the client's properties. /// [JsonProperty("properties")] public ClientProperties ClientProperties => new() { Discord = this.Discord }; /// /// Gets or sets whether to encrypt websocket traffic. /// [JsonProperty("compress")] public bool Compress { get; set; } /// /// Gets or sets the member count at which the guild is to be considered large. /// [JsonProperty("large_threshold")] public int LargeThreshold { get; set; } /// /// Gets or sets the shard info for this connection. /// [JsonProperty("shard")] public ShardInfo ShardInfo { get; set; } /// /// Gets or sets the presence for this connection. /// [JsonProperty("presence", NullValueHandling = NullValueHandling.Ignore)] - public StatusUpdate Presence { get; set; } = null; + public StatusUpdate Presence { get; set; } /// /// Gets or sets the intent flags for this connection. /// [JsonProperty("intents")] public DiscordIntents Intents { get; set; } } /// /// Represents data for websocket identify payload. /// internal sealed class GatewayResume { /// /// Gets or sets the token used to identify the client to Discord. /// [JsonProperty("token")] public string Token { get; set; } /// /// Gets or sets the session id used to resume last session. /// [JsonProperty("session_id")] public string SessionId { get; set; } /// /// Gets or sets the last received sequence number. /// [JsonProperty("seq")] public long SequenceNumber { get; set; } } } diff --git a/DisCatSharp/Net/Abstractions/Gateway/GatewayRequestGuildMembers.cs b/DisCatSharp/Net/Abstractions/Gateway/GatewayRequestGuildMembers.cs index c32972b33..f779b423f 100644 --- a/DisCatSharp/Net/Abstractions/Gateway/GatewayRequestGuildMembers.cs +++ b/DisCatSharp/Net/Abstractions/Gateway/GatewayRequestGuildMembers.cs @@ -1,79 +1,79 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System.Collections.Generic; using DisCatSharp.Entities; using Newtonsoft.Json; namespace DisCatSharp.Net.Abstractions { /// /// Request guild members. /// internal sealed class GatewayRequestGuildMembers { /// /// Gets the guild id. /// [JsonProperty("guild_id")] public ulong GuildId { get; } /// /// Gets the query. /// [JsonProperty("query", NullValueHandling = NullValueHandling.Ignore)] - public string Query { get; set; } = null; + public string Query { get; set; } /// /// Gets the limit. /// [JsonProperty("limit")] - public int Limit { get; set; } = 0; + public int Limit { get; set; } /// /// Gets whether presences should be returned. /// [JsonProperty("presences", NullValueHandling = NullValueHandling.Ignore)] - public bool? Presences { get; set; } = null; + public bool? Presences { get; set; } /// /// Gets the user ids. /// [JsonProperty("user_ids", NullValueHandling = NullValueHandling.Ignore)] - public IEnumerable UserIds { get; set; } = null; + public IEnumerable UserIds { get; set; } /// /// Gets the nonce. /// [JsonProperty("nonce", NullValueHandling = NullValueHandling.Ignore)] public string Nonce { get; internal set; } /// /// Initializes a new instance of the class. /// /// The guild. public GatewayRequestGuildMembers(DiscordGuild guild) { this.GuildId = guild.Id; } } } diff --git a/DisCatSharp/Net/Abstractions/ShardInfo.cs b/DisCatSharp/Net/Abstractions/ShardInfo.cs index fc666f1fa..f7af67fa2 100644 --- a/DisCatSharp/Net/Abstractions/ShardInfo.cs +++ b/DisCatSharp/Net/Abstractions/ShardInfo.cs @@ -1,99 +1,97 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace DisCatSharp.Net.Abstractions { /// /// Represents data for identify payload's shard info. /// [JsonConverter(typeof(ShardInfoConverter))] internal sealed class ShardInfo { /// /// Gets or sets this client's shard id. /// public int ShardId { get; set; } /// /// Gets or sets the total shard count for this token. /// public int ShardCount { get; set; } } /// /// Represents a shard info converter. /// internal sealed class ShardInfoConverter : JsonConverter { /// /// Writes the json. /// /// The writer. /// The value. /// The serializer. public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { var sinfo = value as ShardInfo; var obj = new object[] { sinfo.ShardId, sinfo.ShardCount }; serializer.Serialize(writer, obj); } /// /// 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) { var arr = this.ReadArrayObject(reader, serializer); return new ShardInfo { ShardId = (int)arr[0], ShardCount = (int)arr[1], }; } /// /// Reads the array object. /// /// The reader. /// The serializer. - private JArray ReadArrayObject(JsonReader reader, JsonSerializer serializer) - { - return serializer.Deserialize(reader) is not JArray arr || arr.Count != 2 + private JArray ReadArrayObject(JsonReader reader, JsonSerializer serializer) => + serializer.Deserialize(reader) is not JArray arr || arr.Count != 2 ? throw new JsonSerializationException("Expected array of length 2") : arr; - } /// /// Whether this can be converted. /// /// The object type. public override bool CanConvert(Type objectType) => objectType == typeof(ShardInfo); } } diff --git a/DisCatSharp/Net/Abstractions/StatusUpdate.cs b/DisCatSharp/Net/Abstractions/StatusUpdate.cs index ac86d3100..288e12ac8 100644 --- a/DisCatSharp/Net/Abstractions/StatusUpdate.cs +++ b/DisCatSharp/Net/Abstractions/StatusUpdate.cs @@ -1,79 +1,74 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using DisCatSharp.Entities; using Newtonsoft.Json; namespace DisCatSharp.Net.Abstractions { /// /// Represents data for websocket status update payload. /// internal sealed class StatusUpdate { /// /// Gets or sets the unix millisecond timestamp of when the user went idle. /// [JsonProperty("since", NullValueHandling = NullValueHandling.Include)] public long? IdleSince { get; set; } /// /// Gets or sets whether the user is AFK. /// [JsonProperty("afk")] public bool IsAfk { get; set; } /// /// Gets or sets the status of the user. /// [JsonIgnore] public UserStatus Status { get; set; } = UserStatus.Online; /// /// Gets the status string of the user. /// [JsonProperty("status")] - internal string StatusString - { - get + internal string StatusString => + this.Status switch { - return this.Status switch - { - UserStatus.Online => "online", - UserStatus.Idle => "idle", - UserStatus.DoNotDisturb => "dnd", - UserStatus.Invisible or UserStatus.Offline => "invisible", - UserStatus.Streaming => "streaming", - _ => "online", - }; - } - } + UserStatus.Online => "online", + UserStatus.Idle => "idle", + UserStatus.DoNotDisturb => "dnd", + UserStatus.Invisible or UserStatus.Offline => "invisible", + UserStatus.Streaming => "streaming", + _ => "online", + }; /// /// Gets or sets the game the user is playing. /// [JsonProperty("game", NullValueHandling = NullValueHandling.Ignore)] public TransportActivity Activity { get; set; } internal DiscordActivity ActivityInternal; } } diff --git a/DisCatSharp/Net/Abstractions/Transport/TransportActivity.cs b/DisCatSharp/Net/Abstractions/Transport/TransportActivity.cs index d7357f0fc..4443c7b27 100644 --- a/DisCatSharp/Net/Abstractions/Transport/TransportActivity.cs +++ b/DisCatSharp/Net/Abstractions/Transport/TransportActivity.cs @@ -1,374 +1,372 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Generic; using System.Globalization; using DisCatSharp.Entities; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace DisCatSharp.Net.Abstractions { /// /// Represents a game a user is playing. /// internal sealed class TransportActivity { /// /// Gets or sets the id of user's activity. /// [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)] public string Id { get; internal set; } /// /// Gets or sets the name of the game the user is playing. /// [JsonProperty("name", NullValueHandling = NullValueHandling.Include)] public string Name { get; internal set; } /// /// Gets or sets the stream URI, if applicable. /// [JsonProperty("url", NullValueHandling = NullValueHandling.Ignore)] public string StreamUrl { get; internal set; } /// /// Gets or sets the livesteam type. /// [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] public ActivityType ActivityType { get; internal set; } /// /// Gets or sets the details. /// /// This is a component of the rich presence, and, as such, can only be used by regular users. /// [JsonProperty("details", NullValueHandling = NullValueHandling.Ignore)] public string Details { get; internal set; } /// /// Gets or sets game state. /// /// This is a component of the rich presence, and, as such, can only be used by regular users. /// [JsonProperty("state", NullValueHandling = NullValueHandling.Ignore)] public string State { get; internal set; } /// /// Gets the emoji details for a custom status, if any. /// [JsonProperty("emoji", NullValueHandling = NullValueHandling.Ignore)] public DiscordEmoji Emoji { get; internal set; } /// /// Gets ID of the application for which this rich presence is for. /// /// This is a component of the rich presence, and, as such, can only be used by regular users. /// [JsonIgnore] public ulong? ApplicationId { get => this.ApplicationIdStr != null ? (ulong?)ulong.Parse(this.ApplicationIdStr, CultureInfo.InvariantCulture) : null; internal set => this.ApplicationIdStr = value?.ToString(CultureInfo.InvariantCulture); } /// /// Gets or sets the application id string. /// [JsonProperty("application_id", NullValueHandling = NullValueHandling.Ignore)] internal string ApplicationIdStr { get; set; } /// /// Gets or sets instance status. /// /// This is a component of the rich presence, and, as such, can only be used by regular users. /// [JsonProperty("instance", NullValueHandling = NullValueHandling.Ignore)] public bool? Instance { get; internal set; } /// /// Gets or sets information about the current game's party. /// /// This is a component of the rich presence, and, as such, can only be used by regular users. /// [JsonProperty("party", NullValueHandling = NullValueHandling.Ignore)] public GameParty Party { get; internal set; } /// /// Gets or sets information about assets related to this rich presence. /// /// This is a component of the rich presence, and, as such, can only be used by regular users. /// [JsonProperty("assets", NullValueHandling = NullValueHandling.Ignore)] public PresenceAssets Assets { get; internal set; } /// /// Gets or sets information about buttons in this rich presence. /// /// This is a component of the rich presence, and, as such, can only be used by regular users. /// [JsonProperty("buttons", NullValueHandling = NullValueHandling.Ignore)] public IReadOnlyList Buttons { get; internal set; } /// /// Gets or sets platform in this rich presence. /// /// This is a component of the rich presence, and, as such, can only be used by regular users. /// [JsonProperty("platform", NullValueHandling = NullValueHandling.Ignore)] public string Platform { get; internal set; } /// /// Gets or sets sync_id in this rich presence. /// /// This is a component of the rich presence, and, as such, can only be used by regular users. /// [JsonProperty("sync_id", NullValueHandling = NullValueHandling.Ignore)] public string SyncId { get; internal set; } /// /// Gets or sets session_id in this rich presence. /// /// This is a component of the rich presence, and, as such, can only be used by regular users. /// [JsonProperty("session_id", NullValueHandling = NullValueHandling.Ignore)] public string SessionId { get; internal set; } /// /// Gets or sets infromation about current game's timestamps. /// /// This is a component of the rich presence, and, as such, can only be used by regular users. /// [JsonProperty("timestamps", NullValueHandling = NullValueHandling.Ignore)] public GameTimestamps Timestamps { get; internal set; } /// /// Gets or sets information about current game's secret values. /// /// This is a component of the rich presence, and, as such, can only be used by regular users. /// [JsonProperty("secrets", NullValueHandling = NullValueHandling.Ignore)] public GameSecrets Secrets { get; internal set; } /// /// Initializes a new instance of the class. /// internal TransportActivity() { } /// /// Initializes a new instance of the class. /// /// The game. internal TransportActivity(DiscordActivity game) { if (game == null) return; this.Name = game.Name; this.ActivityType = game.ActivityType; this.StreamUrl = game.StreamUrl; } /// /// Whether this activity is a rich presence. /// public bool IsRichPresence() => this.Details != null || this.State != null || this.ApplicationId != null || this.Instance != null || this.Party != null || this.Assets != null || this.Secrets != null || this.Timestamps != null || this.Buttons != null; /// /// Whether this activity is a custom status. /// public bool IsCustomStatus() => this.Name == "Custom Status"; /// /// Represents information about assets attached to a rich presence. /// public class PresenceAssets { /// /// Gets the large image asset ID. /// [JsonProperty("large_image")] public string LargeImage { get; set; } /// /// Gets the large image text. /// [JsonProperty("large_text", NullValueHandling = NullValueHandling.Ignore)] public string LargeImageText { get; internal set; } /// /// Gets the small image asset ID. /// [JsonProperty("small_image")] internal string SmallImage { get; set; } /// /// Gets the small image text. /// [JsonProperty("small_text", NullValueHandling = NullValueHandling.Ignore)] public string SmallImageText { get; internal set; } } /// /// Represents information about rich presence game party. /// public class GameParty { /// /// Gets the game party ID. /// [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)] public string Id { get; internal set; } /// /// Gets the size of the party. /// [JsonProperty("size", NullValueHandling = NullValueHandling.Ignore)] public GamePartySize Size { get; internal set; } /// /// Represents information about party size. /// [JsonConverter(typeof(GamePartySizeConverter))] public class GamePartySize { /// /// Gets the current number of players in the party. /// public long Current { get; internal set; } /// /// Gets the maximum party size. /// public long Maximum { get; internal set; } } } /// /// Represents information about the game state's timestamps. /// public class GameTimestamps { /// /// Gets the time the game has started. /// [JsonIgnore] public DateTimeOffset? Start => this.StartInternal != null ? (DateTimeOffset?)Utilities.GetDateTimeOffsetFromMilliseconds(this.StartInternal.Value, false) : null; [JsonProperty("start", NullValueHandling = NullValueHandling.Ignore)] internal long? StartInternal; /// /// Gets the time the game is going to end. /// [JsonIgnore] public DateTimeOffset? End => this.EndInternal != null ? (DateTimeOffset?)Utilities.GetDateTimeOffsetFromMilliseconds(this.EndInternal.Value, false) : null; [JsonProperty("end", NullValueHandling = NullValueHandling.Ignore)] internal long? EndInternal; } /// /// Represents information about secret values for the Join, Spectate, and Match actions. /// public class GameSecrets { /// /// Gets the secret value for join action. /// [JsonProperty("join", NullValueHandling = NullValueHandling.Ignore)] public string Join { get; internal set; } /// /// Gets the secret value for match action. /// [JsonProperty("match", NullValueHandling = NullValueHandling.Ignore)] public string Match { get; internal set; } /// /// Gets the secret value for spectate action. /// [JsonProperty("spectate", NullValueHandling = NullValueHandling.Ignore)] public string Spectate { get; internal set; } } } /// /// Represents a game party size converter. /// internal sealed class GamePartySizeConverter : JsonConverter { /// /// Writes the json. /// /// The writer. /// The value. /// The serializer. public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { var obj = value is TransportActivity.GameParty.GamePartySize sinfo ? new object[] { sinfo.Current, sinfo.Maximum } : null; serializer.Serialize(writer, obj); } /// /// 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) { var arr = this.ReadArrayObject(reader, serializer); return new TransportActivity.GameParty.GamePartySize { Current = (long)arr[0], Maximum = (long)arr[1], }; } /// /// Reads the array object. /// /// The reader. /// The serializer. - private JArray ReadArrayObject(JsonReader reader, JsonSerializer serializer) - { - return serializer.Deserialize(reader) is not JArray arr || arr.Count != 2 + private JArray ReadArrayObject(JsonReader reader, JsonSerializer serializer) => + serializer.Deserialize(reader) is not JArray arr || arr.Count != 2 ? throw new JsonSerializationException("Expected array of length 2") : arr; - } /// /// Whether it can convert. /// /// The object type. public override bool CanConvert(Type objectType) => objectType == typeof(TransportActivity.GameParty.GamePartySize); } } diff --git a/DisCatSharp/Net/Rest/BaseRestRequest.cs b/DisCatSharp/Net/Rest/BaseRestRequest.cs index b663dda39..37e52d558 100644 --- a/DisCatSharp/Net/Rest/BaseRestRequest.cs +++ b/DisCatSharp/Net/Rest/BaseRestRequest.cs @@ -1,132 +1,132 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace DisCatSharp.Net { /// /// Represents a request sent over HTTP. /// public abstract class BaseRestRequest { /// /// Gets the discord client. /// protected internal BaseDiscordClient Discord { get; } /// /// Gets the request task source. /// protected internal TaskCompletionSource RequestTaskSource { get; } /// /// Gets the url to which this request is going to be made. /// public Uri Url { get; } /// /// Gets the HTTP method used for this request. /// public RestRequestMethod Method { get; } /// /// Gets the generic path (no parameters) for this request. /// public string Route { get; } /// /// Gets the headers sent with this request. /// - public IReadOnlyDictionary Headers { get; } = null; + public IReadOnlyDictionary Headers { get; } /// /// Gets the override for the rate limit bucket wait time. /// public double? RateLimitWaitOverride { get; } /// /// Gets the rate limit bucket this request is in. /// internal RateLimitBucket RateLimitBucket { get; } /// /// Creates a new with specified parameters. /// /// from which this request originated. /// Rate limit bucket to place this request in. /// Uri to which this request is going to be sent to. /// Method to use for this request, /// The generic route the request url will use. /// Additional headers for this request. /// Override for ratelimit bucket wait time. internal BaseRestRequest(BaseDiscordClient client, RateLimitBucket bucket, Uri url, RestRequestMethod method, string route, IReadOnlyDictionary headers = null, double? ratelimitWaitOverride = null) { this.Discord = client; this.RateLimitBucket = bucket; this.RequestTaskSource = new TaskCompletionSource(); this.Url = url; this.Method = method; this.Route = route; this.RateLimitWaitOverride = ratelimitWaitOverride; if (headers != null) { headers = headers.Select(x => new KeyValuePair(x.Key, Uri.EscapeDataString(x.Value))) .ToDictionary(x => x.Key, x => x.Value); this.Headers = headers; } } /// /// Asynchronously waits for this request to complete. /// /// HTTP response to this request. public Task WaitForCompletionAsync() => this.RequestTaskSource.Task; /// /// Sets as completed. /// /// The response to set. protected internal void SetCompleted(RestResponse response) => this.RequestTaskSource.SetResult(response); /// /// Sets as faulted. /// /// The exception to set. protected internal void SetFaulted(Exception ex) => this.RequestTaskSource.SetException(ex); /// /// Tries to set as faulted. /// /// The exception to set. protected internal bool TrySetFaulted(Exception ex) => this.RequestTaskSource.TrySetException(ex); } } diff --git a/DisCatSharp/Net/Rest/DiscordApiClient.cs b/DisCatSharp/Net/Rest/DiscordApiClient.cs index bdc43d6e1..6a78babad 100644 --- a/DisCatSharp/Net/Rest/DiscordApiClient.cs +++ b/DisCatSharp/Net/Rest/DiscordApiClient.cs @@ -1,5411 +1,5411 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Globalization; using System.Linq; using System.Net; using System.Text; using System.Threading.Tasks; using DisCatSharp.Entities; using DisCatSharp.Net.Abstractions; using DisCatSharp.Net.Serialization; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace DisCatSharp.Net { /// /// Represents a discord api client. /// public sealed class DiscordApiClient { /// /// The audit log reason header name. /// private const string REASON_HEADER_NAME = "X-Audit-Log-Reason"; /// /// Gets the discord client. /// internal BaseDiscordClient Discord { get; } /// /// Gets the rest client. /// internal RestClient Rest { get; } /// /// Initializes a new instance of the class. /// /// The client. internal DiscordApiClient(BaseDiscordClient client) { this.Discord = client; this.Rest = new RestClient(client); } /// /// Initializes a new instance of the class. /// /// The proxy. /// The timeout. /// If true, use relative rate limit. /// The logger. internal DiscordApiClient(IWebProxy proxy, TimeSpan timeout, bool useRelativeRateLimit, ILogger logger) // This is for meta-clients, such as the webhook client { this.Rest = new RestClient(proxy, timeout, useRelativeRateLimit, logger); } /// /// Builds the query string. /// /// The values. /// If true, post. /// A string. private static string BuildQueryString(IDictionary values, bool post = false) { if (values == null || values.Count == 0) return string.Empty; var 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 ret. private void PopulateMessage(TransportUser author, DiscordMessage ret) { var guild = ret.Channel?.Guild; //If this is a webhook, it shouldn't be in the user cache. if (author.IsBot && int.Parse(author.Discriminator) == 0) { ret.Author = new DiscordUser(author) { Discord = this.Discord }; } else { if (!this.Discord.UserCache.TryGetValue(author.Id, out var usr)) { this.Discord.UserCache[author.Id] = usr = new DiscordUser(author) { Discord = this.Discord }; } if (guild != null) { if (!guild.Members.TryGetValue(author.Id, out var mbr)) mbr = new DiscordMember(usr) { Discord = this.Discord, 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 discorverySplashb64, Optional preferredLocale, Optional premiumProgressBarEnabled, string reason) { var pld = new RestGuildModifyPayload { Name = name, VerificationLevel = verificationLevel, DefaultMessageNotifications = defaultMessageNotifications, MfaLevel = mfaLevel, ExplicitContentFilter = explicitContentFilter, AfkChannelId = afkChannelId, AfkTimeout = afkTimeout, IconBase64 = iconb64, SplashBase64 = splashb64, BannerBase64 = bannerb64, DiscoverySplashBase64 = discorverySplashb64, OwnerId = ownerId, SystemChannelId = systemChannelId, SystemChannelFlags = systemChannelFlags, RulesChannelId = rulesChannelId, PublicUpdatesChannelId = publicUpdatesChannelId, PreferredLocale = preferredLocale, Description = description, PremiumProgressBarEnabled = premiumProgressBarEnabled }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var json = JObject.Parse(res.Response); var rawMembers = (JArray)json["members"]; var guild = json.ToDiscordObject(); foreach (var r in guild.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 = description ?? Optional.FromNoValue(), Features = features }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new { guild_id = guildId }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PATCH, route, headers, DiscordJson.SerializeObject(pld)).ConfigureAwait(false); var json = JObject.Parse(res.Response); var rawMembers = (JArray)json["members"]; var guild = json.ToDiscordObject(); foreach (var r in guild.RolesInternal.Values) r.GuildId = guild.Id; if (this.Discord is DiscordClient dc) await dc.OnGuildUpdateEventAsync(guild, rawMembers).ConfigureAwait(false); return guild; } /// /// Gets the guild bans async. /// /// The guild_id. /// A Task. internal async Task> GetGuildBansAsync(ulong guildId) { var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.BANS}"; 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 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. /// A Task. 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. /// A Task. 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. /// A Task. 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. /// A Task. 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. /// A Task. 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. /// A Task. 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. /// A Task. 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. /// A Task. 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. /// A Task. 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. /// A Task. 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. /// A Task. 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. /// A Task. 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. /// A Task. 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. /// A Task. 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. /// A Task. 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. /// A Task. 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. /// A Task. 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. /// A Task. 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. /// A Task. 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. /// A Task. 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. /// A Task. 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. /// A Task. 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. /// A Task. 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. /// A Task. 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. /// A Task. 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. /// A Task. 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, string reason = null) { var pld = new RestGuildScheduledEventCreatePayload { ChannelId = channelId, EntityMetadata = metadata, Name = name, ScheduledStartTime = scheduledStartTime, ScheduledEndTime = scheduledEndTime, Description = description, EntityType = type }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.SCHEDULED_EVENTS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new {guild_id = 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, string reason = null) { var pld = new RestGuildSheduledEventModifyPayload { ChannelId = channelId, EntityMetadata = metadata, Name = name, ScheduledStartTime = scheduledStartTime, ScheduledEndTime = scheduledEndTime, Description = description, EntityType = type, Status = status }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.SCHEDULED_EVENTS}/:scheduled_event_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new {guild_id = 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 RestGuildSheduledEventModifyPayload { Status = status }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers[REASON_HEADER_NAME] = reason; var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.SCHEDULED_EVENTS}/:scheduled_event_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new {guild_id = 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 sheduled event. /// /// The guild_id. /// The sheduled 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 sheduled event. /// Optional with member objects. /// This endpoint is paginated. /// /// The guild_id. /// The sheduled event id. /// The limit how many users to receive from the event. /// Get results before the given id. /// Get results after the given id. /// Wether to include guild member data. attaches guild_member property to the user object. internal async Task> GetGuildScheduledEventRspvUsersAsync(ulong 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 reason. /// A Task. 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, 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 }; 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. /// A Task. 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. /// A Task. 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. /// A Task. 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. /// A Task. 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. /// A Task. 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. /// A Task. 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. /// A Task. 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. /// A Task. 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. /// A Task. internal async Task EditMessageAsync(ulong channelId, ulong messageId, Optional content, Optional> embeds, IEnumerable 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.HasValue ? (string)content : null, HasEmbed = embeds.HasValue && (embeds.Value?.Any() ?? false), Embeds = embeds.HasValue && (embeds.Value?.Any() ?? false) ? embeds.Value : null, Components = components ?? null, Flags = suppressEmbed.HasValue ? (bool)suppressEmbed ? MessageFlags.SuppressedEmbeds : null : null }; pld.Mentions = new DiscordMentions(mentions ?? Mentions.None, false, mentions?.OfType().Any() ?? false); 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.HasValue ? attachments.Value : null; var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}/:message_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new {channel_id = 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. /// A Task. 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. /// A Task. 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. /// A Task. 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. /// A Task. 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. /// A Task. 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. /// A Task. 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. /// A Task. 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. /// A Task. 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. /// A Task. 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. /// A Task. 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. /// A Task. 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. /// A Task. 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. /// A Task. 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. /// A Task. 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. /// A Task. 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. /// A Task. 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. /// /// A Task. internal Task GetCurrentUserAsync() => this.GetUserAsync("@me"); /// /// Gets the user async. /// /// The user_id. /// A Task. internal Task GetUserAsync(ulong userId) => this.GetUserAsync(userId.ToString(CultureInfo.InvariantCulture)); /// /// Gets the user async. /// /// The user_id. /// A Task. 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. /// A Task. 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. /// A Task. 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. /// A Task. internal async Task ModifyCurrentUserAsync(string username, Optional base64Avatar) { var pld = new RestUserUpdateCurrentPayload { Username = username, AvatarBase64 = base64Avatar.HasValue ? base64Avatar.Value : null, 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. /// A Task. internal async Task> GetCurrentUserGuildsAsync(int limit = 100, ulong? before = null, ulong? after = null) { var route = $"{Endpoints.USERS}{Endpoints.ME}{Endpoints.GUILDS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { }, out var path); var url = Utilities.GetApiUriBuilderFor(path, this.Discord.Configuration) .AddParameter($"limit", limit.ToString(CultureInfo.InvariantCulture)); if (before != null) url.AddParameter("before", before.Value.ToString(CultureInfo.InvariantCulture)); if (after != null) url.AddParameter("after", after.Value.ToString(CultureInfo.InvariantCulture)); var res = await this.DoRequestAsync(this.Discord, bucket, url.Build(), RestRequestMethod.GET, route).ConfigureAwait(false); if (this.Discord is DiscordClient) { var 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. /// A Task. 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. /// A Task. 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. /// A Task. 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. /// A Task. 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. /// A Task. 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. /// A Task. 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. /// A Task. 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. /// A Task. 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.Count(); for (var i = 0; i < roleArrayCount; i++) sb.Append($"&include_roles={roleArray[i]}"); } var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.PRUNE}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new {guild_id = 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. /// A Task. 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.Count(); for (var i = 0; i < roleArrayCount; i++) sb.Append($"&include_roles={roleArray[i]}"); } var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.PRUNE}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new {guild_id = 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. /// A Task. internal async Task GetTemplateAsync(string code) { var route = $"{Endpoints.GUILDS}{Endpoints.TEMPLATES}/:code"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { code }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var templatesRaw = JsonConvert.DeserializeObject(res.Response); return templatesRaw; } /// /// Gets the guild integrations async. /// /// The guild_id. /// A Task. 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. /// A Task. 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. /// A Task. 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. /// A Task. 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. /// A Task. 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. /// A Task. 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. /// A Task. 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. /// A Task. 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. /// A Task. 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. /// A Task. 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. /// /// A Task. internal async Task> GetUsersConnectionsAsync() { var route = $"{Endpoints.USERS}{Endpoints.ME}{Endpoints.CONNECTIONS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var 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. /// /// A Task. internal async Task> ListVoiceRegionsAsync() { var route = $"{Endpoints.VOICE}{Endpoints.REGIONS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var regions = JsonConvert.DeserializeObject>(res.Response); return new ReadOnlyCollection(new List(regions)); } #endregion #region Webhooks /// /// Creates the webhook async. /// /// The channel_id. /// The name. /// The base64_avatar. /// The reason. /// A Task. internal async Task CreateWebhookAsync(ulong channelId, string name, Optional base64Avatar, string reason) { var pld = new RestWebhookPayload { Name = name, AvatarBase64 = base64Avatar.HasValue ? base64Avatar.Value : null, 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. /// A Task. 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. /// A Task. 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. /// A Task. 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. /// A Task. 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. /// A Task. internal async Task ModifyWebhookAsync(ulong webhookId, ulong channelId, string name, Optional base64Avatar, string reason) { var pld = new RestWebhookPayload { Name = name, AvatarBase64 = base64Avatar.HasValue ? base64Avatar.Value : null, 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. /// A Task. 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. /// A Task. 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. /// A Task. 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. /// A Task. 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.HasValue ? builder.Username.Value : null, AvatarUrl = builder.AvatarUrl.HasValue ? builder.AvatarUrl.Value : null, IsTts = builder.IsTts, Embeds = builder.Embeds, Components = builder.Components }; if (builder.Mentions != null) pld.Mentions = new DiscordMentions(builder.Mentions, builder.Mentions.Any()); if (builder.Files?.Count > 0) { ulong 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. /// A Task. 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. /// A Task. 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. /// A Task. 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. /// A Task. 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. /// A Task. 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. /// A Task. 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. /// A Task. 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. /// A Task. 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. /// A Task. 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. /// A Task. 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. /// A Task. 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. /// A Task. 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. /// A Task. 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. /// A Task. 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. /// A Task. 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. /// A Task. 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 with message. /// /// The channel id to create the thread in. /// The message id to create the thread from. /// The name of the thread. /// The auto_archive_duration for the thread. /// The rate limit per user. /// The reason. internal async Task CreateThreadWithMessageAsync(ulong channelId, ulong messageId, string name, ThreadAutoArchiveDuration autoArchiveDuration, int? rateLimitPerUser, string reason = null) { var pld = new RestThreadChannelCreatePayload { Name = name, AutoArchiveDuration = autoArchiveDuration, PerUserRateLimit = rateLimitPerUser }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.CHANNELS}/:channel_id{Endpoints.MESSAGES}/:message_id{Endpoints.THREADS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new {channel_id = 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.POST, route, headers, DiscordJson.SerializeObject(pld)); var threadChannel = JsonConvert.DeserializeObject(res.Response); return threadChannel; } /// /// Creates the thread without a message. /// /// The channel id to create the thread in. /// The name of the thread. /// The auto_archive_duration for the thread. /// Can be either or . /// The rate limit per user. /// The reason. internal async Task CreateThreadWithoutMessageAsync(ulong channelId, string name, ThreadAutoArchiveDuration autoArchiveDuration, ChannelType type = ChannelType.PublicThread, int? rateLimitPerUser = null, string reason = null) { 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{Endpoints.THREADS}"; 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)); var threadChannel = JsonConvert.DeserializeObject(res.Response); return threadChannel; } /// /// Gets the thread. /// /// The thread id. internal async Task GetThreadAsync(ulong threadId) { var route = $"{Endpoints.CHANNELS}/:channel_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 auto archive duration. /// The new per user rate limit. /// The new user invitable state. /// The reason for the modification. internal Task ModifyThreadAsync(ulong threadId, string name, Optional locked, Optional archived, Optional autoArchiveDuration, Optional perUserRateLimit, Optional invitable, string reason) { var pld = new RestThreadChannelModifyPayload { Name = name, Archived = archived, AutoArchiveDuration = autoArchiveDuration, Locked = locked, PerUserRateLimit = perUserRateLimit, Invitable = invitable }; var headers = Utilities.GetBaseHeaders(); if (!string.IsNullOrWhiteSpace(reason)) headers.Add(REASON_HEADER_NAME, reason); var route = $"{Endpoints.CHANNELS}/:thread_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new {thread_id = 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. /// A Task. 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. /// A Task. 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. /// A Task. 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. /// A Task. 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. /// A Task. 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 async. /// /// The application_id. /// A Task. internal async Task> GetGlobalApplicationCommandsAsync(ulong applicationId) { var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.COMMANDS}"; 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); var ret = JsonConvert.DeserializeObject>(res.Response); foreach (var app in ret) app.Discord = this.Discord; return ret.ToList(); } /// /// Bulks the overwrite global application commands async. /// /// The application_id. /// The commands. /// A Task. internal async Task> BulkOverwriteGlobalApplicationCommandsAsync(ulong 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, DefaultPermission = command.DefaultPermission, NameLocalizations = command.NameLocalizations?.GetKeyValuePairs(), DescriptionLocalizations = command.DescriptionLocalizations?.GetKeyValuePairs() }); } this.Discord.Logger.LogDebug(DiscordJson.SerializeObject(pld)); var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.COMMANDS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new {application_id = 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 the global application command async. /// /// The application_id. /// The command. /// A Task. internal async Task CreateGlobalApplicationCommandAsync(ulong applicationId, DiscordApplicationCommand command) { var pld = new RestApplicationCommandCreatePayload { Type = command.Type, Name = command.Name, Description = command.Description, Options = command.Options, DefaultPermission = command.DefaultPermission, NameLocalizations = command.NameLocalizations.GetKeyValuePairs(), DescriptionLocalizations = command.DescriptionLocalizations.GetKeyValuePairs() }; var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.COMMANDS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new {application_id = 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 the global application command async. /// /// The application_id. /// The command_id. /// A Task. 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 the global application command async. /// /// The application_id. /// The command_id. /// The name. /// The description. /// The options. /// The default_permission. /// The localizations of the name. /// The localizations of the description. /// A Task. internal async Task EditGlobalApplicationCommandAsync(ulong applicationId, ulong commandId, Optional name, Optional description, Optional> options, Optional defaultPermission, Optional nameLocalization, Optional descriptionLocalization) { var pld = new RestApplicationCommandEditPayload { Name = name, Description = description, Options = options, DefaultPermission = defaultPermission, NameLocalizations = nameLocalization.HasValue ? nameLocalization.Value.GetKeyValuePairs() : null, DescriptionLocalizations = descriptionLocalization.HasValue ? descriptionLocalization.Value.GetKeyValuePairs() : null }; 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 the global application command async. /// /// The application_id. /// The command_id. /// A Task. 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 async. /// /// The application_id. /// The guild_id. /// A Task. internal async Task> GetGuildApplicationCommandsAsync(ulong applicationId, ulong guildId) { 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 url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route); var ret = JsonConvert.DeserializeObject>(res.Response); foreach (var app in ret) app.Discord = this.Discord; return ret.ToList(); } /// /// Bulks the overwrite guild application commands async. /// /// The application_id. /// The guild_id. /// The commands. /// A Task. internal async Task> BulkOverwriteGuildApplicationCommandsAsync(ulong 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, DefaultPermission = command.DefaultPermission, NameLocalizations = command.NameLocalizations?.GetKeyValuePairs(), DescriptionLocalizations = command.DescriptionLocalizations?.GetKeyValuePairs() }); } this.Discord.Logger.LogDebug(DiscordJson.SerializeObject(pld)); var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.GUILDS}/:guild_id{Endpoints.COMMANDS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new {application_id = 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 the guild application command async. /// /// The application_id. /// The guild_id. /// The command. /// A Task. 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, DefaultPermission = command.DefaultPermission, NameLocalizations = command.NameLocalizations.GetKeyValuePairs(), DescriptionLocalizations = command.DescriptionLocalizations.GetKeyValuePairs() }; var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.GUILDS}/:guild_id{Endpoints.COMMANDS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.POST, route, new {application_id = 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 the guild application command async. /// /// 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 the guild application command async. /// /// The application_id. /// The guild_id. /// The command_id. /// The name. /// The description. /// The options. /// The default_permission. /// The localizations of the name. /// The localizations of the description. internal async Task EditGuildApplicationCommandAsync(ulong applicationId, ulong guildId, ulong commandId, Optional name, Optional description, Optional> options, Optional defaultPermission, Optional nameLocalization, Optional descriptionLocalization) { var pld = new RestApplicationCommandEditPayload { Name = name, Description = description, Options = options, DefaultPermission = defaultPermission, NameLocalizations = nameLocalization.HasValue ? nameLocalization.Value.GetKeyValuePairs() : null, DescriptionLocalizations = descriptionLocalization.HasValue ? descriptionLocalization.Value.GetKeyValuePairs() : null }; var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.GUILDS}/:guild_id{Endpoints.COMMANDS}/:command_id"; var bucket = this.Rest.GetBucket(RestRequestMethod.PATCH, route, new {application_id = 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 the guild application command async. /// /// 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); } /// /// 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 the application command permission. /// /// The target application id. /// The target guild id. /// The target command id. internal async Task GetApplicationCommandPermissionAsync(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; } /// /// Overwrites the guild application command permissions. /// /// The target application id. /// The target guild id. /// The target command id. /// Array of permissions. internal async Task OverwriteGuildApplicationCommandPermissionsAsync(ulong applicationId, ulong guildId, ulong commandId, IEnumerable permissions) { var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.GUILDS}/:guild_id{Endpoints.COMMANDS}/:command_id{Endpoints.PERMISSIONS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new {application_id = applicationId, guild_id = guildId, command_id = commandId }, out var path); if (permissions.ToArray().Length > 10) throw new NotSupportedException("You can add only up to 10 permission overwrites per command."); var pld = new RestApplicationCommandPermissionEditPayload { Permissions = permissions }; var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route, payload: DiscordJson.SerializeObject(pld)); var ret = JsonConvert.DeserializeObject(res.Response); ret.Discord = this.Discord; return ret; } /// /// Bulks overwrite the application command permissions. /// /// The target application id. /// The target guild id. /// internal async Task> BulkOverwriteApplicationCommandPermissionsAsync(ulong applicationId, ulong guildId, IEnumerable permissionOverwrites) { var route = $"{Endpoints.APPLICATIONS}/:application_id{Endpoints.GUILDS}/:guild_id{Endpoints.COMMANDS}{Endpoints.PERMISSIONS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.PUT, route, new {application_id = applicationId, guild_id = guildId }, out var path); var pld = new List(); foreach (var overwrite in permissionOverwrites) { if (overwrite.Permissions.Count > 10) throw new NotSupportedException("You can add only up to 10 permission overwrites per command."); pld.Add(new RestGuildApplicationCommandPermissionEditPayload { CommandId = overwrite.Id, Permissions = overwrite.Permissions }); } var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route, payload: DiscordJson.SerializeObject(pld.ToArray())); var ret = JsonConvert.DeserializeObject>(res.Response); foreach (var app in ret) app.Discord = this.Discord; return ret.ToList(); } /// /// Creates the interaction response async. /// /// The interaction_id. /// The interaction_token. /// The type. /// The builder. /// A Task. internal async Task CreateInteractionResponseAsync(ulong 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 async. /// /// The interaction_id. /// The interaction_token. /// The type. /// The builder. /// A Task. 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 async. /// /// The application_id. /// The interaction_token. /// A Task. internal Task GetOriginalInteractionResponseAsync(ulong applicationId, string interactionToken) => this.GetWebhookMessageAsync(applicationId, interactionToken, Endpoints.ORIGINAL, null); /// /// Edits the original interaction response async. /// /// The application_id. /// The interaction_token. /// The builder. /// A Task. internal Task EditOriginalInteractionResponseAsync(ulong applicationId, string interactionToken, DiscordWebhookBuilder builder) => this.EditWebhookMessageAsync(applicationId, interactionToken, Endpoints.ORIGINAL, builder, null); /// /// Deletes the original interaction response async. /// /// The application_id. /// The interaction_token. /// A Task. internal Task DeleteOriginalInteractionResponseAsync(ulong applicationId, string interactionToken) => this.DeleteWebhookMessageAsync(applicationId, interactionToken, Endpoints.ORIGINAL, null); /// /// Creates the followup message async. /// /// The application_id. /// The interaction_token. /// The builder. /// A Task. 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 async. /// /// The application_id. /// The interaction_token. /// The message_id. /// A Task. internal Task GetFollowupMessageAsync(ulong applicationId, string interactionToken, ulong messageId) => this.GetWebhookMessageAsync(applicationId, interactionToken, messageId); /// /// Edits the followup message async. /// /// The application_id. /// The interaction_token. /// The message_id. /// The builder. /// A Task. internal Task EditFollowupMessageAsync(ulong applicationId, string interactionToken, ulong messageId, DiscordWebhookBuilder builder) => this.EditWebhookMessageAsync(applicationId, interactionToken, messageId.ToString(), builder, null); /// /// Deletes the followup message async. /// /// The application_id. /// The interaction_token. /// The message_id. /// A Task. internal Task DeleteFollowupMessageAsync(ulong applicationId, string interactionToken, ulong messageId) => this.DeleteWebhookMessageAsync(applicationId, interactionToken, messageId); #endregion #region Misc /// /// Gets the current application info async. /// /// A Task. internal Task GetCurrentApplicationInfoAsync() => this.GetApplicationInfoAsync("@me"); /// /// Gets the application info async. /// /// The application_id. /// A Task. internal Task GetApplicationInfoAsync(ulong applicationId) => this.GetApplicationInfoAsync(applicationId.ToString(CultureInfo.InvariantCulture)); /// /// Gets the application info async. /// /// The application_id. /// A Task. 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. /// A Task. internal async Task> GetApplicationAssetsAsync(DiscordApplication application) { var route = $"{Endpoints.OAUTH2}{Endpoints.APPLICATIONS}/:application_id{Endpoints.ASSETS}"; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { application_id = application.Id }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false); var assets = JsonConvert.DeserializeObject>(res.Response); foreach (var asset in assets) { asset.Discord = application.Discord; asset.Application = application; } return new ReadOnlyCollection(new List(assets)); } /// /// Gets the gateway info async. /// /// A Task. internal async Task GetGatewayInfoAsync() { var headers = Utilities.GetBaseHeaders(); var route = Endpoints.GATEWAY; if (this.Discord.Configuration.TokenType == TokenType.Bot) route += Endpoints.BOT; var bucket = this.Rest.GetBucket(RestRequestMethod.GET, route, new { }, out var path); var url = Utilities.GetApiUriFor(path, this.Discord.Configuration); var res = await this.DoRequestAsync(this.Discord, bucket, url, RestRequestMethod.GET, route, headers).ConfigureAwait(false); var info = JObject.Parse(res.Response).ToObject(); info.SessionBucket.ResetAfter = DateTimeOffset.UtcNow + TimeSpan.FromMilliseconds(info.SessionBucket.ResetAfterInternal); return info; } #endregion #region DCS Internals /// /// Gets the DisCatSharp team. /// > internal async Task GetDisCatSharpTeamAsync() { try { var wc = new WebClient(); var dcs = await wc.DownloadStringTaskAsync(new Uri("https://dcs.aitsys.dev/api/devs/")); var dcsGuild = await wc.DownloadStringTaskAsync(new Uri("https://dcs.aitsys.dev/api/guild/")); var app = JsonConvert.DeserializeObject(dcs); var guild = JsonConvert.DeserializeObject(dcsGuild); var dcst = new DisCatSharpTeam { IconHash = app.Team.IconHash, TeamName = app.Team.Name, PrivacyPolicyUrl = app.PrivacyPolicyUrl, TermsOfServiceUrl = app.TermsOfServiceUrl, RepoUrl = "https://github.com/Aiko-IT-Systems/DisCatSharp", DocsUrl = "https://docs.dcs.aitsys.dev", Id = app.Team.Id, BannerHash = guild.BannerHash, LogoHash = guild.IconHash, GuildId = guild.Id, Guild = guild, SupportInvite = await this.GetInviteAsync("discatsharp", true, true, null) }; List team = new(); DisCatSharpTeamMember owner = new(); foreach (var mb in app.Team.Members.OrderBy(m => m.User.Username)) { var tuser = await this.GetUserAsync(mb.User.Id); var user = mb.User; if (mb.User.Id == 856780995629154305) { owner.Id = user.Id; owner.Username = user.Username; owner.Discriminator = user.Discriminator; owner.AvatarHash = user.AvatarHash; owner.BannerHash = tuser.BannerHash; owner.BannerColorInternal = tuser.BannerColorInternal; team.Add(owner); } else { - team.Add(new() + team.Add(new DisCatSharpTeamMember { Id = user.Id, Username = user.Username, Discriminator = user.Discriminator, AvatarHash = user.AvatarHash, BannerHash = tuser.BannerHash, BannerColorInternal = tuser.BannerColorInternal }); } } dcst.Owner = owner; dcst.Developers = team; return dcst; } catch (Exception ex) { this.Discord.Logger.LogDebug(ex.Message); this.Discord.Logger.LogDebug(ex.StackTrace); return null; } } #endregion } } diff --git a/DisCatSharp/Net/Rest/RateLimitBucket.cs b/DisCatSharp/Net/Rest/RateLimitBucket.cs index d3027623d..9468d560f 100644 --- a/DisCatSharp/Net/Rest/RateLimitBucket.cs +++ b/DisCatSharp/Net/Rest/RateLimitBucket.cs @@ -1,291 +1,291 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Concurrent; using System.Threading; using System.Threading.Tasks; namespace DisCatSharp.Net { /// /// Represents a rate limit bucket. /// internal class RateLimitBucket : IEquatable { /// /// Gets the Id of the guild bucket. /// public string GuildId { get; internal set; } /// /// Gets the Id of the channel bucket. /// public string ChannelId { get; internal set; } /// /// Gets the ID of the webhook bucket. /// public string WebhookId { get; internal set; } /// /// Gets the Id of the ratelimit bucket. /// public volatile string BucketId; /// /// Gets or sets the ratelimit hash of this bucket. /// public string Hash { get => Volatile.Read(ref this.HashInternal); internal set { this.IsUnlimited = value.Contains(s_unlimitedHash); if (this.BucketId != null && !this.BucketId.StartsWith(value)) { var id = GenerateBucketId(value, this.GuildId, this.ChannelId, this.WebhookId); this.BucketId = id; this.RouteHashes.Add(id); } Volatile.Write(ref this.HashInternal, value); } } internal string HashInternal; /// /// Gets the past route hashes associated with this bucket. /// public ConcurrentBag RouteHashes { get; } /// /// Gets when this bucket was last called in a request. /// public DateTimeOffset LastAttemptAt { get; internal set; } /// /// Gets the number of uses left before pre-emptive rate limit is triggered. /// public int Remaining => this.RemainingInternal; /// /// Gets the maximum number of uses within a single bucket. /// public int Maximum { get; set; } /// /// Gets the timestamp at which the rate limit resets. /// public DateTimeOffset Reset { get; internal set; } /// /// Gets the time interval to wait before the rate limit resets. /// - public TimeSpan? ResetAfter { get; internal set; } = null; + public TimeSpan? ResetAfter { get; internal set; } /// /// Gets a value indicating whether the ratelimit global. /// public bool IsGlobal { get; internal set; } = false; /// /// Gets the ratelimit scope. /// public string Scope { get; internal set; } = "user"; /// /// Gets the time interval to wait before the rate limit resets as offset /// internal DateTimeOffset ResetAfterOffset { get; set; } internal volatile int RemainingInternal; /// /// Gets whether this bucket has it's ratelimit determined. /// This will be if the ratelimit is determined. /// internal volatile bool IsUnlimited; /// /// If the initial request for this bucket that is deterternining the rate limits is currently executing /// This is a int because booleans can't be accessed atomically /// 0 => False, all other values => True /// internal volatile int LimitTesting; /// /// Task to wait for the rate limit test to finish /// internal volatile Task LimitTestFinished; /// /// If the rate limits have been determined /// internal volatile bool LimitValid; /// /// Rate limit reset in ticks, UTC on the next response after the rate limit has been reset /// internal long NextReset; /// /// If the rate limit is currently being reset. /// This is a int because booleans can't be accessed atomically. /// 0 => False, all other values => True /// internal volatile int LimitResetting; private static readonly string s_unlimitedHash = "unlimited"; /// /// Initializes a new instance of the class. /// /// The hash. /// The guild_id. /// The channel_id. /// The webhook_id. internal RateLimitBucket(string hash, string guildId, string channelId, string webhookId) { this.Hash = hash; this.ChannelId = channelId; this.GuildId = guildId; this.WebhookId = webhookId; this.BucketId = GenerateBucketId(hash, guildId, channelId, webhookId); this.RouteHashes = new ConcurrentBag(); } /// /// Generates an ID for this request bucket. /// /// Hash for this bucket. /// Guild Id for this bucket. /// Channel Id for this bucket. /// Webhook Id for this bucket. /// Bucket Id. public static string GenerateBucketId(string hash, string guildId, string channelId, string webhookId) => $"{hash}:{guildId}:{channelId}:{webhookId}"; /// /// Generates the hash key. /// /// The method. /// The route. /// A string. public static string GenerateHashKey(RestRequestMethod method, string route) => $"{method}:{route}"; /// /// Generates the unlimited hash. /// /// The method. /// The route. /// A string. public static string GenerateUnlimitedHash(RestRequestMethod method, string route) => $"{GenerateHashKey(method, route)}:{s_unlimitedHash}"; /// /// Returns a string representation of this bucket. /// /// String representation of this bucket. public override string ToString() { var guildId = this.GuildId != string.Empty ? this.GuildId : "guild_id"; var channelId = this.ChannelId != string.Empty ? this.ChannelId : "channel_id"; var webhookId = this.WebhookId != string.Empty ? this.WebhookId : "webhook_id"; return $"{this.Scope} rate limit bucket [{this.Hash}:{guildId}:{channelId}:{webhookId}] [{this.Remaining}/{this.Maximum}] {(this.ResetAfter.HasValue ? this.ResetAfterOffset : this.Reset)}"; } /// /// 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 RateLimitBucket); /// /// Checks whether this is equal to another . /// /// to compare to. /// Whether the is equal to this . public bool Equals(RateLimitBucket e) => e is not null && (ReferenceEquals(this, e) || this.BucketId == e.BucketId); /// /// Gets the hash code for this . /// /// The hash code for this . public override int GetHashCode() => this.BucketId.GetHashCode(); /// /// Sets remaining number of requests to the maximum when the ratelimit is reset /// /// internal async Task TryResetLimitAsync(DateTimeOffset now) { if (this.ResetAfter.HasValue) this.ResetAfter = this.ResetAfterOffset - now; if (this.NextReset == 0) return; if (this.NextReset > now.UtcTicks) return; while (Interlocked.CompareExchange(ref this.LimitResetting, 1, 0) != 0) #pragma warning restore 420 await Task.Yield(); if (this.NextReset != 0) { this.RemainingInternal = this.Maximum; this.NextReset = 0; } this.LimitResetting = 0; } /// /// Sets the initial values. /// /// The max. /// The uses left. /// The new reset. internal void SetInitialValues(int max, int usesLeft, DateTimeOffset newReset) { this.Maximum = max; this.RemainingInternal = usesLeft; this.NextReset = newReset.UtcTicks; this.LimitValid = true; this.LimitTestFinished = null; this.LimitTesting = 0; } } } diff --git a/DisCatSharp/Net/Rest/RestClient.cs b/DisCatSharp/Net/Rest/RestClient.cs index 5a30e885b..3a4d1f302 100644 --- a/DisCatSharp/Net/Rest/RestClient.cs +++ b/DisCatSharp/Net/Rest/RestClient.cs @@ -1,860 +1,862 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.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 Regex(@":([a-z_]+)"); + 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. /// /// The proxy. /// The timeout. /// If true, use relative ratelimit. /// The logger. internal RestClient(IWebProxy proxy, TimeSpan timeout, bool useRelativeRatelimit, ILogger logger) // This is for meta-clients, such as the webhook client { 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 bucket. /// /// The method. /// The route. /// The route paramaters. /// 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 async. /// /// 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 async. /// 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, "Pre-emptive 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 { this._logger.LogError(LoggerEvents.RatelimitHit, "Ratelimit hit, requeueing 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. /// If true, reset to initial. 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 signel 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; } /// /// Cleanups 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 (resetOffset != null && !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(); + { + 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/WebSocket/WebSocketClient.cs b/DisCatSharp/Net/WebSocket/WebSocketClient.cs index 42a443829..3983fb52d 100644 --- a/DisCatSharp/Net/WebSocket/WebSocketClient.cs +++ b/DisCatSharp/Net/WebSocket/WebSocketClient.cs @@ -1,415 +1,415 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.IO; using System.Net; using System.Net.WebSockets; using System.Threading; using System.Threading.Tasks; using DisCatSharp.Common.Utilities; using DisCatSharp.EventArgs; namespace DisCatSharp.Net.WebSocket { // weebsocket // not even sure whether emzi or I posted this. much love, naam. /// /// The default, native-based WebSocket client implementation. /// public class WebSocketClient : IWebSocketClient { /// /// The outgoing chunk size. /// private const int OUTGOING_CHUNK_SIZE = 8192; // 8 KiB /// /// The incoming chunk size. /// private const int INCOMING_CHUNK_SIZE = 32768; // 32 KiB /// /// Gets the proxy settings for this client. /// public IWebProxy Proxy { get; } /// /// Gets the collection of default headers to send when connecting to the remote endpoint. /// public IReadOnlyDictionary DefaultHeaders { get; } /// /// Gets or sets the service provider. /// IServiceProvider IWebSocketClient.ServiceProvider { get => this._serviceProvider; set => this._serviceProvider = value; } private readonly Dictionary _defaultHeaders; private Task _receiverTask; private CancellationTokenSource _receiverTokenSource; private CancellationToken _receiverToken; private readonly SemaphoreSlim _senderLock; private CancellationTokenSource _socketTokenSource; private CancellationToken _socketToken; private ClientWebSocket _ws; - private volatile bool _isClientClose = false; - private volatile bool _isConnected = false; - private bool _isDisposed = false; + private volatile bool _isClientClose; + private volatile bool _isConnected; + private bool _isDisposed; /// /// Instantiates a new WebSocket client with specified proxy settings. /// /// Proxy settings for the client. /// Service provider. private WebSocketClient(IWebProxy proxy, IServiceProvider provider) { this._connected = new AsyncEvent("WS_CONNECT", TimeSpan.Zero, this.EventErrorHandler); this._disconnected = new AsyncEvent("WS_DISCONNECT", TimeSpan.Zero, this.EventErrorHandler); this._messageReceived = new AsyncEvent("WS_MESSAGE", TimeSpan.Zero, this.EventErrorHandler); this._exceptionThrown = new AsyncEvent("WS_ERROR", TimeSpan.Zero, null); this.Proxy = proxy; this._defaultHeaders = new Dictionary(); this.DefaultHeaders = new ReadOnlyDictionary(this._defaultHeaders); this._receiverTokenSource = null; this._receiverToken = CancellationToken.None; this._senderLock = new SemaphoreSlim(1); this._socketTokenSource = null; this._socketToken = CancellationToken.None; this._serviceProvider = provider; } /// /// Connects to a specified remote WebSocket endpoint. /// /// The URI of the WebSocket endpoint. public async Task ConnectAsync(Uri uri) { // Disconnect first try { await this.DisconnectAsync().ConfigureAwait(false); } catch { } // Disallow sending messages await this._senderLock.WaitAsync().ConfigureAwait(false); try { // This can be null at this point this._receiverTokenSource?.Dispose(); this._socketTokenSource?.Dispose(); this._ws?.Dispose(); this._ws = new ClientWebSocket(); this._ws.Options.Proxy = this.Proxy; this._ws.Options.KeepAliveInterval = TimeSpan.Zero; if (this._defaultHeaders != null) foreach (var (k, v) in this._defaultHeaders) this._ws.Options.SetRequestHeader(k, v); this._receiverTokenSource = new CancellationTokenSource(); this._receiverToken = this._receiverTokenSource.Token; this._socketTokenSource = new CancellationTokenSource(); this._socketToken = this._socketTokenSource.Token; this._isClientClose = false; this._isDisposed = false; await this._ws.ConnectAsync(uri, this._socketToken).ConfigureAwait(false); this._receiverTask = Task.Run(this.ReceiverLoopAsync, this._receiverToken); } finally { this._senderLock.Release(); } } /// /// Disconnects the WebSocket connection. /// /// The code /// The message /// Lala Sabathil,06.07.2021 /// Lala Sabathil,06.07.2021 public async Task DisconnectAsync(int code = 1000, string message = "") { // Ensure that messages cannot be sent await this._senderLock.WaitAsync().ConfigureAwait(false); try { this._isClientClose = true; if (this._ws != null && (this._ws.State == WebSocketState.Open || this._ws.State == WebSocketState.CloseReceived)) await this._ws.CloseOutputAsync((WebSocketCloseStatus)code, message, CancellationToken.None).ConfigureAwait(false); if (this._receiverTask != null) await this._receiverTask.ConfigureAwait(false); // Ensure that receiving completed if (this._isConnected) this._isConnected = false; if (!this._isDisposed) { // Cancel all running tasks if (this._socketToken.CanBeCanceled) this._socketTokenSource?.Cancel(); this._socketTokenSource?.Dispose(); if (this._receiverToken.CanBeCanceled) this._receiverTokenSource?.Cancel(); this._receiverTokenSource?.Dispose(); this._isDisposed = true; } } catch { } finally { this._senderLock.Release(); } } /// /// Send a message to the WebSocket server. /// /// The message to send. public async Task SendMessageAsync(string message) { if (this._ws == null) return; if (this._ws.State != WebSocketState.Open && this._ws.State != WebSocketState.CloseReceived) return; var bytes = Utilities.UTF8.GetBytes(message); await this._senderLock.WaitAsync().ConfigureAwait(false); try { var len = bytes.Length; var segCount = len / OUTGOING_CHUNK_SIZE; if (len % OUTGOING_CHUNK_SIZE != 0) segCount++; for (var i = 0; i < segCount; i++) { var segStart = OUTGOING_CHUNK_SIZE * i; var segLen = Math.Min(OUTGOING_CHUNK_SIZE, len - segStart); await this._ws.SendAsync(new ArraySegment(bytes, segStart, segLen), WebSocketMessageType.Text, i == segCount - 1, CancellationToken.None).ConfigureAwait(false); } } finally { this._senderLock.Release(); } } /// /// Adds a header to the default header collection. /// /// Name of the header to add. /// Value of the header to add. /// Whether the operation succeeded. public bool AddDefaultHeader(string name, string value) { this._defaultHeaders[name] = value; return true; } /// /// Removes a header from the default header collection. /// /// Name of the header to remove. /// Whether the operation succeeded. public bool RemoveDefaultHeader(string name) => this._defaultHeaders.Remove(name); /// /// Disposes of resources used by this WebSocket client instance. /// public void Dispose() { if (this._isDisposed) return; this._isDisposed = true; this.DisconnectAsync().ConfigureAwait(false).GetAwaiter().GetResult(); this._receiverTokenSource?.Dispose(); this._socketTokenSource?.Dispose(); } /// /// Receivers the loop. /// internal async Task ReceiverLoopAsync() { await Task.Yield(); var token = this._receiverToken; var buffer = new ArraySegment(new byte[INCOMING_CHUNK_SIZE]); try { using var bs = new MemoryStream(); while (!token.IsCancellationRequested) { // See https://github.com/RogueException/Discord.Net/commit/ac389f5f6823e3a720aedd81b7805adbdd78b66d // for explanation on the cancellation token WebSocketReceiveResult result; byte[] resultBytes; do { result = await this._ws.ReceiveAsync(buffer, CancellationToken.None).ConfigureAwait(false); if (result.MessageType == WebSocketMessageType.Close) break; bs.Write(buffer.Array, 0, result.Count); } while (!result.EndOfMessage); resultBytes = new byte[bs.Length]; bs.Position = 0; bs.Read(resultBytes, 0, resultBytes.Length); bs.Position = 0; bs.SetLength(0); if (!this._isConnected && result.MessageType != WebSocketMessageType.Close) { this._isConnected = true; await this._connected.InvokeAsync(this, new SocketEventArgs(this._serviceProvider)).ConfigureAwait(false); } if (result.MessageType == WebSocketMessageType.Binary) { await this._messageReceived.InvokeAsync(this, new SocketBinaryMessageEventArgs(resultBytes)).ConfigureAwait(false); } else if (result.MessageType == WebSocketMessageType.Text) { await this._messageReceived.InvokeAsync(this, new SocketTextMessageEventArgs(Utilities.UTF8.GetString(resultBytes))).ConfigureAwait(false); } else // close { if (!this._isClientClose) { var code = result.CloseStatus.Value; code = code == WebSocketCloseStatus.NormalClosure || code == WebSocketCloseStatus.EndpointUnavailable ? (WebSocketCloseStatus)4000 : code; await this._ws.CloseOutputAsync(code, result.CloseStatusDescription, CancellationToken.None).ConfigureAwait(false); } await this._disconnected.InvokeAsync(this, new SocketCloseEventArgs(this._serviceProvider) { CloseCode = (int)result.CloseStatus, CloseMessage = result.CloseStatusDescription }).ConfigureAwait(false); break; } } } catch (Exception ex) { await this._exceptionThrown.InvokeAsync(this, new SocketErrorEventArgs(this._serviceProvider) { Exception = ex }).ConfigureAwait(false); await this._disconnected.InvokeAsync(this, new SocketCloseEventArgs(this._serviceProvider) { CloseCode = -1, CloseMessage = "" }).ConfigureAwait(false); } // Don't await or you deadlock // DisconnectAsync waits for this method _ = this.DisconnectAsync().ConfigureAwait(false); } /// /// Creates a new instance of . /// /// Proxy to use for this client instance. /// Service provider. /// An instance of . public static IWebSocketClient CreateNew(IWebProxy proxy, IServiceProvider provider) => new WebSocketClient(proxy, provider); #region Events /// /// Triggered when the client connects successfully. /// public event AsyncEventHandler Connected { add => this._connected.Register(value); remove => this._connected.Unregister(value); } private readonly AsyncEvent _connected; /// /// Triggered when the client is disconnected. /// public event AsyncEventHandler Disconnected { add => this._disconnected.Register(value); remove => this._disconnected.Unregister(value); } private readonly AsyncEvent _disconnected; /// /// Triggered when the client receives a message from the remote party. /// public event AsyncEventHandler MessageReceived { add => this._messageReceived.Register(value); remove => this._messageReceived.Unregister(value); } private readonly AsyncEvent _messageReceived; /// /// Triggered when an error occurs in the client. /// public event AsyncEventHandler ExceptionThrown { add => this._exceptionThrown.Register(value); remove => this._exceptionThrown.Unregister(value); } private readonly AsyncEvent _exceptionThrown; private IServiceProvider _serviceProvider; /// /// Events the error handler. /// /// The event. /// The exeption. /// The handler. /// The sender. /// The event args. private void EventErrorHandler(AsyncEvent asyncEvent, Exception ex, AsyncEventHandler handler, WebSocketClient sender, TArgs eventArgs) where TArgs : AsyncEventArgs => this._exceptionThrown.InvokeAsync(this, new SocketErrorEventArgs(this._serviceProvider) { Exception = ex }).ConfigureAwait(false).GetAwaiter().GetResult(); #endregion } } diff --git a/DisCatSharp/QueryUriBuilder.cs b/DisCatSharp/QueryUriBuilder.cs index 7a242345f..b6bde9f0d 100644 --- a/DisCatSharp/QueryUriBuilder.cs +++ b/DisCatSharp/QueryUriBuilder.cs @@ -1,96 +1,94 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Generic; using System.Linq; namespace DisCatSharp { /// /// Represents a query uri builder. /// internal class QueryUriBuilder { /// /// Gets the source uri. /// public Uri SourceUri { get; } /// /// Gets the query parameters. /// public IReadOnlyList> QueryParameters => this._queryParams; private readonly List> _queryParams = new(); /// /// Initializes a new instance of the class. /// /// The uri. public QueryUriBuilder(string uri) { if (uri == null) throw new ArgumentNullException(nameof(uri)); this.SourceUri = new Uri(uri); } /// /// Initializes a new instance of the class. /// /// The uri. public QueryUriBuilder(Uri uri) { if (uri == null) throw new ArgumentNullException(nameof(uri)); this.SourceUri = uri; } /// /// Adds a parameter. /// /// The key to be added. /// The value to be added. public QueryUriBuilder AddParameter(string key, string value) { this._queryParams.Add(new KeyValuePair(key, value)); return this; } /// /// Builds the uri. /// - public Uri Build() - { - return new UriBuilder(this.SourceUri) + public Uri Build() => + new UriBuilder(this.SourceUri) { Query = string.Join("&", this._queryParams.Select(e => Uri.EscapeDataString(e.Key) + '=' + Uri.EscapeDataString(e.Value))) }.Uri; - } /// /// Returns a readable string. /// public override string ToString() => this.Build().ToString(); } } diff --git a/DisCatSharp/RingBuffer.cs b/DisCatSharp/RingBuffer.cs index 1e042083a..553edcc5f 100644 --- a/DisCatSharp/RingBuffer.cs +++ b/DisCatSharp/RingBuffer.cs @@ -1,241 +1,239 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections; using System.Collections.Generic; using System.Linq; namespace DisCatSharp { /// /// A circular buffer collection. /// /// Type of elements within this ring buffer. public class RingBuffer : ICollection { /// /// Gets the current index of the buffer items. /// public int CurrentIndex { get; protected set; } /// /// Gets the capacity of this ring buffer. /// public int Capacity { get; protected set; } /// /// Gets the number of items in this ring buffer. /// public int Count => this._reachedEnd ? this.Capacity : this.CurrentIndex; /// /// Gets whether this ring buffer is read-only. /// public bool IsReadOnly => false; /// /// Gets or sets the internal collection of items. /// protected T[] InternalBuffer { get; set; } - private bool _reachedEnd = false; + private bool _reachedEnd; /// /// Creates a new ring buffer with specified size. /// /// Size of the buffer to create. /// public RingBuffer(int size) { if (size <= 0) throw new ArgumentOutOfRangeException(nameof(size), "Size must be positive."); this.CurrentIndex = 0; this.Capacity = size; this.InternalBuffer = new T[this.Capacity]; } /// /// Creates a new ring buffer, filled with specified elements. /// /// Elements to fill the buffer with. /// /// public RingBuffer(IEnumerable elements) : this(elements, 0) { } /// /// Creates a new ring buffer, filled with specified elements, and starting at specified index. /// /// Elements to fill the buffer with. /// Starting element index. /// /// public RingBuffer(IEnumerable elements, int index) { if (elements == null || !elements.Any()) throw new ArgumentException(nameof(elements), "The collection cannot be null or empty."); this.CurrentIndex = index; this.InternalBuffer = elements.ToArray(); this.Capacity = this.InternalBuffer.Length; if (this.CurrentIndex >= this.InternalBuffer.Length || this.CurrentIndex < 0) throw new ArgumentOutOfRangeException(nameof(index), "Index must be less than buffer capacity, and greater than zero."); } /// /// Inserts an item into this ring buffer. /// /// Item to insert. public void Add(T item) { this.InternalBuffer[this.CurrentIndex++] = item; if (this.CurrentIndex == this.Capacity) { this.CurrentIndex = 0; this._reachedEnd = true; } } /// /// Gets first item from the buffer that matches the predicate. /// /// Predicate used to find the item. /// Item that matches the predicate, or default value for the type of the items in this ring buffer, if one is not found. /// Whether an item that matches the predicate was found or not. public bool TryGet(Func predicate, out T item) { for (var i = this.CurrentIndex; i < this.InternalBuffer.Length; i++) { if (this.InternalBuffer[i] != null && predicate(this.InternalBuffer[i])) { item = this.InternalBuffer[i]; return true; } } for (var i = 0; i < this.CurrentIndex; i++) { if (this.InternalBuffer[i] != null && predicate(this.InternalBuffer[i])) { item = this.InternalBuffer[i]; return true; } } item = default; return false; } /// /// Clears this ring buffer and resets the current item index. /// public void Clear() { for (var i = 0; i < this.InternalBuffer.Length; i++) this.InternalBuffer[i] = default; this.CurrentIndex = 0; } /// /// Checks whether given item is present in the buffer. This method is not implemented. Use instead. /// /// Item to check for. /// Whether the buffer contains the item. /// public bool Contains(T item) => throw new NotImplementedException("This method is not implemented. Use .Contains(predicate) instead."); /// /// Checks whether given item is present in the buffer using given predicate to find it. /// /// Predicate used to check for the item. /// Whether the buffer contains the item. public bool Contains(Func predicate) => this.InternalBuffer.Any(predicate); /// /// Copies this ring buffer to target array, attempting to maintain the order of items within. /// /// Target array. /// Index starting at which to copy the items to. public void CopyTo(T[] array, int index) { if (array.Length - index < 1) throw new ArgumentException("Target array is too small to contain the elements from this buffer.", nameof(array)); var ci = 0; for (var i = this.CurrentIndex; i < this.InternalBuffer.Length; i++) array[ci++] = this.InternalBuffer[i]; for (var i = 0; i < this.CurrentIndex; i++) array[ci++] = this.InternalBuffer[i]; } /// /// Removes an item from the buffer. This method is not implemented. Use instead. /// /// Item to remove. /// Whether an item was removed or not. public bool Remove(T item) => throw new NotImplementedException("This method is not implemented. Use .Remove(predicate) instead."); /// /// Removes an item from the buffer using given predicate to find it. /// /// Predicate used to find the item. /// Whether an item was removed or not. public bool Remove(Func predicate) { for (var i = 0; i < this.InternalBuffer.Length; i++) { if (this.InternalBuffer[i] != null && predicate(this.InternalBuffer[i])) { this.InternalBuffer[i] = default; return true; } } return false; } /// /// Returns an enumerator for this ring buffer. /// /// Enumerator for this ring buffer. - public IEnumerator GetEnumerator() - { - return !this._reachedEnd + public IEnumerator GetEnumerator() => + !this._reachedEnd ? this.InternalBuffer.AsEnumerable().GetEnumerator() : this.InternalBuffer.Skip(this.CurrentIndex) - .Concat(this.InternalBuffer.Take(this.CurrentIndex)) - .GetEnumerator(); - } + .Concat(this.InternalBuffer.Take(this.CurrentIndex)) + .GetEnumerator(); /// /// Returns an enumerator for this ring buffer. /// /// Enumerator for this ring buffer. IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); } } diff --git a/DisCatSharp/Utilities.cs b/DisCatSharp/Utilities.cs index c306ee1c1..7f6d515df 100644 --- a/DisCatSharp/Utilities.cs +++ b/DisCatSharp/Utilities.cs @@ -1,466 +1,462 @@ // This file is part of the DisCatSharp project, based off DSharpPlus. // // Copyright (c) 2021 AITSYS // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using DisCatSharp.Entities; using DisCatSharp.Net; using Microsoft.Extensions.Logging; namespace DisCatSharp { /// /// Various Discord-related utilities. /// public static class Utilities { /// /// Gets the version of the library /// internal static string VersionHeader { get; set; } /// /// Gets or sets the permission strings. /// internal static Dictionary PermissionStrings { get; set; } /// /// Gets the utf8 encoding /// // ReSharper disable once InconsistentNaming - internal static UTF8Encoding UTF8 { get; } = new UTF8Encoding(false); + internal static UTF8Encoding UTF8 { get; } = new(false); /// /// Initializes a new instance of the class. /// static Utilities() { PermissionStrings = new Dictionary(); var t = typeof(Permissions); var ti = t.GetTypeInfo(); var vals = Enum.GetValues(t).Cast(); foreach (var xv in vals) { var xsv = xv.ToString(); var xmv = ti.DeclaredMembers.FirstOrDefault(xm => xm.Name == xsv); var xav = xmv.GetCustomAttribute(); PermissionStrings[xv] = xav.String; } var a = typeof(DiscordClient).GetTypeInfo().Assembly; var vs = ""; var iv = a.GetCustomAttribute(); if (iv != null) vs = iv.InformationalVersion; else { var v = a.GetName().Version; vs = v.ToString(3); } VersionHeader = $"DiscordBot (https://github.com/Aiko-IT-Systems/DisCatSharp, v{vs})"; } /// /// Gets the api base uri. /// /// The config /// A string. internal static string GetApiBaseUri(DiscordConfiguration config = null) => config == null ? Endpoints.BASE_URI + "9" : config.UseCanary ? Endpoints.CANARY_URI + config.ApiVersion : Endpoints.BASE_URI + config.ApiVersion; /// /// Gets the api uri for. /// /// The path. /// The config /// An Uri. internal static Uri GetApiUriFor(string path, DiscordConfiguration config) => new($"{GetApiBaseUri(config)}{path}"); /// /// Gets the api uri for. /// /// The path. /// The query string. /// The config /// An Uri. internal static Uri GetApiUriFor(string path, string queryString, DiscordConfiguration config) => new($"{GetApiBaseUri(config)}{path}{queryString}"); /// /// Gets the api uri builder for. /// /// The path. /// The config /// A QueryUriBuilder. internal static QueryUriBuilder GetApiUriBuilderFor(string path, DiscordConfiguration config) => new($"{GetApiBaseUri(config)}{path}"); /// /// Gets the formatted token. /// /// The client. /// A string. internal static string GetFormattedToken(BaseDiscordClient client) => GetFormattedToken(client.Configuration); /// /// Gets the formatted token. /// /// The config. /// A string. - internal static string GetFormattedToken(DiscordConfiguration config) - { - return config.TokenType switch + internal static string GetFormattedToken(DiscordConfiguration config) => + config.TokenType switch { TokenType.Bearer => $"Bearer {config.Token}", TokenType.Bot => $"Bot {config.Token}", _ => throw new ArgumentException("Invalid token type specified.", nameof(config.Token)), }; - } /// /// Gets the base headers. /// /// A Dictionary. internal static Dictionary GetBaseHeaders() => new(); /// /// Gets the user agent. /// /// A string. internal static string GetUserAgent() => VersionHeader; /// /// Contains the user mentions. /// /// The message. /// A bool. internal static bool ContainsUserMentions(string message) { var pattern = @"<@(\d+)>"; var regex = new Regex(pattern, RegexOptions.ECMAScript); return regex.IsMatch(message); } /// /// Contains the nickname mentions. /// /// The message. /// A bool. internal static bool ContainsNicknameMentions(string message) { var pattern = @"<@!(\d+)>"; var regex = new Regex(pattern, RegexOptions.ECMAScript); return regex.IsMatch(message); } /// /// Contains the channel mentions. /// /// The message. /// A bool. internal static bool ContainsChannelMentions(string message) { var pattern = @"<#(\d+)>"; var regex = new Regex(pattern, RegexOptions.ECMAScript); return regex.IsMatch(message); } /// /// Contains the role mentions. /// /// The message. /// A bool. internal static bool ContainsRoleMentions(string message) { var pattern = @"<@&(\d+)>"; var regex = new Regex(pattern, RegexOptions.ECMAScript); return regex.IsMatch(message); } /// /// Contains the emojis. /// /// The message. /// A bool. internal static bool ContainsEmojis(string message) { var pattern = @""; var regex = new Regex(pattern, RegexOptions.ECMAScript); return regex.IsMatch(message); } /// /// Gets the user mentions. /// /// The message. /// A list of ulong. internal static IEnumerable GetUserMentions(DiscordMessage message) { var regex = new Regex(@"<@!?(\d+)>", RegexOptions.ECMAScript); var matches = regex.Matches(message.Content); foreach (Match match in matches) yield return ulong.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture); } /// /// Gets the role mentions. /// /// The message. /// A list of ulong. internal static IEnumerable GetRoleMentions(DiscordMessage message) { var regex = new Regex(@"<@&(\d+)>", RegexOptions.ECMAScript); var matches = regex.Matches(message.Content); foreach (Match match in matches) yield return ulong.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture); } /// /// Gets the channel mentions. /// /// The message. /// A list of ulong. internal static IEnumerable GetChannelMentions(DiscordMessage message) { var regex = new Regex(@"<#(\d+)>", RegexOptions.ECMAScript); var matches = regex.Matches(message.Content); foreach (Match match in matches) yield return ulong.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture); } /// /// Gets the emojis. /// /// The message. /// A list of ulong. internal static IEnumerable GetEmojis(DiscordMessage message) { var regex = new Regex(@"", RegexOptions.ECMAScript); var matches = regex.Matches(message.Content); foreach (Match match in matches) yield return ulong.Parse(match.Groups[2].Value, CultureInfo.InvariantCulture); } /// /// Are the valid slash command name. /// /// The name. /// A bool. internal static bool IsValidSlashCommandName(string name) { var regex = new Regex(@"^[\w-]{1,32}$", RegexOptions.ECMAScript); return regex.IsMatch(name); } /// /// Checks the thread auto archive duration feature. /// /// The guild. /// The taad. /// A bool. - internal static bool CheckThreadAutoArchiveDurationFeature(DiscordGuild guild, ThreadAutoArchiveDuration taad) - { - return taad == ThreadAutoArchiveDuration.ThreeDays - ? (guild.PremiumTier.HasFlag(PremiumTier.TierOne) || guild.Features.CanSetThreadArchiveDurationThreeDays) + internal static bool CheckThreadAutoArchiveDurationFeature(DiscordGuild guild, ThreadAutoArchiveDuration taad) => + taad == ThreadAutoArchiveDuration.ThreeDays + ? guild.PremiumTier.HasFlag(PremiumTier.TierOne) || guild.Features.CanSetThreadArchiveDurationThreeDays : taad != ThreadAutoArchiveDuration.OneWeek || guild.PremiumTier.HasFlag(PremiumTier.TierTwo) || guild.Features.CanSetThreadArchiveDurationSevenDays; - } /// /// Checks the thread private feature. /// /// The guild. /// A bool. internal static bool CheckThreadPrivateFeature(DiscordGuild guild) => guild.PremiumTier.HasFlag(PremiumTier.TierTwo) || guild.Features.CanCreatePrivateThreads; /// /// Have the message intents. /// /// The intents. /// A bool. internal static bool HasMessageIntents(DiscordIntents intents) => intents.HasIntent(DiscordIntents.GuildMessages) || intents.HasIntent(DiscordIntents.DirectMessages); /// /// Have the reaction intents. /// /// The intents. /// A bool. internal static bool HasReactionIntents(DiscordIntents intents) => intents.HasIntent(DiscordIntents.GuildMessageReactions) || intents.HasIntent(DiscordIntents.DirectMessageReactions); /// /// Have the typing intents. /// /// The intents. /// A bool. internal static bool HasTypingIntents(DiscordIntents intents) => intents.HasIntent(DiscordIntents.GuildMessageTyping) || intents.HasIntent(DiscordIntents.DirectMessageTyping); // https://discord.com/developers/docs/topics/gateway#sharding-sharding-formula /// /// Gets a shard id from a guild id and total shard count. /// /// The guild id the shard is on. /// The total amount of shards. /// The shard id. public static int GetShardId(ulong guildId, int shardCount) => (int)(guildId >> 22) % shardCount; /// /// Helper method to create a from Unix time seconds for targets that do not support this natively. /// /// Unix time seconds to convert. /// Whether the method should throw on failure. Defaults to true. /// Calculated . public static DateTimeOffset GetDateTimeOffset(long unixTime, bool shouldThrow = true) { try { return DateTimeOffset.FromUnixTimeSeconds(unixTime); } catch (Exception) { if (shouldThrow) throw; return DateTimeOffset.MinValue; } } /// /// Helper method to create a from Unix time milliseconds for targets that do not support this natively. /// /// Unix time milliseconds to convert. /// Whether the method should throw on failure. Defaults to true. /// Calculated . public static DateTimeOffset GetDateTimeOffsetFromMilliseconds(long unixTime, bool shouldThrow = true) { try { return DateTimeOffset.FromUnixTimeMilliseconds(unixTime); } catch (Exception) { if (shouldThrow) throw; return DateTimeOffset.MinValue; } } /// /// Helper method to calculate Unix time seconds from a for targets that do not support this natively. /// /// to calculate Unix time for. /// Calculated Unix time. public static long GetUnixTime(DateTimeOffset dto) => dto.ToUnixTimeMilliseconds(); /// /// Computes a timestamp from a given snowflake. /// /// Snowflake to compute a timestamp from. /// Computed timestamp. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static DateTimeOffset GetSnowflakeTime(this ulong snowflake) => DiscordClient.DiscordEpoch.AddMilliseconds(snowflake >> 22); /// /// Converts this into human-readable format. /// /// Permissions enumeration to convert. /// Human-readable permissions. public static string ToPermissionString(this Permissions perm) { if (perm == Permissions.None) return PermissionStrings[perm]; perm &= PermissionMethods.FullPerms; var strs = PermissionStrings .Where(xkvp => xkvp.Key != Permissions.None && (perm & xkvp.Key) == xkvp.Key) .Select(xkvp => xkvp.Value); return string.Join(", ", strs.OrderBy(xs => xs)); } /// /// Checks whether this string contains given characters. /// /// String to check. /// Characters to check for. /// Whether the string contained these characters. public static bool Contains(this string str, params char[] characters) { foreach (var xc in str) if (characters.Contains(xc)) return true; return false; } /// /// Logs the task fault. /// /// The task. /// The logger. /// The level. /// The event id. /// The message. internal static void LogTaskFault(this Task task, ILogger logger, LogLevel level, EventId eventId, string message) { if (task == null) throw new ArgumentNullException(nameof(task)); if (logger == null) return; task.ContinueWith(t => logger.Log(level, eventId, t.Exception, message), TaskContinuationOptions.OnlyOnFaulted); } /// /// Deconstructs the. /// /// The kvp. /// The key. /// The value. internal static void Deconstruct(this KeyValuePair kvp, out TKey key, out TValue value) { key = kvp.Key; value = kvp.Value; } } }