diff --git a/.editorconfig b/.editorconfig index eef8b7267..3b4dc1c76 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,271 +1,279 @@ # Remove the line below if you want to inherit .editorconfig settings from higher directories root = true #### Core EditorConfig Options #### # All files [*] # General charset = utf-8 trim_trailing_whitespace = true # Indentation and spacing indent_size = 4 indent_style =tab tab_width = 4 # New line preferences end_of_line = crlf insert_final_newline = true dotnet_style_readonly_field=true:suggestion # Project files [*.{csproj,targets,yml}] indent_size = 2 [NuGet.config] indent_size = 2 # Solution files [*.sln] indent_style = tab tab_width = 4 #### .NET Coding Conventions #### # C# files [*.cs] # Organize usings dotnet_separate_import_directive_groups = false dotnet_sort_system_directives_first = true file_header_template = This file is part of the DisCatSharp project, based off DSharpPlus.\n\nCopyright (c) 2021 AITSYS\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the "Software"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE. # this. and Me. preferences dotnet_style_qualification_for_event = true:warning dotnet_style_qualification_for_field = true:warning dotnet_style_qualification_for_method = true:warning dotnet_style_qualification_for_property = true:warning # Language keywords vs BCL types preferences dotnet_style_predefined_type_for_locals_parameters_members = true:error dotnet_style_predefined_type_for_member_access = true:warning # Parentheses preferences dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:warning dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:warning dotnet_style_parentheses_in_other_operators = never_if_unnecessary:suggestion dotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary:suggestion # Modifier preferences dotnet_style_require_accessibility_modifiers = for_non_interface_members # Expression-level preferences dotnet_style_coalesce_expression = true:warning dotnet_style_collection_initializer = true dotnet_style_explicit_tuple_names = true:warning dotnet_style_null_propagation = true:warning dotnet_style_object_initializer = true dotnet_style_operator_placement_when_wrapping = beginning_of_line dotnet_style_prefer_auto_properties = true:suggestion dotnet_style_prefer_compound_assignment = true:warning dotnet_style_prefer_conditional_expression_over_assignment = true:warning dotnet_style_prefer_conditional_expression_over_return = true:warning dotnet_style_prefer_inferred_anonymous_type_member_names = true:warning dotnet_style_prefer_inferred_tuple_names = true:warning dotnet_style_prefer_is_null_check_over_reference_equality_method = true:error dotnet_style_prefer_simplified_boolean_expressions = true dotnet_style_prefer_simplified_interpolation = true # Field preferences dotnet_style_readonly_field = true:warning # Parameter preferences dotnet_code_quality_unused_parameters = non_public # Suppression preferences dotnet_remove_unnecessary_suppression_exclusions = none #### C# Coding Conventions #### # var preferences csharp_style_var_elsewhere = true:suggestion csharp_style_var_for_built_in_types = true:suggestion csharp_style_var_when_type_is_apparent = true:warning # Expression-bodied members csharp_style_expression_bodied_accessors = when_on_single_line:warning csharp_style_expression_bodied_constructors = false:warning csharp_style_expression_bodied_indexers = when_on_single_line:warning csharp_style_expression_bodied_lambdas = true:warning csharp_style_expression_bodied_local_functions = when_on_single_line:warning csharp_style_expression_bodied_methods = when_on_single_line:warning csharp_style_expression_bodied_operators = when_on_single_line:warning csharp_style_expression_bodied_properties = when_on_single_line:warning # Pattern matching preferences csharp_style_pattern_matching_over_as_with_null_check = true:warning csharp_style_pattern_matching_over_is_with_cast_check = true:warning csharp_style_prefer_not_pattern = true:warning csharp_style_prefer_pattern_matching = false csharp_style_prefer_switch_expression = true # Null-checking preferences csharp_style_conditional_delegate_call = false # Modifier preferences csharp_prefer_static_local_function = true:warning csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async # Code-block preferences csharp_prefer_braces = false:suggestion csharp_prefer_simple_using_statement = true # Expression-level preferences csharp_prefer_simple_default_expression = true:warning csharp_style_deconstructed_variable_declaration = true csharp_style_implicit_object_creation_when_type_is_apparent = true csharp_style_inlined_variable_declaration = true:warning csharp_style_pattern_local_over_anonymous_function = true:warning csharp_style_prefer_index_operator = true csharp_style_prefer_range_operator = true csharp_style_throw_expression = false:warning csharp_style_unused_value_assignment_preference = discard_variable:silent csharp_style_unused_value_expression_statement_preference = discard_variable # 'using' directive preferences csharp_using_directive_placement = outside_namespace:error #### C# Formatting Rules #### # New line preferences csharp_new_line_before_catch = true csharp_new_line_before_else = true csharp_new_line_before_finally = true csharp_new_line_before_members_in_anonymous_types = true csharp_new_line_before_members_in_object_initializers = true csharp_new_line_before_open_brace = all csharp_new_line_between_query_expression_clauses = true # Indentation preferences csharp_indent_block_contents = true csharp_indent_braces = false csharp_indent_case_contents = true csharp_indent_case_contents_when_block = false csharp_indent_labels = one_less_than_current csharp_indent_switch_labels = true # Space preferences csharp_space_after_cast = false csharp_space_after_colon_in_inheritance_clause = true csharp_space_after_comma = true csharp_space_after_dot = false csharp_space_after_keywords_in_control_flow_statements = true csharp_space_after_semicolon_in_for_statement = true csharp_space_around_binary_operators = before_and_after csharp_space_around_declaration_statements = ignore csharp_space_before_colon_in_inheritance_clause = true csharp_space_before_comma = false csharp_space_before_dot = false csharp_space_before_open_square_brackets = false csharp_space_before_semicolon_in_for_statement = false csharp_space_between_empty_square_brackets = false csharp_space_between_method_call_empty_parameter_list_parentheses = false csharp_space_between_method_call_name_and_opening_parenthesis = false csharp_space_between_method_call_parameter_list_parentheses = false csharp_space_between_method_declaration_empty_parameter_list_parentheses = false csharp_space_between_method_declaration_name_and_open_parenthesis = false csharp_space_between_method_declaration_parameter_list_parentheses = false csharp_space_between_parentheses = false csharp_space_between_square_brackets = false # Wrapping preferences csharp_preserve_single_line_blocks = true csharp_preserve_single_line_statements = true #### Naming styles #### # Naming rules dotnet_naming_rule.interface_should_be_begins_with_i.severity = error dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i dotnet_naming_rule.private_static_fields_convention.severity = error dotnet_naming_rule.private_static_fields_convention.symbols = private_static_field_props dotnet_naming_rule.private_static_fields_convention.style = private_static_camel_case dotnet_naming_rule.readonly.severity = error dotnet_naming_rule.readonly.symbols = readonly dotnet_naming_rule.readonly.style = underscore_prefixed_camel_case +dotnet_naming_rule.private_field_should_be_underscore_prefixed_camel_case.severity = error +dotnet_naming_rule.private_field_should_be_underscore_prefixed_camel_case.symbols = private_fields +dotnet_naming_rule.private_field_should_be_underscore_prefixed_camel_case.style = underscore_prefixed_camel_case + dotnet_naming_rule.types_should_be_pascal_case.severity = error dotnet_naming_rule.types_should_be_pascal_case.symbols = types dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case dotnet_naming_rule.const_fields_should_be_all_upper.severity = error dotnet_naming_rule.const_fields_should_be_all_upper.symbols = const_fields dotnet_naming_rule.const_fields_should_be_all_upper.style = constant_style dotnet_naming_rule.constants_should_be_upper_case.severity = error dotnet_naming_rule.constants_should_be_upper_case.symbols = constants dotnet_naming_rule.constants_should_be_upper_case.style = constant_style -dotnet_naming_rule.private_field_should_be_underscore_prefixed_camel_case.severity = error -dotnet_naming_rule.private_field_should_be_underscore_prefixed_camel_case.symbols = private_fields -dotnet_naming_rule.private_field_should_be_underscore_prefixed_camel_case.style = underscore_prefixed_camel_case +dotnet_naming_rule.private_props_not_allowed.severity = error +dotnet_naming_rule.private_props_not_allowed.symbols = private_prop +dotnet_naming_rule.private_props_not_allowed.style = constant_style # Symbol specifications dotnet_naming_symbols.interface.applicable_kinds = interface dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected dotnet_naming_symbols.interface.required_modifiers = dotnet_naming_symbols.private_static_field_props.applicable_kinds = field, property dotnet_naming_symbols.private_static_field_props.applicable_accessibilities = private dotnet_naming_symbols.private_static_field_props.required_modifiers = static +dotnet_naming_symbols.private_prop.applicable_kinds = property +dotnet_naming_symbols.private_prop.applicable_accessibilities = private +dotnet_naming_symbols.private_prop.required_modifiers = + dotnet_naming_symbols.constants.applicable_kinds = field, local dotnet_naming_symbols.constants.required_modifiers = const dotnet_naming_symbols.const_fields.applicable_kinds = field, property dotnet_naming_symbols.const_fields.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected dotnet_naming_symbols.const_fields.required_modifiers = const -dotnet_naming_symbols.readonly.applicable_kinds = field +dotnet_naming_symbols.readonly.applicable_kinds = field, property dotnet_naming_symbols.readonly.applicable_accessibilities = private dotnet_naming_symbols.readonly.required_modifiers = readonly dotnet_naming_symbols.private_fields.applicable_kinds = field dotnet_naming_symbols.private_fields.applicable_accessibilities = private, private_protected dotnet_naming_symbols.private_fields.required_modifiers = dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected dotnet_naming_symbols.types.required_modifiers = # Naming styles dotnet_naming_style.pascal_case.required_prefix = dotnet_naming_style.pascal_case.required_suffix = dotnet_naming_style.pascal_case.word_separator = dotnet_naming_style.pascal_case.capitalization = pascal_case dotnet_naming_style.begins_with_i.required_prefix = I dotnet_naming_style.begins_with_i.required_suffix = dotnet_naming_style.begins_with_i.word_separator = dotnet_naming_style.begins_with_i.capitalization = pascal_case dotnet_naming_style.underscore_prefixed_camel_case.required_prefix = _ dotnet_naming_style.underscore_prefixed_camel_case.required_suffix = dotnet_naming_style.underscore_prefixed_camel_case.word_separator = dotnet_naming_style.underscore_prefixed_camel_case.capitalization = camel_case dotnet_naming_style.private_static_camel_case.required_prefix = s_ dotnet_naming_style.private_static_camel_case.capitalization = camel_case dotnet_naming_style.constant_style.required_prefix = dotnet_naming_style.constant_style.required_suffix = dotnet_naming_style.constant_style.word_separator = _ dotnet_naming_style.constant_style.capitalization = all_upper diff --git a/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs b/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs index 1a8748946..4e5396eb8 100644 --- a/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs +++ b/DisCatSharp.ApplicationCommands/ApplicationCommandsExtension.cs @@ -1,1776 +1,1776 @@ // 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(); /// /// List of groups. /// private static List s_groupCommands { get; set; } = new List(); /// /// List of groups with subgroups. /// private static List s_subGroupCommands { get; set; } = new List(); /// /// List of context menus. /// private static List s_contextMenuCommands { get; set; } = new List(); /// /// List of global commands on discords backend. /// internal static List GlobalDiscordCommands { get; set; } = null; /// /// List of guild commands on discords backend. /// internal static Dictionary> GuildDiscordCommands { get; set; } = null; /// /// Singleton modules. /// private static List s_singletonModules { get; set; } = new List(); /// /// List of modules to register. /// - private List> UpdateList { get; set; } = new List>(); + private List> _updateList = new List>(); /// /// 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; /// /// Gets a list of registered commands. The key is the guild id (null if global). /// public IReadOnlyList>> RegisteredCommands => _registeredCommands; private static readonly List>> _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; /// /// Gets the expected count. /// private static int s_expectedCount { get; set; } = 0; /// /// Gets the guild ids where the applications.commands scope is missing. /// - private IReadOnlyList MissingScopeGuildIds { get; set; } + 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)))); + 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))); + 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))); + 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))); + this._updateList.Add(new KeyValuePair(guildId, new ApplicationCommandsModuleConfiguration(type, permissionSetup, translationSetup))); } /* /// /// Registers a command class with permission setup but without a guild id. /// /// The command class to register. /// A callback to setup permissions with. public void RegisterCommands(Action permissionSetup = null) where T : ApplicationCommandsModule { if (this.Client.ShardId == 0) this._updateList.Add(new KeyValuePair(null, new ApplicationCommandsModuleConfiguration(typeof(T), permissionSetup))); } /// /// Registers a command class with permission setup but without a guild id. /// /// The of the command class to register. /// A callback to setup permissions with. public void RegisterCommands(Type type, Action permissionSetup = null) { if (!typeof(ApplicationCommandsModule).IsAssignableFrom(type)) throw new ArgumentException("Command classes have to inherit from ApplicationCommandsModule", nameof(type)); //If sharding, only register for shard 0 if (this.Client.ShardId == 0) this._updateList.Add(new KeyValuePair(null, new ApplicationCommandsModuleConfiguration(type, permissionSetup))); } */ /// /// Fired when the application commands module is ready. /// public event AsyncEventHandler ApplicationCommandsModuleReady { add { this._applicationCommandsModuleReady.Register(value); } remove { this._applicationCommandsModuleReady.Unregister(value); } } private AsyncEvent _applicationCommandsModuleReady; /// /// Fired when the application commands modules startup is finished. /// public event AsyncEventHandler ApplicationCommandsModuleStartupFinished { add { this._applicationCommandsModuleStartupFinished.Register(value); } remove { this._applicationCommandsModuleStartupFinished.Unregister(value); } } private AsyncEvent _applicationCommandsModuleStartupFinished; /// /// Fired when guild commands are registered on a guild. /// public event AsyncEventHandler GuildApplicationCommandsRegistered { add { this._guildApplicationCommandsRegistered.Register(value); } remove { this._guildApplicationCommandsRegistered.Unregister(value); } } private AsyncEvent _guildApplicationCommandsRegistered; /// /// Fired when the global commands are registered. /// public event AsyncEventHandler GlobalApplicationCommandsRegistered { add { this._globalApplicationCommandsRegistered.Register(value); } remove { this._globalApplicationCommandsRegistered.Unregister(value); } } private AsyncEvent _globalApplicationCommandsRegistered; /// /// Used for RegisterCommands and the event. /// internal async Task UpdateAsync() { //Only update for shard 0 if (this.Client.ShardId == 0) { GlobalDiscordCommands = new(); GuildDiscordCommands = new(); - var commandsPending = this.UpdateList.Select(x => x.Key).Distinct(); + 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 + 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.RegisterCommands(this._updateList.Where(x => x.Key == key).Select(x => x.Value), key); } - this.MissingScopeGuildIds = failedGuilds; + 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.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); _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.Single(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 = MissingScopeGuildIds + 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() ? 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(); _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); } } private AsyncEvent _slashError; /// /// Fires when the execution of a slash command is successful. /// public event AsyncEventHandler SlashCommandExecuted { add { this._slashExecuted.Register(value); } remove { this._slashExecuted.Unregister(value); } } private AsyncEvent _slashExecuted; /// /// Fires when the execution of a context menu fails. /// public event AsyncEventHandler ContextMenuErrored { add { this._contextMenuErrored.Register(value); } remove { this._contextMenuErrored.Unregister(value); } } private AsyncEvent _contextMenuErrored; /// /// Fire when the execution of a context menu is successful. /// public event AsyncEventHandler ContextMenuExecuted { add { this._contextMenuExecuted.Register(value); } remove { this._contextMenuExecuted.Unregister(value); } } private AsyncEvent _contextMenuExecuted; } /// /// Holds configuration data for setting up an application command. /// internal class ApplicationCommandsModuleConfiguration { /// /// The type of the command module. /// public Type Type { get; } /// /// The permission setup. /// public Action Setup { get; } public Action Translations { get; } /// /// Creates a new command configuration. /// /// The type of the command module. /// The permission setup callback. /// The translation setup callback. public ApplicationCommandsModuleConfiguration(Type type, Action setup = null, Action translations = null) { this.Type = type; this.Setup = setup; this.Translations = translations; } } /// /// Links a command to its original command module. /// internal class ApplicationCommandSourceLink { /// /// The command. /// public DiscordApplicationCommand ApplicationCommand { get; set; } /// /// The base/root module the command is contained in. /// public Type RootCommandContainerType { get; set; } /// /// The direct group the command is contained in. /// public Type CommandContainerType { get; set; } } /// /// The command method. /// internal class CommandMethod { /// /// Gets or sets the command id. /// public ulong CommandId { get; set; } /// /// Gets or sets the name. /// public string Name { get; set; } /// /// Gets or sets the method. /// public MethodInfo Method { get; set; } } /// /// The group command. /// internal class GroupCommand { /// /// Gets or sets the command id. /// public ulong CommandId { get; set; } /// /// Gets or sets the name. /// public string Name { get; set; } /// /// Gets or sets the methods. /// public List> Methods { get; set; } = null; } /// /// The sub group command. /// internal class SubGroupCommand { /// /// Gets or sets the command id. /// public ulong CommandId { get; set; } /// /// Gets or sets the name. /// public string Name { get; set; } /// /// Gets or sets the sub commands. /// public List SubCommands { get; set; } = new List(); } /// /// The context menu command. /// internal class ContextMenuCommand { /// /// Gets or sets the command id. /// public ulong CommandId { get; set; } /// /// Gets or sets the name. /// public string Name { get; set; } /// /// Gets or sets the method. /// public MethodInfo Method { get; set; } } #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.CommandsNext/Attributes/CooldownAttribute.cs b/DisCatSharp.CommandsNext/Attributes/CooldownAttribute.cs index 50a45be29..4721c42c0 100644 --- a/DisCatSharp.CommandsNext/Attributes/CooldownAttribute.cs +++ b/DisCatSharp.CommandsNext/Attributes/CooldownAttribute.cs @@ -1,333 +1,333 @@ // 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.Globalization; using System.Threading; using System.Threading.Tasks; namespace DisCatSharp.CommandsNext.Attributes { /// /// Defines a cooldown for this command. This allows you to define how many times can users execute a specific command /// [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true, Inherited = false)] public sealed class CooldownAttribute : CheckBaseAttribute { /// /// Gets the maximum number of uses before this command triggers a cooldown for its bucket. /// public int MaxUses { get; } /// /// Gets the time after which the cooldown is reset. /// public TimeSpan Reset { get; } /// /// Gets the type of the cooldown bucket. This determines how cooldowns are applied. /// public CooldownBucketType BucketType { get; } /// /// Gets the cooldown buckets for this command. /// - private ConcurrentDictionary Buckets { get; } + private readonly ConcurrentDictionary _buckets; /// /// Defines a cooldown for this command. This means that users will be able to use the command a specific number of times before they have to wait to use it again. /// /// Number of times the command can be used before triggering a cooldown. /// Number of seconds after which the cooldown is reset. /// Type of cooldown bucket. This allows controlling whether the bucket will be cooled down per user, guild, channel, or globally. public CooldownAttribute(int maxUses, double resetAfter, CooldownBucketType bucketType) { this.MaxUses = maxUses; this.Reset = TimeSpan.FromSeconds(resetAfter); this.BucketType = bucketType; - this.Buckets = new ConcurrentDictionary(); + this._buckets = new ConcurrentDictionary(); } /// /// Gets a cooldown bucket for given command context. /// /// Command context to get cooldown bucket for. /// Requested cooldown bucket, or null if one wasn't present. public CommandCooldownBucket GetBucket(CommandContext ctx) { var bid = this.GetBucketId(ctx, out _, out _, out _); - this.Buckets.TryGetValue(bid, out var bucket); + this._buckets.TryGetValue(bid, out var bucket); return bucket; } /// /// Calculates the cooldown remaining for given command context. /// /// Context for which to calculate the cooldown. /// Remaining cooldown, or zero if no cooldown is active. public TimeSpan GetRemainingCooldown(CommandContext ctx) { var bucket = this.GetBucket(ctx); return bucket == null ? TimeSpan.Zero : bucket.RemainingUses > 0 ? TimeSpan.Zero : bucket.ResetsAt - DateTimeOffset.UtcNow; } /// /// Calculates bucket ID for given command context. /// /// Context for which to calculate bucket ID for. /// ID of the user with which this bucket is associated. /// ID of the channel with which this bucket is associated. /// ID of the guild with which this bucket is associated. /// Calculated bucket ID. private string GetBucketId(CommandContext ctx, out ulong userId, out ulong channelId, out ulong guildId) { userId = 0ul; if ((this.BucketType & CooldownBucketType.User) != 0) userId = ctx.User.Id; channelId = 0ul; if ((this.BucketType & CooldownBucketType.Channel) != 0) channelId = ctx.Channel.Id; if ((this.BucketType & CooldownBucketType.Guild) != 0 && ctx.Guild == null) channelId = ctx.Channel.Id; guildId = 0ul; if (ctx.Guild != null && (this.BucketType & CooldownBucketType.Guild) != 0) guildId = ctx.Guild.Id; var bid = CommandCooldownBucket.MakeId(userId, channelId, guildId); return bid; } /// /// Executes a check. /// /// The command context. /// If true, help - returns true. public override async Task ExecuteCheckAsync(CommandContext ctx, bool help) { if (help) return true; var bid = this.GetBucketId(ctx, out var usr, out var chn, out var gld); - if (!this.Buckets.TryGetValue(bid, out var bucket)) + if (!this._buckets.TryGetValue(bid, out var bucket)) { bucket = new CommandCooldownBucket(this.MaxUses, this.Reset, usr, chn, gld); - this.Buckets.AddOrUpdate(bid, bucket, (k, v) => bucket); + this._buckets.AddOrUpdate(bid, bucket, (k, v) => bucket); } return await bucket.DecrementUseAsync().ConfigureAwait(false); } } /// /// Defines how are command cooldowns applied. /// public enum CooldownBucketType : int { /// /// Denotes that the command will have its cooldown applied per-user. /// User = 1, /// /// Denotes that the command will have its cooldown applied per-channel. /// Channel = 2, /// /// Denotes that the command will have its cooldown applied per-guild. In DMs, this applies the cooldown per-channel. /// Guild = 4, /// /// Denotes that the command will have its cooldown applied globally. /// Global = 0 } /// /// Represents a cooldown bucket for commands. /// public sealed class CommandCooldownBucket : IEquatable { /// /// Gets the ID of the user with whom this cooldown is associated. /// public ulong UserId { get; } /// /// Gets the ID of the channel with which this cooldown is associated. /// public ulong ChannelId { get; } /// /// Gets the ID of the guild with which this cooldown is associated. /// public ulong GuildId { get; } /// /// Gets the ID of the bucket. This is used to distinguish between cooldown buckets. /// public string BucketId { get; } /// /// Gets the remaining number of uses before the cooldown is triggered. /// public int RemainingUses => Volatile.Read(ref this._remainingUses); private int _remainingUses; /// /// Gets the maximum number of times this command can be used in given timespan. /// public int MaxUses { get; } /// /// Gets the date and time at which the cooldown resets. /// public DateTimeOffset ResetsAt { get; internal set; } /// /// Gets the time after which this cooldown resets. /// public TimeSpan Reset { get; internal set; } /// /// Gets the semaphore used to lock the use value. /// - private SemaphoreSlim UsageSemaphore { get; } + private readonly SemaphoreSlim _usageSemaphore; /// /// Creates a new command cooldown bucket. /// /// Maximum number of uses for this bucket. /// Time after which this bucket resets. /// ID of the user with which this cooldown is associated. /// ID of the channel with which this cooldown is associated. /// ID of the guild with which this cooldown is associated. internal CommandCooldownBucket(int maxUses, TimeSpan resetAfter, ulong userId = 0, ulong channelId = 0, ulong guildId = 0) { this._remainingUses = maxUses; this.MaxUses = maxUses; this.ResetsAt = DateTimeOffset.UtcNow + resetAfter; this.Reset = resetAfter; this.UserId = userId; this.ChannelId = channelId; this.GuildId = guildId; this.BucketId = MakeId(userId, channelId, guildId); - this.UsageSemaphore = new SemaphoreSlim(1, 1); + this._usageSemaphore = new SemaphoreSlim(1, 1); } /// /// Decrements the remaining use counter. /// /// Whether decrement succeded or not. internal async Task DecrementUseAsync() { - await this.UsageSemaphore.WaitAsync().ConfigureAwait(false); + await this._usageSemaphore.WaitAsync().ConfigureAwait(false); // if we're past reset time... var now = DateTimeOffset.UtcNow; if (now >= this.ResetsAt) { // ...do the reset and set a new reset time Interlocked.Exchange(ref this._remainingUses, this.MaxUses); this.ResetsAt = now + this.Reset; } // check if we have any uses left, if we do... var success = false; if (this.RemainingUses > 0) { // ...decrement, and return success... Interlocked.Decrement(ref this._remainingUses); success = true; } // ...otherwise just fail - this.UsageSemaphore.Release(); + this._usageSemaphore.Release(); return success; } /// /// Returns a string representation of this command cooldown bucket. /// /// String representation of this command cooldown bucket. public override string ToString() => $"Command bucket {this.BucketId}"; /// /// 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 CommandCooldownBucket); /// /// Checks whether this is equal to another . /// /// to compare to. /// Whether the is equal to this . public bool Equals(CommandCooldownBucket other) => other is not null && (ReferenceEquals(this, other) || (this.UserId == other.UserId && this.ChannelId == other.ChannelId && this.GuildId == other.GuildId)); /// /// Gets the hash code for this . /// /// The hash code for this . public override int GetHashCode() => HashCode.Combine(this.UserId, this.ChannelId, this.GuildId); /// /// Gets whether the two objects are equal. /// /// First bucket to compare. /// Second bucket to compare. /// Whether the two buckets are equal. public static bool operator ==(CommandCooldownBucket bucket1, CommandCooldownBucket bucket2) { var null1 = bucket1 is null; var null2 = bucket2 is null; return (null1 && null2) || (null1 == null2 && null1.Equals(null2)); } /// /// Gets whether the two objects are not equal. /// /// First bucket to compare. /// Second bucket to compare. /// Whether the two buckets are not equal. public static bool operator !=(CommandCooldownBucket bucket1, CommandCooldownBucket bucket2) => !(bucket1 == bucket2); /// /// Creates a bucket ID from given bucket parameters. /// /// ID of the user with which this cooldown is associated. /// ID of the channel with which this cooldown is associated. /// ID of the guild with which this cooldown is associated. /// Generated bucket ID. public static string MakeId(ulong userId = 0, ulong channelId = 0, ulong guildId = 0) => $"{userId.ToString(CultureInfo.InvariantCulture)}:{channelId.ToString(CultureInfo.InvariantCulture)}:{guildId.ToString(CultureInfo.InvariantCulture)}"; } } diff --git a/DisCatSharp.CommandsNext/CommandsNextExtension.cs b/DisCatSharp.CommandsNext/CommandsNextExtension.cs index 25f0dbe80..84bd842cb 100644 --- a/DisCatSharp.CommandsNext/CommandsNextExtension.cs +++ b/DisCatSharp.CommandsNext/CommandsNextExtension.cs @@ -1,1081 +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 CommandsNextConfiguration Config { get; } + private readonly CommandsNextConfiguration _config; + /// /// Gets the help formatter. /// - private HelpFormatterFactory HelpFormatter { get; } + private readonly HelpFormatterFactory _helpFormatter; /// /// Gets the convert generic. /// - private MethodInfo ConvertGeneric { get; } + private readonly MethodInfo _convertGeneric; + /// /// Gets the user friendly type names. /// - private Dictionary UserFriendlyTypeNames { get; } + 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; + => 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._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() + 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]; + 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; + 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(); + 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) + 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) + if (this._config.EnableDefaultHelp) { this.RegisterCommands(typeof(DefaultHelpModule), null, null, out var tcmds); - if (this.Config.DefaultHelpChecks != null) + if (this._config.DefaultHelpChecks != null) { - var checks = this.Config.DefaultHelpChecks.ToArray(); + 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) + if (!this._config.EnableDms && e.Channel.IsPrivate) return; var mpos = -1; - if (this.Config.EnableMentionPrefix) + 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 (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); + 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 && 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 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, + 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 Dictionary TopLevelCommands { get; set; } + private 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) + 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()); + 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) + 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()); + 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); + 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)))) + 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; + this._topLevelCommands[cmd.Name] = cmd; if (cmd.Aliases != null) foreach (var xs in cmd.Aliases) - this.TopLevelCommands[xs] = cmd; + 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); + 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, + 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); + 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 (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); + 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; + this._userFriendlyTypeNames[t] = value; if (!ti.IsValueType) return; var nullableType = typeof(Nullable<>).MakeGenericType(t); - this.UserFriendlyTypeNames[nullableType] = value; + 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]; + 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 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 + => 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); } } 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); } } 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/DefaultHelpFormatter.cs b/DisCatSharp.CommandsNext/Converters/DefaultHelpFormatter.cs index 85506f091..34b0c560b 100644 --- a/DisCatSharp.CommandsNext/Converters/DefaultHelpFormatter.cs +++ b/DisCatSharp.CommandsNext/Converters/DefaultHelpFormatter.cs @@ -1,123 +1,124 @@ // 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 System.Linq; using System.Text; using DisCatSharp.CommandsNext.Entities; using DisCatSharp.Entities; namespace DisCatSharp.CommandsNext.Converters { /// /// Default CommandsNext help formatter. /// public class DefaultHelpFormatter : BaseHelpFormatter { /// /// Gets the embed builder. /// public DiscordEmbedBuilder EmbedBuilder { get; } + /// /// Gets or sets the command. /// - private Command Command { get; set; } + private Command _command; /// /// Creates a new default help formatter. /// /// Context in which this formatter is being invoked. public DefaultHelpFormatter(CommandContext ctx) : base(ctx) { this.EmbedBuilder = new DiscordEmbedBuilder() .WithTitle("Help") .WithColor(0x007FFF); } /// /// Sets the command this help message will be for. /// /// Command for which the help message is being produced. /// This help formatter. public override BaseHelpFormatter WithCommand(Command command) { - this.Command = command; + this._command = command; this.EmbedBuilder.WithDescription($"{Formatter.InlineCode(command.Name)}: {command.Description ?? "No description provided."}"); if (command is CommandGroup cgroup && cgroup.IsExecutableWithoutSubcommands) this.EmbedBuilder.WithDescription($"{this.EmbedBuilder.Description}\n\nThis group can be executed as a standalone command."); if (command.Aliases?.Any() == true) this.EmbedBuilder.AddField("Aliases", string.Join(", ", command.Aliases.Select(Formatter.InlineCode)), false); if (command.Overloads?.Any() == true) { var sb = new StringBuilder(); foreach (var ovl in command.Overloads.OrderByDescending(x => x.Priority)) { sb.Append('`').Append(command.QualifiedName); foreach (var arg in ovl.Arguments) sb.Append(arg.IsOptional || arg.IsCatchAll ? " [" : " <").Append(arg.Name).Append(arg.IsCatchAll ? "..." : "").Append(arg.IsOptional || arg.IsCatchAll ? ']' : '>'); sb.Append("`\n"); foreach (var arg in ovl.Arguments) sb.Append('`').Append(arg.Name).Append(" (").Append(this.CommandsNext.GetUserFriendlyTypeName(arg.Type)).Append(")`: ").Append(arg.Description ?? "No description provided.").Append('\n'); sb.Append('\n'); } this.EmbedBuilder.AddField("Arguments", sb.ToString().Trim(), false); } return this; } /// /// Sets the subcommands for this command, if applicable. This method will be called with filtered data. /// /// Subcommands for this command group. /// This help formatter. public override BaseHelpFormatter WithSubcommands(IEnumerable subcommands) { - this.EmbedBuilder.AddField(this.Command != null ? "Subcommands" : "Commands", string.Join(", ", subcommands.Select(x => Formatter.InlineCode(x.Name))), false); + this.EmbedBuilder.AddField(this._command != null ? "Subcommands" : "Commands", string.Join(", ", subcommands.Select(x => Formatter.InlineCode(x.Name))), false); return this; } /// /// Construct the help message. /// /// Data for the help message. public override CommandHelpMessage Build() { - if (this.Command == null) + if (this._command == null) this.EmbedBuilder.WithDescription("Listing all top-level commands and groups. Specify a command to see more information."); return new CommandHelpMessage(embed: this.EmbedBuilder.Build()); } } } diff --git a/DisCatSharp.CommandsNext/Converters/HelpFormatterFactory.cs b/DisCatSharp.CommandsNext/Converters/HelpFormatterFactory.cs index ba40c826e..668dae2ab 100644 --- a/DisCatSharp.CommandsNext/Converters/HelpFormatterFactory.cs +++ b/DisCatSharp.CommandsNext/Converters/HelpFormatterFactory.cs @@ -1,53 +1,53 @@ // 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.CommandsNext.Converters { /// /// Represents the help formatter factory. /// internal class HelpFormatterFactory { /// /// Gets or sets the factory. /// - private ObjectFactory Factory { get; set; } + private ObjectFactory _factory; /// /// Initializes a new instance of the class. /// public HelpFormatterFactory() { } /// /// Sets the formatter type. /// - public void SetFormatterType() where T : BaseHelpFormatter => this.Factory = ActivatorUtilities.CreateFactory(typeof(T), new[] { typeof(CommandContext) }); + public void SetFormatterType() where T : BaseHelpFormatter => this._factory = ActivatorUtilities.CreateFactory(typeof(T), new[] { typeof(CommandContext) }); /// /// Creates the help formatter. /// /// The command context. - public BaseHelpFormatter Create(CommandContext ctx) => this.Factory(ctx.Services, new object[] { ctx }) as BaseHelpFormatter; + public BaseHelpFormatter Create(CommandContext ctx) => this._factory(ctx.Services, new object[] { ctx }) as BaseHelpFormatter; } } diff --git a/DisCatSharp.CommandsNext/Entities/Builders/CommandBuilder.cs b/DisCatSharp.CommandsNext/Entities/Builders/CommandBuilder.cs index 8d0b8e58f..b02e158b7 100644 --- a/DisCatSharp.CommandsNext/Entities/Builders/CommandBuilder.cs +++ b/DisCatSharp.CommandsNext/Entities/Builders/CommandBuilder.cs @@ -1,300 +1,305 @@ // 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 DisCatSharp.CommandsNext.Attributes; using DisCatSharp.CommandsNext.Entities; using DisCatSharp.CommandsNext.Exceptions; namespace DisCatSharp.CommandsNext.Builders { /// /// Represents an interface to build a command. /// public class CommandBuilder { /// /// Gets the name set for this command. /// public string Name { get; private set; } /// /// Gets the aliases set for this command. /// public IReadOnlyList Aliases { get; } + /// /// Gets the alias list. /// - private List AliasList { get; } + private readonly List _aliasList; /// /// Gets the description set for this command. /// public string Description { get; private set; } /// /// Gets whether this command will be hidden or not. /// public bool IsHidden { get; private set; } /// /// Gets the execution checks defined for this command. /// public IReadOnlyList ExecutionChecks { get; } + /// /// Gets the execution check list. /// - private List ExecutionCheckList { get; } + private readonly List _executionCheckList; /// /// Gets the collection of this command's overloads. /// public IReadOnlyList Overloads { get; } + /// /// Gets the overload list. /// - private List OverloadList { get; } + private readonly List _overloadList; + /// /// Gets the overload argument sets. /// - private HashSet OverloadArgumentSets { get; } + private readonly HashSet _overloadArgumentSets; /// /// Gets the module on which this command is to be defined. /// public ICommandModule Module { get; } /// /// Gets custom attributes defined on this command. /// public IReadOnlyList CustomAttributes { get; } + /// /// Gets the custom attribute list. /// - private List CustomAttributeList { get; } + private readonly List _customAttributeList; /// /// Creates a new module-less command builder. /// public CommandBuilder() : this(null) { } /// /// Creates a new command builder. /// /// Module on which this command is to be defined. public CommandBuilder(ICommandModule module) { - this.AliasList = new List(); - this.Aliases = new ReadOnlyCollection(this.AliasList); + this._aliasList = new List(); + this.Aliases = new ReadOnlyCollection(this._aliasList); - this.ExecutionCheckList = new List(); - this.ExecutionChecks = new ReadOnlyCollection(this.ExecutionCheckList); + this._executionCheckList = new List(); + this.ExecutionChecks = new ReadOnlyCollection(this._executionCheckList); - this.OverloadArgumentSets = new HashSet(); - this.OverloadList = new List(); - this.Overloads = new ReadOnlyCollection(this.OverloadList); + this._overloadArgumentSets = new HashSet(); + this._overloadList = new List(); + this.Overloads = new ReadOnlyCollection(this._overloadList); this.Module = module; - this.CustomAttributeList = new List(); - this.CustomAttributes = new ReadOnlyCollection(this.CustomAttributeList); + this._customAttributeList = new List(); + this.CustomAttributes = new ReadOnlyCollection(this._customAttributeList); } /// /// Sets the name for this command. /// /// Name for this command. /// This builder. public CommandBuilder WithName(string name) { if (name == null || name.ToCharArray().Any(xc => char.IsWhiteSpace(xc))) throw new ArgumentException("Command name cannot be null or contain any whitespace characters.", nameof(name)); if (this.Name != null) throw new InvalidOperationException("This command already has a name."); - if (this.AliasList.Contains(name)) + if (this._aliasList.Contains(name)) throw new ArgumentException("Command name cannot be one of its aliases.", nameof(name)); this.Name = name; return this; } /// /// Adds aliases to this command. /// /// Aliases to add to the command. /// This builder. public CommandBuilder WithAliases(params string[] aliases) { if (aliases == null || !aliases.Any()) throw new ArgumentException("You need to pass at least one alias.", nameof(aliases)); foreach (var alias in aliases) this.WithAlias(alias); return this; } /// /// Adds an alias to this command. /// /// Alias to add to the command. /// This builder. public CommandBuilder WithAlias(string alias) { if (alias.ToCharArray().Any(xc => char.IsWhiteSpace(xc))) throw new ArgumentException("Aliases cannot contain whitespace characters or null strings.", nameof(alias)); - if (this.Name == alias || this.AliasList.Contains(alias)) + if (this.Name == alias || this._aliasList.Contains(alias)) throw new ArgumentException("Aliases cannot contain the command name, and cannot be duplicate.", nameof(alias)); - this.AliasList.Add(alias); + this._aliasList.Add(alias); return this; } /// /// Sets the description for this command. /// /// Description to use for this command. /// This builder. public CommandBuilder WithDescription(string description) { this.Description = description; return this; } /// /// Sets whether this command is to be hidden. /// /// Whether the command is to be hidden. /// This builder. public CommandBuilder WithHiddenStatus(bool hidden) { this.IsHidden = hidden; return this; } /// /// Adds pre-execution checks to this command. /// /// Pre-execution checks to add to this command. /// This builder. public CommandBuilder WithExecutionChecks(params CheckBaseAttribute[] checks) { - this.ExecutionCheckList.AddRange(checks.Except(this.ExecutionCheckList)); + this._executionCheckList.AddRange(checks.Except(this._executionCheckList)); return this; } /// /// Adds a pre-execution check to this command. /// /// Pre-execution check to add to this command. /// This builder. public CommandBuilder WithExecutionCheck(CheckBaseAttribute check) { - if (!this.ExecutionCheckList.Contains(check)) - this.ExecutionCheckList.Add(check); + if (!this._executionCheckList.Contains(check)) + this._executionCheckList.Add(check); return this; } /// /// Adds overloads to this command. An executable command needs to have at least one overload. /// /// Overloads to add to this command. /// This builder. public CommandBuilder WithOverloads(params CommandOverloadBuilder[] overloads) { foreach (var overload in overloads) this.WithOverload(overload); return this; } /// /// Adds an overload to this command. An executable command needs to have at least one overload. /// /// Overload to add to this command. /// This builder. public CommandBuilder WithOverload(CommandOverloadBuilder overload) { - if (this.OverloadArgumentSets.Contains(overload.ArgumentSet)) + if (this._overloadArgumentSets.Contains(overload.ArgumentSet)) throw new DuplicateOverloadException(this.Name, overload.Arguments.Select(x => x.Type).ToList(), overload.ArgumentSet); - this.OverloadArgumentSets.Add(overload.ArgumentSet); - this.OverloadList.Add(overload); + this._overloadArgumentSets.Add(overload.ArgumentSet); + this._overloadList.Add(overload); return this; } /// /// Adds a custom attribute to this command. This can be used to indicate various custom information about a command. /// /// Attribute to add. /// This builder. public CommandBuilder WithCustomAttribute(Attribute attribute) { - this.CustomAttributeList.Add(attribute); + this._customAttributeList.Add(attribute); return this; } /// /// Adds multiple custom attributes to this command. This can be used to indicate various custom information about a command. /// /// Attributes to add. /// This builder. public CommandBuilder WithCustomAttributes(params Attribute[] attributes) { foreach (var attr in attributes) this.WithCustomAttribute(attr); return this; } /// /// Builds the command. /// /// The parent command group. internal virtual Command Build(CommandGroup parent) { var cmd = new Command { Name = this.Name, Description = this.Description, Aliases = this.Aliases, ExecutionChecks = this.ExecutionChecks, IsHidden = this.IsHidden, Parent = parent, Overloads = new ReadOnlyCollection(this.Overloads.Select(xo => xo.Build()).ToList()), Module = this.Module, CustomAttributes = this.CustomAttributes }; return cmd; } } } diff --git a/DisCatSharp.CommandsNext/Entities/Builders/CommandGroupBuilder.cs b/DisCatSharp.CommandsNext/Entities/Builders/CommandGroupBuilder.cs index add38ac17..350920138 100644 --- a/DisCatSharp.CommandsNext/Entities/Builders/CommandGroupBuilder.cs +++ b/DisCatSharp.CommandsNext/Entities/Builders/CommandGroupBuilder.cs @@ -1,100 +1,101 @@ // 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 System.Collections.ObjectModel; using System.Linq; using DisCatSharp.CommandsNext.Entities; namespace DisCatSharp.CommandsNext.Builders { /// /// Represents an interface to build a command group. /// public sealed class CommandGroupBuilder : CommandBuilder { /// /// Gets the list of child commands registered for this group. /// public IReadOnlyList Children { get; } + /// /// Gets the children list. /// - private List ChildrenList { get; } + private readonly List _childrenList; /// /// Creates a new module-less command group builder. /// public CommandGroupBuilder() : this(null) { } /// /// Creates a new command group builder. /// /// Module on which this group is to be defined. public CommandGroupBuilder(ICommandModule module) : base(module) { - this.ChildrenList = new List(); - this.Children = new ReadOnlyCollection(this.ChildrenList); + this._childrenList = new List(); + this.Children = new ReadOnlyCollection(this._childrenList); } /// /// Adds a command to the collection of child commands for this group. /// /// Command to add to the collection of child commands for this group. /// This builder. public CommandGroupBuilder WithChild(CommandBuilder child) { - this.ChildrenList.Add(child); + this._childrenList.Add(child); return this; } /// /// Builds the command group. /// /// The parent command group. internal override Command Build(CommandGroup parent) { var cmd = new CommandGroup { Name = this.Name, Description = this.Description, Aliases = this.Aliases, ExecutionChecks = this.ExecutionChecks, IsHidden = this.IsHidden, Parent = parent, Overloads = new ReadOnlyCollection(this.Overloads.Select(xo => xo.Build()).ToList()), Module = this.Module, CustomAttributes = this.CustomAttributes }; var cs = new List(); foreach (var xc in this.Children) cs.Add(xc.Build(cmd)); cmd.Children = new ReadOnlyCollection(cs); return cmd; } } } diff --git a/DisCatSharp.CommandsNext/Entities/Builders/CommandOverloadBuilder.cs b/DisCatSharp.CommandsNext/Entities/Builders/CommandOverloadBuilder.cs index 06bb6d14a..1314302cb 100644 --- a/DisCatSharp.CommandsNext/Entities/Builders/CommandOverloadBuilder.cs +++ b/DisCatSharp.CommandsNext/Entities/Builders/CommandOverloadBuilder.cs @@ -1,193 +1,193 @@ // 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.Linq.Expressions; using System.Reflection; using System.Text; using DisCatSharp.CommandsNext.Attributes; using DisCatSharp.CommandsNext.Exceptions; namespace DisCatSharp.CommandsNext.Builders { /// /// Represents an interface to build a command overload. /// public sealed class CommandOverloadBuilder { /// /// Gets a value that uniquely identifies an overload. /// internal string ArgumentSet { get; } /// /// Gets the collection of arguments this overload takes. /// public IReadOnlyList Arguments { get; } /// /// Gets this overload's priority when picking a suitable one for execution. /// public int Priority { get; set; } /// /// Gets the overload's callable delegate. /// public Delegate Callable { get; set; } /// /// Gets the invocation target. /// - private object InvocationTarget { get; } + private readonly object _invocationTarget; /// /// Creates a new command overload builder from specified method. /// /// Method to use for this overload. public CommandOverloadBuilder(MethodInfo method) : this(method, null) { } /// /// Creates a new command overload builder from specified delegate. /// /// Delegate to use for this overload. public CommandOverloadBuilder(Delegate method) : this(method.GetMethodInfo(), method.Target) { } /// /// Prevents a default instance of the class from being created. /// /// The method. /// The target. private CommandOverloadBuilder(MethodInfo method, object target) { if (!method.IsCommandCandidate(out var prms)) throw new ArgumentException("Specified method is not suitable for a command.", nameof(method)); - this.InvocationTarget = target; + this._invocationTarget = target; // create the argument array var ea = new ParameterExpression[prms.Length + 1]; var iep = Expression.Parameter(target?.GetType() ?? method.DeclaringType, "instance"); ea[0] = iep; ea[1] = Expression.Parameter(typeof(CommandContext), "ctx"); var pri = method.GetCustomAttribute(); if (pri != null) this.Priority = pri.Priority; var i = 2; var args = new List(prms.Length - 1); var setb = new StringBuilder(); foreach (var arg in prms.Skip(1)) { setb.Append(arg.ParameterType).Append(";"); var ca = new CommandArgument { Name = arg.Name, Type = arg.ParameterType, IsOptional = arg.IsOptional, DefaultValue = arg.IsOptional ? arg.DefaultValue : null }; var attrsCustom = new List(); var attrs = arg.GetCustomAttributes(); var isParams = false; foreach (var xa in attrs) { switch (xa) { case DescriptionAttribute d: ca.Description = d.Description; break; case RemainingTextAttribute r: ca.IsCatchAll = true; break; case ParamArrayAttribute p: ca.IsCatchAll = true; ca.Type = arg.ParameterType.GetElementType(); ca.IsArray = true; isParams = true; break; default: attrsCustom.Add(xa); break; } } if (i > 2 && !ca.IsOptional && !ca.IsCatchAll && args[i - 3].IsOptional) throw new InvalidOverloadException("Non-optional argument cannot appear after an optional one", method, arg); if (arg.ParameterType.IsArray && !isParams) throw new InvalidOverloadException("Cannot use array arguments without params modifier.", method, arg); ca.CustomAttributes = new ReadOnlyCollection(attrsCustom); args.Add(ca); ea[i++] = Expression.Parameter(arg.ParameterType, arg.Name); } //var ec = Expression.Call(iev, method, ea.Skip(2)); var ec = Expression.Call(iep, method, ea.Skip(1)); var el = Expression.Lambda(ec, ea); this.ArgumentSet = setb.ToString(); this.Arguments = new ReadOnlyCollection(args); this.Callable = el.Compile(); } /// /// Sets the priority for this command overload. /// /// Priority for this command overload. /// This builder. public CommandOverloadBuilder WithPriority(int priority) { this.Priority = priority; return this; } /// /// Builds the command overload. /// internal CommandOverload Build() { var ovl = new CommandOverload() { Arguments = this.Arguments, Priority = this.Priority, Callable = this.Callable, - InvocationTarget = this.InvocationTarget + InvocationTarget = this._invocationTarget }; return ovl; } } } diff --git a/DisCatSharp.CommandsNext/Exceptions/DuplicateOverloadException.cs b/DisCatSharp.CommandsNext/Exceptions/DuplicateOverloadException.cs index 59ffb5560..8d4fa6579 100644 --- a/DisCatSharp.CommandsNext/Exceptions/DuplicateOverloadException.cs +++ b/DisCatSharp.CommandsNext/Exceptions/DuplicateOverloadException.cs @@ -1,69 +1,69 @@ // 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; namespace DisCatSharp.CommandsNext.Exceptions { /// /// Indicates that given argument set already exists as an overload for specified command. /// public class DuplicateOverloadException : Exception { /// /// Gets the name of the command that already has the overload. /// public string CommandName { get; } /// /// Gets the ordered collection of argument types for the specified overload. /// public IReadOnlyList ArgumentTypes { get; } /// /// Gets the argument set key. /// - private string ArgumentSetKey { get; } + private readonly string _argumentSetKey; /// /// Creates a new exception indicating given argument set already exists as an overload for specified command. /// /// Name of the command with duplicated argument sets. /// Collection of ordered argument types for the command. /// Overload identifier. internal DuplicateOverloadException(string name, IList argumentTypes, string argumentSetKey) : base("An overload with specified argument types exists.") { this.CommandName = name; this.ArgumentTypes = new ReadOnlyCollection(argumentTypes); - this.ArgumentSetKey = argumentSetKey; + this._argumentSetKey = argumentSetKey; } /// /// Returns a string representation of this . /// /// A string representation. - public override string ToString() => $"{this.GetType()}: {this.Message}\nCommand name: {this.CommandName}\nArgument types: {this.ArgumentSetKey}"; // much like System.ArgumentException works + public override string ToString() => $"{this.GetType()}: {this.Message}\nCommand name: {this.CommandName}\nArgument types: {this._argumentSetKey}"; // much like System.ArgumentException works } } diff --git a/DisCatSharp.Common/Types/CharSpanLookupDictionary.cs b/DisCatSharp.Common/Types/CharSpanLookupDictionary.cs index 840cae40f..206be380f 100644 --- a/DisCatSharp.Common/Types/CharSpanLookupDictionary.cs +++ b/DisCatSharp.Common/Types/CharSpanLookupDictionary.cs @@ -1,830 +1,832 @@ // 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; /// /// 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(); /// /// 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 Dictionary InternalBuckets { get; } + private readonly Dictionary _internalBuckets; /// /// Creates a new, empty with string keys and items of type . /// public CharSpanLookupDictionary() { - this.InternalBuckets = new Dictionary(); + 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); + 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._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) + 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) + 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)) + if (!this._internalBuckets.ContainsKey(hash)) { - this.InternalBuckets.Add(hash, new KeyedValue(key, hash, value)); + this._internalBuckets.Add(hash, new KeyedValue(key, hash, value)); this.Count++; return true; } - var kdv = this.InternalBuckets[hash]; + 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)) + 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)) + 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._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._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)) + 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) + 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) + 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); /// /// Gets the internal dictionary. /// - private CharSpanLookupDictionary InternalDictionary { get; } + private readonly CharSpanLookupDictionary _internalDictionary; + /// /// Gets the internal enumerator. /// - private IEnumerator> InternalEnumerator { get; } + private readonly IEnumerator> _internalEnumerator; + /// /// Gets or sets the current value. /// - private KeyedValue CurrentValue { get; set; } = null; + 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(); + this._internalDictionary = spDict; + this._internalEnumerator = this._internalDictionary._internalBuckets.GetEnumerator(); } /// /// Moves the next. /// /// A bool. public bool MoveNext() { - var kdv = this.CurrentValue; + var kdv = this._currentValue; if (kdv == null) { - if (!this.InternalEnumerator.MoveNext()) + if (!this._internalEnumerator.MoveNext()) return false; - kdv = this.InternalEnumerator.Current.Value; + kdv = this._internalEnumerator.Current.Value; this.Current = new KeyValuePair(kdv.Key, kdv.Value); - this.CurrentValue = kdv.Next; + this._currentValue = kdv.Next; return true; } this.Current = new KeyValuePair(kdv.Key, kdv.Value); - this.CurrentValue = kdv.Next; + this._currentValue = kdv.Next; return true; } /// /// Resets the. /// public void Reset() { - this.InternalEnumerator.Reset(); + this._internalEnumerator.Reset(); this.Current = default; - this.CurrentValue = null; + this._currentValue = null; } /// /// Disposes the. /// public void Dispose() { this.Reset(); } } } } diff --git a/DisCatSharp.Common/Types/CharSpanLookupReadOnlyDictionary.cs b/DisCatSharp.Common/Types/CharSpanLookupReadOnlyDictionary.cs index 6ee32c4f7..b65618d41 100644 --- a/DisCatSharp.Common/Types/CharSpanLookupReadOnlyDictionary.cs +++ b/DisCatSharp.Common/Types/CharSpanLookupReadOnlyDictionary.cs @@ -1,417 +1,419 @@ // 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 IReadOnlyDictionary InternalBuckets { get; } + 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._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)) + 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)) + 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) + 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) + 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 CharSpanLookupReadOnlyDictionary InternalDictionary { get; } + private readonly CharSpanLookupReadOnlyDictionary _internalDictionary; + /// /// Gets the internal enumerator. /// - private IEnumerator> InternalEnumerator { get; } + private readonly IEnumerator> _internalEnumerator; + /// /// Gets or sets the current value. /// - private KeyedValue CurrentValue { get; set; } = null; + 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(); + this._internalDictionary = spDict; + this._internalEnumerator = this._internalDictionary._internalBuckets.GetEnumerator(); } /// /// Moves the next. /// /// A bool. public bool MoveNext() { - var kdv = this.CurrentValue; + var kdv = this._currentValue; if (kdv == null) { - if (!this.InternalEnumerator.MoveNext()) + if (!this._internalEnumerator.MoveNext()) return false; - kdv = this.InternalEnumerator.Current.Value; + kdv = this._internalEnumerator.Current.Value; this.Current = new KeyValuePair(kdv.Key, kdv.Value); - this.CurrentValue = kdv.Next; + this._currentValue = kdv.Next; return true; } this.Current = new KeyValuePair(kdv.Key, kdv.Value); - this.CurrentValue = kdv.Next; + this._currentValue = kdv.Next; return true; } /// /// Resets the. /// public void Reset() { - this.InternalEnumerator.Reset(); + this._internalEnumerator.Reset(); this.Current = default; - this.CurrentValue = null; + this._currentValue = null; } /// /// Disposes the. /// public void Dispose() { this.Reset(); } } } } diff --git a/DisCatSharp.Common/Types/Optional.cs b/DisCatSharp.Common/Types/Optional.cs index d97163f75..8d537369b 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(@"{DebuggerDisplay,nq}")] + [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(); /// /// 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 DebuggerDisplay + 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); /// /// 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); /// /// 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); /// /// 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 d5dd2bdc4..e27526a15 100644 --- a/DisCatSharp.Common/Types/SecureRandom.cs +++ b/DisCatSharp.Common/Types/SecureRandom.cs @@ -1,348 +1,348 @@ // 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 RandomNumberGenerator Rng { get; } = RandomNumberGenerator.Create(); + private readonly RandomNumberGenerator _rng = RandomNumberGenerator.Create(); private volatile bool _isDisposed = false; /// /// 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); + 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); + 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); + 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); + 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(); + 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.Interactivity/EventHandling/Components/Requests/ComponentMatchRequest.cs b/DisCatSharp.Interactivity/EventHandling/Components/Requests/ComponentMatchRequest.cs index f45a25db6..163473569 100644 --- a/DisCatSharp.Interactivity/EventHandling/Components/Requests/ComponentMatchRequest.cs +++ b/DisCatSharp.Interactivity/EventHandling/Components/Requests/ComponentMatchRequest.cs @@ -1,69 +1,69 @@ // 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; using DisCatSharp.Entities; using DisCatSharp.EventArgs; namespace DisCatSharp.Interactivity.EventHandling { /// /// Represents a match that is being waited for. /// internal class ComponentMatchRequest { /// /// The id to wait on. This should be uniquely formatted to avoid collisions. /// public DiscordMessage Message { get; private set; } /// /// The completion source that represents the result of the match. /// public TaskCompletionSource Tcs { get; private set; } = new(); - protected readonly CancellationToken _cancellation; - protected readonly Func _predicate; + protected readonly CancellationToken Cancellation; + protected readonly Func Predicate; /// /// Initializes a new instance of the class. /// /// The message. /// The predicate. /// The cancellation token. public ComponentMatchRequest(DiscordMessage message, Func predicate, CancellationToken cancellation) { this.Message = message; - this._predicate = predicate; - this._cancellation = cancellation; - this._cancellation.Register(() => this.Tcs.TrySetResult(null)); // TrySetCancelled would probably be better but I digress ~Velvet // + this.Predicate = predicate; + this.Cancellation = cancellation; + this.Cancellation.Register(() => this.Tcs.TrySetResult(null)); // TrySetCancelled would probably be better but I digress ~Velvet // } /// /// Whether it is a match. /// /// The arguments. - public bool IsMatch(ComponentInteractionCreateEventArgs args) => this._predicate(args); + public bool IsMatch(ComponentInteractionCreateEventArgs args) => this.Predicate(args); } } diff --git a/DisCatSharp.Lavalink/LavalinkNodeConnection.cs b/DisCatSharp.Lavalink/LavalinkNodeConnection.cs index 21ff5d844..fd18e688d 100644 --- a/DisCatSharp.Lavalink/LavalinkNodeConnection.cs +++ b/DisCatSharp.Lavalink/LavalinkNodeConnection.cs @@ -1,605 +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); } } private readonly AsyncEvent _lavalinkSocketError; /// /// Triggered when this node disconnects. /// public event AsyncEventHandler Disconnected { 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); } } 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); } } 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); } } 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); } } 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); } } 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); } } 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; /// /// 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 { get; set; } + private IWebSocketClient _webSocket; /// /// Gets the voice state updates. /// - private ConcurrentDictionary> VoiceStateUpdates { get; } + private readonly ConcurrentDictionary> _voiceStateUpdates; + /// /// Gets the voice server updates. /// - private ConcurrentDictionary> VoiceServerUpdates { get; } + 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._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 = 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)); + 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); + 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); + 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); + 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; + 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)) + 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)) + 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); + await this._webSocket.SendMessageAsync(payload).ConfigureAwait(false); } internal event NodeDisconnectedEventHandler NodeDisconnected; } } diff --git a/DisCatSharp.VoiceNext/Codec/Opus.cs b/DisCatSharp.VoiceNext/Codec/Opus.cs index 71507c161..1d26a4394 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 IntPtr Encoder { get; } + private readonly IntPtr _encoder; /// /// Gets the managed decoders. /// - private List ManagedDecoders { get; } + 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); + 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); + 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(); + 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); + 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) + lock (this._managedDecoders) { var managedDecoder = new OpusDecoder(this); - this.ManagedDecoders.Add(managedDecoder); + this._managedDecoders.Add(managedDecoder); return managedDecoder; } } /// /// Destroys the decoder. /// /// The decoder. public void DestroyDecoder(OpusDecoder decoder) { - lock (this.ManagedDecoders) + lock (this._managedDecoders) { - if (!this.ManagedDecoders.Contains(decoder)) + if (!this._managedDecoders.Contains(decoder)) return; - this.ManagedDecoders.Remove(decoder); + this._managedDecoders.Remove(decoder); decoder.Dispose(); } } /// /// Disposes the Opus. /// public void Dispose() { - Interop.OpusDestroyEncoder(this.Encoder); + Interop.OpusDestroyEncoder(this._encoder); - lock (this.ManagedDecoders) + lock (this._managedDecoders) { - foreach (var decoder in 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; /// /// 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/Sodium.cs b/DisCatSharp.VoiceNext/Codec/Sodium.cs index c344f33df..4fab379b5 100644 --- a/DisCatSharp.VoiceNext/Codec/Sodium.cs +++ b/DisCatSharp.VoiceNext/Codec/Sodium.cs @@ -1,292 +1,294 @@ // 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; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Runtime.CompilerServices; using System.Security.Cryptography; namespace DisCatSharp.VoiceNext.Codec { /// /// The sodium. /// internal sealed class Sodium : IDisposable { /// /// Gets the supported modes. /// public static IReadOnlyDictionary SupportedModes { get; } /// /// Gets the nonce size. /// public static int NonceSize => Interop.SodiumNonceSize; /// /// Gets the c s p r n g. /// - private RandomNumberGenerator Csprng { get; } + private readonly RandomNumberGenerator _csprng; + /// /// Gets the buffer. /// - private byte[] Buffer { get; } + private readonly byte[] _buffer; + /// /// Gets the key. /// - private ReadOnlyMemory Key { get; } + private readonly ReadOnlyMemory _key; /// /// Initializes a new instance of the class. /// static Sodium() { SupportedModes = new ReadOnlyDictionary(new Dictionary() { ["xsalsa20_poly1305_lite"] = EncryptionMode.XSalsa20Poly1305Lite, ["xsalsa20_poly1305_suffix"] = EncryptionMode.XSalsa20Poly1305Suffix, ["xsalsa20_poly1305"] = EncryptionMode.XSalsa20Poly1305 }); } /// /// Initializes a new instance of the class. /// /// The key. public Sodium(ReadOnlyMemory key) { if (key.Length != Interop.SodiumKeySize) throw new ArgumentException($"Invalid Sodium key size. Key needs to have a length of {Interop.SodiumKeySize} bytes.", nameof(key)); - this.Key = key; + this._key = key; - this.Csprng = RandomNumberGenerator.Create(); - this.Buffer = new byte[Interop.SodiumNonceSize]; + this._csprng = RandomNumberGenerator.Create(); + this._buffer = new byte[Interop.SodiumNonceSize]; } /// /// Generates the nonce. /// /// The rtp header. /// The target. public void GenerateNonce(ReadOnlySpan rtpHeader, Span target) { if (rtpHeader.Length != Rtp.HEADER_SIZE) throw new ArgumentException($"RTP header needs to have a length of exactly {Rtp.HEADER_SIZE} bytes.", nameof(rtpHeader)); if (target.Length != Interop.SodiumNonceSize) throw new ArgumentException($"Invalid nonce buffer size. Target buffer for the nonce needs to have a capacity of {Interop.SodiumNonceSize} bytes.", nameof(target)); // Write the header to the beginning of the span. rtpHeader.CopyTo(target); // Zero rest of the span. Helpers.ZeroFill(target[rtpHeader.Length..]); } /// /// Generates the nonce. /// /// The target. public void GenerateNonce(Span target) { if (target.Length != Interop.SodiumNonceSize) throw new ArgumentException($"Invalid nonce buffer size. Target buffer for the nonce needs to have a capacity of {Interop.SodiumNonceSize} bytes.", nameof(target)); - this.Csprng.GetBytes(this.Buffer); - this.Buffer.AsSpan().CopyTo(target); + this._csprng.GetBytes(this._buffer); + this._buffer.AsSpan().CopyTo(target); } /// /// Generates the nonce. /// /// The nonce. /// The target. public void GenerateNonce(uint nonce, Span target) { if (target.Length != Interop.SodiumNonceSize) throw new ArgumentException($"Invalid nonce buffer size. Target buffer for the nonce needs to have a capacity of {Interop.SodiumNonceSize} bytes.", nameof(target)); // Write the uint to memory BinaryPrimitives.WriteUInt32BigEndian(target, nonce); // Zero rest of the buffer. Helpers.ZeroFill(target[4..]); } /// /// Appends the nonce. /// /// The nonce. /// The target. /// The encryption mode. public void AppendNonce(ReadOnlySpan nonce, Span target, EncryptionMode encryptionMode) { switch (encryptionMode) { case EncryptionMode.XSalsa20Poly1305: return; case EncryptionMode.XSalsa20Poly1305Suffix: nonce.CopyTo(target[^12..]); return; case EncryptionMode.XSalsa20Poly1305Lite: nonce[..4].CopyTo(target[^4..]); return; default: throw new ArgumentException("Unsupported encryption mode.", nameof(encryptionMode)); } } /// /// Gets the nonce. /// /// The source. /// The target. /// The encryption mode. public void GetNonce(ReadOnlySpan source, Span target, EncryptionMode encryptionMode) { if (target.Length != Interop.SodiumNonceSize) throw new ArgumentException($"Invalid nonce buffer size. Target buffer for the nonce needs to have a capacity of {Interop.SodiumNonceSize} bytes.", nameof(target)); switch (encryptionMode) { case EncryptionMode.XSalsa20Poly1305: source[..12].CopyTo(target); return; case EncryptionMode.XSalsa20Poly1305Suffix: source[^Interop.SodiumNonceSize..].CopyTo(target); return; case EncryptionMode.XSalsa20Poly1305Lite: source[^4..].CopyTo(target); return; default: throw new ArgumentException("Unsupported encryption mode.", nameof(encryptionMode)); } } /// /// Encrypts the Sodium. /// /// The source. /// The target. /// The nonce. public void Encrypt(ReadOnlySpan source, Span target, ReadOnlySpan nonce) { if (nonce.Length != Interop.SodiumNonceSize) throw new ArgumentException($"Invalid nonce size. Nonce needs to have a length of {Interop.SodiumNonceSize} bytes.", nameof(nonce)); if (target.Length != Interop.SodiumMacSize + source.Length) throw new ArgumentException($"Invalid target buffer size. Target buffer needs to have a length that is a sum of input buffer length and Sodium MAC size ({Interop.SodiumMacSize} bytes).", nameof(target)); int result; - if ((result = Interop.Encrypt(source, target, this.Key.Span, nonce)) != 0) + if ((result = Interop.Encrypt(source, target, this._key.Span, nonce)) != 0) throw new CryptographicException($"Could not encrypt the buffer. Sodium returned code {result}."); } /// /// Decrypts the Sodium. /// /// The source. /// The target. /// The nonce. public void Decrypt(ReadOnlySpan source, Span target, ReadOnlySpan nonce) { if (nonce.Length != Interop.SodiumNonceSize) throw new ArgumentException($"Invalid nonce size. Nonce needs to have a length of {Interop.SodiumNonceSize} bytes.", nameof(nonce)); if (target.Length != source.Length - Interop.SodiumMacSize) throw new ArgumentException($"Invalid target buffer size. Target buffer needs to have a length that is input buffer decreased by Sodium MAC size ({Interop.SodiumMacSize} bytes).", nameof(target)); int result; - if ((result = Interop.Decrypt(source, target, this.Key.Span, nonce)) != 0) + if ((result = Interop.Decrypt(source, target, this._key.Span, nonce)) != 0) throw new CryptographicException($"Could not decrypt the buffer. Sodium returned code {result}."); } /// /// Disposes the Sodium. /// - public void Dispose() => this.Csprng.Dispose(); + public void Dispose() => this._csprng.Dispose(); /// /// Selects the mode. /// /// The available modes. /// A KeyValuePair. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static KeyValuePair SelectMode(IEnumerable availableModes) { foreach (var kvMode in SupportedModes) if (availableModes.Contains(kvMode.Key)) return kvMode; throw new CryptographicException("Could not negotiate Sodium encryption modes, as none of the modes offered by Discord are supported. This is usually an indicator that something went very wrong."); } /// /// Calculates the target size. /// /// The source. /// An int. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int CalculateTargetSize(ReadOnlySpan source) => source.Length + Interop.SodiumMacSize; /// /// Calculates the source size. /// /// The source. /// An int. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int CalculateSourceSize(ReadOnlySpan source) => source.Length - Interop.SodiumMacSize; } /// /// Specifies an encryption mode to use with Sodium. /// public enum EncryptionMode { /// /// The nonce is an incrementing uint32 value. It is encoded as big endian value at the beginning of the nonce buffer. The 4 bytes are also appended at the end of the packet. /// XSalsa20Poly1305Lite, /// /// The nonce consists of random bytes. It is appended at the end of a packet. /// XSalsa20Poly1305Suffix, /// /// The nonce consists of the RTP header. Nothing is appended to the packet. /// XSalsa20Poly1305 } } diff --git a/DisCatSharp.VoiceNext/VoiceNextConnection.cs b/DisCatSharp.VoiceNext/VoiceNextConnection.cs index e0440c728..74d7dc82f 100644 --- a/DisCatSharp.VoiceNext/VoiceNextConnection.cs +++ b/DisCatSharp.VoiceNext/VoiceNextConnection.cs @@ -1,1330 +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); } } 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); } } 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); } } 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); } } 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); } } 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); /// /// Gets the discord. /// - private DiscordClient Discord { get; } + private readonly DiscordClient _discord; + /// /// Gets the guild. /// - private DiscordGuild Guild { get; } + private readonly DiscordGuild _guild; + /// /// Gets the transmitting s s r cs. /// - private ConcurrentDictionary TransmittingSsrCs { get; } + private readonly ConcurrentDictionary _transmittingSsrCs; /// /// Gets the udp client. /// - private BaseUdpClient UdpClient { get; } + private readonly BaseUdpClient _udpClient; + /// /// Gets or sets the voice ws. /// - private IWebSocketClient VoiceWs { get; set; } + private IWebSocketClient _voiceWs; + /// /// Gets or sets the heartbeat task. /// - private Task HeartbeatTask { get; set; } + private Task _heartbeatTask; + /// /// Gets or sets the heartbeat interval. /// - private int HeartbeatInterval { get; set; } + private int _heartbeatInterval; + /// /// Gets or sets the last heartbeat. /// - private DateTimeOffset LastHeartbeat { get; set; } + private DateTimeOffset _lastHeartbeat; /// /// Gets or sets the token source. /// - private CancellationTokenSource TokenSource { get; set; } + private CancellationTokenSource _tokenSource; /// /// Gets the token. /// - private CancellationToken Token - => this.TokenSource.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 VoiceNextConfiguration Configuration { get; } + private readonly VoiceNextConfiguration _configuration; + /// /// Gets or sets the opus. /// - private Opus Opus { get; set; } + private Opus _opus; + /// /// Gets or sets the sodium. /// - private Sodium Sodium { get; set; } + private Sodium _sodium; + /// /// Gets or sets the rtp. /// - private Rtp Rtp { get; set; } + private Rtp _rtp; + /// /// Gets or sets the selected encryption mode. /// - private EncryptionMode SelectedEncryptionMode { get; set; } + private EncryptionMode _selectedEncryptionMode; /// /// Gets or sets the nonce. /// - private uint Nonce { get; set; } = 0; + private uint _nonce = 0; /// /// Gets or sets the sequence. /// - private ushort Sequence { get; set; } + private ushort _sequence; + /// /// Gets or sets the timestamp. /// - private uint Timestamp { get; set; } + private uint _timestamp; + /// /// Gets or sets the s s r c. /// - private uint Ssrc { get; set; } + private uint _ssrc; + /// /// Gets or sets the key. /// - private byte[] Key { get; set; } + private byte[] _key; + /// /// Gets or sets the discovered endpoint. /// - private IpEndpoint DiscoveredEndpoint { get; set; } + 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 TaskCompletionSource ReadyWait { get; set; } + private readonly TaskCompletionSource _readyWait; + /// /// Gets or sets a value indicating whether is initialized. /// - private bool IsInitialized { get; set; } + private bool _isInitialized; + /// /// Gets or sets a value indicating whether is disposed. /// - private bool IsDisposed { get; set; } + private bool _isDisposed; /// /// Gets or sets the playing wait. /// - private TaskCompletionSource PlayingWait { get; set; } + private TaskCompletionSource _playingWait; /// /// Gets the pause event. /// - private AsyncManualResetEvent PauseEvent { get; } + private readonly AsyncManualResetEvent _pauseEvent; + /// /// Gets or sets the transmit stream. /// - private VoiceTransmitSink TransmitStream { get; set; } + private VoiceTransmitSink _transmitStream; + /// /// Gets the transmit channel. /// - private Channel TransmitChannel { get; } + private readonly Channel _transmitChannel; + /// /// Gets the keepalive timestamps. /// - private ConcurrentDictionary KeepaliveTimestamps { get; } + private readonly ConcurrentDictionary _keepaliveTimestamps; private ulong _lastKeepalive = 0; /// /// Gets or sets the sender task. /// - private Task SenderTask { get; set; } + private Task _senderTask; + /// /// Gets or sets the sender token source. /// - private CancellationTokenSource SenderTokenSource { get; set; } + private CancellationTokenSource _senderTokenSource; /// /// Gets the sender token. /// - private CancellationToken SenderToken - => this.SenderTokenSource.Token; + private CancellationToken SENDER_TOKEN + => this._senderTokenSource.Token; /// /// Gets or sets the receiver task. /// - private Task ReceiverTask { get; set; } + private Task _receiverTask; + /// /// Gets or sets the receiver token source. /// - private CancellationTokenSource ReceiverTokenSource { get; set; } + private CancellationTokenSource _receiverTokenSource; /// /// Gets the receiver token. /// - private CancellationToken ReceiverToken - => this.ReceiverTokenSource.Token; + private CancellationToken RECEIVER_TOKEN + => this._receiverTokenSource.Token; /// /// Gets or sets the keepalive task. /// - private Task KeepaliveTask { get; set; } + private Task _keepaliveTask; + /// /// Gets or sets the keepalive token source. /// - private CancellationTokenSource KeepaliveTokenSource { get; set; } + private CancellationTokenSource _keepaliveTokenSource; /// /// Gets the keepalive token. /// - private CancellationToken KeepaliveToken - => this.KeepaliveTokenSource.Token; + private CancellationToken KEEPALIVE_TOKEN + => this._keepaliveTokenSource.Token; private volatile bool _isSpeaking = false; /// /// Gets the audio format used by the Opus encoder. /// - public AudioFormat AudioFormat => this.Configuration.AudioFormat; + public AudioFormat AudioFormat => this._configuration.AudioFormat; /// /// Gets whether this connection is still playing audio. /// public bool IsPlaying - => this.PlayingWait != null && !this.PlayingWait.Task.IsCompleted; + => 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; /// /// Gets the UDP round-trip time in ms. /// public int UdpPing => Volatile.Read(ref this._udpPing); private int _udpPing = 0; 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._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._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._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._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._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; + 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); + return this._voiceWs.ConnectAsync(gwuri.Uri); } /// /// Reconnects . /// /// A Task. internal Task ReconnectAsync() - => this.VoiceWs.DisconnectAsync(); + => 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; + => 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); + 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) + if (this._isDisposed) return false; var audioFormat = this.AudioFormat; - var packetArray = ArrayPool.Shared.Rent(this.Rtp.CalculatePacketSize(audioFormat.SampleCountToSampleSize(audioFormat.CalculateMaximumFrameSize()), this.SelectedEncryptionMode)); + 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); + 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._opus.Encode(pcm, ref opus); - this.Sequence++; - this.Timestamp += (uint)audioFormat.CalculateFrameSize(audioFormat.CalculateSampleDuration(pcm.Length)); + this._sequence++; + this._timestamp += (uint)audioFormat.CalculateFrameSize(audioFormat.CalculateSampleDuration(pcm.Length)); Span nonce = stackalloc byte[Sodium.NonceSize]; - switch (this.SelectedEncryptionMode) + switch (this._selectedEncryptionMode) { case EncryptionMode.XSalsa20Poly1305: - this.Sodium.GenerateNonce(packet[..Rtp.HEADER_SIZE], nonce); + this._sodium.GenerateNonce(packet[..Rtp.HEADER_SIZE], nonce); break; case EncryptionMode.XSalsa20Poly1305Suffix: - this.Sodium.GenerateNonce(nonce); + this._sodium.GenerateNonce(nonce); break; case EncryptionMode.XSalsa20Poly1305Lite: - this.Sodium.GenerateNonce(this.Nonce++, nonce); + 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); + 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); + 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.SenderToken; - var client = this.UdpClient; - var reader = this.TransmitChannel.Reader; + 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); + 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); + 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(); + 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); + 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)) + if (!this._rtp.IsRtpHeader(data)) return false; - this.Rtp.DecodeHeader(data, out var sequence, out var timestamp, out var ssrc, out var hasExtension); + this._rtp.DecodeHeader(data, out var sequence, out var timestamp, out var ssrc, out var hasExtension); - if (!this.TransmittingSsrCs.TryGetValue(ssrc, out var vtx)) + if (!this._transmittingSsrCs.TryGetValue(ssrc, out var vtx)) { - var decoder = this.Opus.CreateDecoder(); + 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"); + 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); + 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); + 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 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 _); + 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); + 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); + 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); + 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) + 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) + 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"); + 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)) + 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); + 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"); + 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.ReceiverToken; - var client = this.UdpClient; + 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) + 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) + 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); + if (this._transmitStream == null) + this._transmitStream = new VoiceTransmitSink(this, sampleDuration); - return this.TransmitStream; + 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); + if (this._playingWait != null) + await this._playingWait.Task.ConfigureAwait(false); } /// /// Pauses playback. /// public void Pause() - => this.PauseEvent.Reset(); + => this._pauseEvent.Reset(); /// /// Asynchronously resumes playback. /// /// public async Task ResumeAsync() - => await this.PauseEvent.SetAsync().ConfigureAwait(false); + => 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) + if (this._isDisposed) return; try { - this.IsDisposed = true; - this.IsInitialized = false; - this.TokenSource?.Cancel(); - this.SenderTokenSource?.Cancel(); - this.ReceiverTokenSource?.Cancel(); + 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); + this._discord.Logger.LogError(ex, ex.Message); } try { - this.VoiceWs.DisconnectAsync().ConfigureAwait(false).GetAwaiter().GetResult(); - this.UdpClient.Close(); + 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; + 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._discord.Logger.LogError(ex, ex.Message); } - this.VoiceDisconnected?.Invoke(this.Guild); + this.VoiceDisconnected?.Invoke(this._guild); } /// /// Heartbeats . /// /// A Task. private async Task HeartbeatAsync() { await Task.Yield(); - var token = this.Token; + var token = this.TOKEN; while (true) { try { token.ThrowIfCancellationRequested(); var dt = DateTime.Now; - this.Discord.Logger.LogTrace(VoiceNextEvents.VoiceHeartbeat, "Sent heartbeat"); + 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); + 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.KeepaliveToken; - var client = this.UdpClient; + 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); + 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); + this._udpClient.Setup(this.UdpEndpoint); var pck = new byte[70]; PreparePacket(pck); - await this.UdpClient.SendAsync(pck, pck.Length).ConfigureAwait(false); + await this._udpClient.SendAsync(pck, pck.Length).ConfigureAwait(false); - var ipd = await this.UdpClient.ReceiveAsync().ConfigureAwait(false); + var ipd = await this._udpClient.ReceiveAsync().ConfigureAwait(false); ReadPacket(ipd, out var ip, out var port); - this.DiscoveredEndpoint = new IpEndpoint + this._discoveredEndpoint = new IpEndpoint { Address = ip, Port = port }; - this.Discord.Logger.LogTrace(VoiceNextEvents.VoiceHandshake, "Endpoint dicovery finished - discovered endpoint is {0}:{1}", ip, 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 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; + this._selectedEncryptionMode = selectedEncryptionMode.Value; // Ready - this.Discord.Logger.LogTrace(VoiceNextEvents.VoiceHandshake, "Selected encryption mode is {0}", selectedEncryptionMode.Key); + 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, + 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.SenderToken); + this._senderTokenSource = new CancellationTokenSource(); + this._senderTask = Task.Run(this.VoiceSenderTask, this.SENDER_TOKEN); - this.ReceiverTokenSource = new CancellationTokenSource(); - this.ReceiverTask = Task.Run(this.UdpReceiverTask, this.ReceiverToken); + 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); + 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(); + 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); + 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)"); + this._discord.Logger.LogTrace(VoiceNextEvents.VoiceDispatch, "Received READY (OP2)"); var vrp = opp.ToObject(); - this.Ssrc = vrp.Ssrc; + 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); + 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)"); + 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()); + 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)"); + 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) + 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) + if (foundUserInCache && this._transmittingSsrCs.TryGetValue(spk.Ssrc, out var txssrc5) && txssrc5.Id == 0) { txssrc5.User = spk.User; } else { - var opus = this.Opus.CreateDecoder(); + var opus = this._opus.CreateDecoder(); var vtx = new AudioSender(spk.Ssrc, opus) { - User = await this.Discord.GetUserAsync(spd.UserId.Value).ConfigureAwait(false) + User = await this._discord.GetUserAsync(spd.UserId.Value).ConfigureAwait(false) }; - if (!this.TransmittingSsrCs.TryAdd(spk.Ssrc, vtx)) - this.Opus.DestroyDecoder(opus); + 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; + 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; + 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(); + 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); + 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)"); + 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 usrj = await this._discord.GetUserAsync(ujpd.UserId).ConfigureAwait(false); { - var opus = this.Opus.CreateDecoder(); + 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); + 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); + 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)"); + 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)) + 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); + 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) + 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); + 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); + 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) + 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; + 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"); + 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); + 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 }); + => 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); + 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/VoiceNextExtension.cs b/DisCatSharp.VoiceNext/VoiceNextExtension.cs index d33e29d84..49213044f 100644 --- a/DisCatSharp.VoiceNext/VoiceNextExtension.cs +++ b/DisCatSharp.VoiceNext/VoiceNextExtension.cs @@ -1,261 +1,263 @@ // 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.Tasks; using DisCatSharp.Entities; using DisCatSharp.EventArgs; using DisCatSharp.Net; using DisCatSharp.VoiceNext.Entities; using Newtonsoft.Json; namespace DisCatSharp.VoiceNext { /// /// Represents VoiceNext extension, which acts as Discord voice client. /// public sealed class VoiceNextExtension : BaseExtension { /// /// Gets or sets the configuration. /// - private VoiceNextConfiguration Configuration { get; set; } + private readonly VoiceNextConfiguration _configuration; /// /// Gets or sets the active connections. /// - private ConcurrentDictionary ActiveConnections { get; set; } + private readonly ConcurrentDictionary _activeConnections; + /// /// Gets or sets the voice state updates. /// - private ConcurrentDictionary> VoiceStateUpdates { get; set; } + private readonly ConcurrentDictionary> _voiceStateUpdates; + /// /// Gets or sets the voice server updates. /// - private ConcurrentDictionary> VoiceServerUpdates { get; set; } + private readonly ConcurrentDictionary> _voiceServerUpdates; /// /// Gets whether this connection has incoming voice enabled. /// public bool IsIncomingEnabled { get; } /// /// Initializes a new instance of the class. /// /// The config. internal VoiceNextExtension(VoiceNextConfiguration config) { - this.Configuration = new VoiceNextConfiguration(config); + this._configuration = new VoiceNextConfiguration(config); this.IsIncomingEnabled = config.EnableIncoming; - this.ActiveConnections = new ConcurrentDictionary(); - this.VoiceStateUpdates = new ConcurrentDictionary>(); - this.VoiceServerUpdates = new ConcurrentDictionary>(); + this._activeConnections = new ConcurrentDictionary(); + this._voiceStateUpdates = new ConcurrentDictionary>(); + this._voiceServerUpdates = new ConcurrentDictionary>(); } /// /// 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.Client.VoiceStateUpdated += this.Client_VoiceStateUpdate; this.Client.VoiceServerUpdated += this.Client_VoiceServerUpdate; } /// /// Create a VoiceNext connection for the specified channel. /// /// Channel to connect to. /// VoiceNext connection for this channel. public async Task ConnectAsync(DiscordChannel channel) { if (channel.Type != ChannelType.Voice && channel.Type != ChannelType.Stage) throw new ArgumentException(nameof(channel), "Invalid channel specified; needs to be voice or stage channel"); if (channel.Guild == null) throw new ArgumentException(nameof(channel), "Invalid channel specified; needs to be guild channel"); if (!channel.PermissionsFor(channel.Guild.CurrentMember).HasPermission(Permissions.AccessChannels | Permissions.UseVoice)) throw new InvalidOperationException("You need AccessChannels and UseVoice permission to connect to this voice channel"); var gld = channel.Guild; - if (this.ActiveConnections.ContainsKey(gld.Id)) + if (this._activeConnections.ContainsKey(gld.Id)) throw new InvalidOperationException("This guild already has a voice connection"); var vstut = new TaskCompletionSource(); var vsrut = new TaskCompletionSource(); - this.VoiceStateUpdates[gld.Id] = vstut; - this.VoiceServerUpdates[gld.Id] = vsrut; + this._voiceStateUpdates[gld.Id] = vstut; + this._voiceServerUpdates[gld.Id] = vsrut; var vsd = new VoiceDispatch { OpCode = 4, Payload = new VoiceStateUpdatePayload { GuildId = gld.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 vstup = new VoiceStateUpdatePayload { SessionId = vstu.SessionId, UserId = vstu.User.Id }; var vsru = await vsrut.Task.ConfigureAwait(false); var vsrup = new VoiceServerUpdatePayload { Endpoint = vsru.Endpoint, GuildId = vsru.Guild.Id, Token = vsru.VoiceToken }; - var vnc = new VoiceNextConnection(this.Client, gld, channel, this.Configuration, vsrup, vstup); + var vnc = new VoiceNextConnection(this.Client, gld, channel, this._configuration, vsrup, vstup); vnc.VoiceDisconnected += this.Vnc_VoiceDisconnected; await vnc.ConnectAsync().ConfigureAwait(false); await vnc.WaitForReadyAsync().ConfigureAwait(false); - this.ActiveConnections[gld.Id] = vnc; + this._activeConnections[gld.Id] = vnc; return vnc; } /// /// Gets a VoiceNext connection for specified guild. /// /// Guild to get VoiceNext connection for. /// VoiceNext connection for the specified guild. - public VoiceNextConnection GetConnection(DiscordGuild guild) => this.ActiveConnections.ContainsKey(guild.Id) ? this.ActiveConnections[guild.Id] : null; + public VoiceNextConnection GetConnection(DiscordGuild guild) => this._activeConnections.ContainsKey(guild.Id) ? this._activeConnections[guild.Id] : null; /// /// Vnc_S the voice disconnected. /// /// The guild. /// A Task. private async Task Vnc_VoiceDisconnected(DiscordGuild guild) { VoiceNextConnection vnc = null; - if (this.ActiveConnections.ContainsKey(guild.Id)) - this.ActiveConnections.TryRemove(guild.Id, out vnc); + if (this._activeConnections.ContainsKey(guild.Id)) + this._activeConnections.TryRemove(guild.Id, out vnc); var vsd = new VoiceDispatch { OpCode = 4, Payload = new VoiceStateUpdatePayload { GuildId = guild.Id, ChannelId = null } }; var vsj = JsonConvert.SerializeObject(vsd, Formatting.None); await (guild.Discord as DiscordClient).WsSendAsync(vsj).ConfigureAwait(false); } /// /// Client_S the voice state update. /// /// The client. /// The e. /// A Task. private Task Client_VoiceStateUpdate(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.Client.CurrentUser.Id) { - if (e.After.Channel == null && this.ActiveConnections.TryRemove(gld.Id, out var ac)) + if (e.After.Channel == null && this._activeConnections.TryRemove(gld.Id, out var ac)) ac.Disconnect(); - if (this.ActiveConnections.TryGetValue(e.Guild.Id, out var vnc)) + if (this._activeConnections.TryGetValue(e.Guild.Id, out var vnc)) vnc.TargetChannel = e.Channel; - if (!string.IsNullOrWhiteSpace(e.SessionId) && e.Channel != null && this.VoiceStateUpdates.TryRemove(gld.Id, out var xe)) + if (!string.IsNullOrWhiteSpace(e.SessionId) && e.Channel != null && this._voiceStateUpdates.TryRemove(gld.Id, out var xe)) xe.SetResult(e); } return Task.CompletedTask; } /// /// Client_S the voice server update. /// /// The client. /// The e. /// A Task. private async Task Client_VoiceServerUpdate(DiscordClient client, VoiceServerUpdateEventArgs e) { var gld = e.Guild; if (gld == null) return; - if (this.ActiveConnections.TryGetValue(e.Guild.Id, out var vnc)) + if (this._activeConnections.TryGetValue(e.Guild.Id, out var vnc)) { vnc.ServerData = new VoiceServerUpdatePayload { Endpoint = e.Endpoint, GuildId = e.Guild.Id, Token = e.VoiceToken }; var eps = e.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; } vnc.WebSocketEndpoint = new ConnectionEndpoint { Hostname = eph, Port = epp }; vnc.Resume = false; await vnc.ReconnectAsync().ConfigureAwait(false); } - if (this.VoiceServerUpdates.ContainsKey(gld.Id)) + if (this._voiceServerUpdates.ContainsKey(gld.Id)) { - this.VoiceServerUpdates.TryRemove(gld.Id, out var xe); + this._voiceServerUpdates.TryRemove(gld.Id, out var xe); xe.SetResult(e); } } } } diff --git a/DisCatSharp.VoiceNext/VoiceTransmitSink.cs b/DisCatSharp.VoiceNext/VoiceTransmitSink.cs index f42de0919..643815646 100644 --- a/DisCatSharp.VoiceNext/VoiceTransmitSink.cs +++ b/DisCatSharp.VoiceNext/VoiceTransmitSink.cs @@ -1,282 +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.Buffers; using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using DisCatSharp.VoiceNext.Codec; namespace DisCatSharp.VoiceNext { /// /// Sink used to transmit audio data via . /// public sealed class VoiceTransmitSink : IDisposable { /// /// Gets the PCM sample duration for this sink. /// public int SampleDuration - => this.PcmBufferDuration; + => this._pcmBufferDuration; /// /// Gets the length of the PCM buffer for this sink. /// Written packets should adhere to this size, but the sink will adapt to fit. /// public int SampleLength - => this.PcmBuffer.Length; + => this._pcmBuffer.Length; /// /// Gets or sets the volume modifier for this sink. Changing this will alter the volume of the output. 1.0 is 100%. /// public double VolumeModifier { get => this._volume; set { if (value < 0 || value > 2.5) throw new ArgumentOutOfRangeException(nameof(value), "Volume needs to be between 0% and 250%."); this._volume = value; } } private double _volume = 1.0; /// /// Gets the connection. /// - private VoiceNextConnection Connection { get; } + private readonly VoiceNextConnection _connection; + /// /// Gets the pcm buffer duration. /// - private int PcmBufferDuration { get; } + private readonly int _pcmBufferDuration; + /// /// Gets the pcm buffer. /// - private byte[] PcmBuffer { get; } + private readonly byte[] _pcmBuffer; + /// /// Gets the pcm memory. /// - private Memory PcmMemory { get; } + private readonly Memory _pcmMemory; + /// /// Gets or sets the pcm buffer length. /// - private int PcmBufferLength { get; set; } + private int _pcmBufferLength; + /// /// Gets the write semaphore. /// - private SemaphoreSlim WriteSemaphore { get; } + private readonly SemaphoreSlim _writeSemaphore; + /// /// Gets the filters. /// - private List Filters { get; } + private readonly List _filters; /// /// Initializes a new instance of the class. /// /// The vnc. /// The pcm buffer duration. internal VoiceTransmitSink(VoiceNextConnection vnc, int pcmBufferDuration) { - this.Connection = vnc; - this.PcmBufferDuration = pcmBufferDuration; - this.PcmBuffer = new byte[vnc.AudioFormat.CalculateSampleSize(pcmBufferDuration)]; - this.PcmMemory = this.PcmBuffer.AsMemory(); - this.PcmBufferLength = 0; - this.WriteSemaphore = new SemaphoreSlim(1, 1); - this.Filters = new List(); + this._connection = vnc; + this._pcmBufferDuration = pcmBufferDuration; + this._pcmBuffer = new byte[vnc.AudioFormat.CalculateSampleSize(pcmBufferDuration)]; + this._pcmMemory = this._pcmBuffer.AsMemory(); + this._pcmBufferLength = 0; + this._writeSemaphore = new SemaphoreSlim(1, 1); + this._filters = new List(); } /// /// Writes PCM data to the sink. The data is prepared for transmission, and enqueued. /// /// PCM data buffer to send. /// Start of the data in the buffer. /// Number of bytes from the buffer. /// The token to monitor for cancellation requests. public async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken = default) => await this.WriteAsync(new ReadOnlyMemory(buffer, offset, count), cancellationToken).ConfigureAwait(false); /// /// Writes PCM data to the sink. The data is prepared for transmission, and enqueued. /// /// PCM data buffer to send. /// The token to monitor for cancellation requests. public async Task WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) { - await this.WriteSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); + await this._writeSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); try { var remaining = buffer.Length; var buffSpan = buffer; - var pcmSpan = this.PcmMemory; + var pcmSpan = this._pcmMemory; while (remaining > 0) { - var len = Math.Min(pcmSpan.Length - this.PcmBufferLength, remaining); + var len = Math.Min(pcmSpan.Length - this._pcmBufferLength, remaining); - var tgt = pcmSpan[this.PcmBufferLength..]; + var tgt = pcmSpan[this._pcmBufferLength..]; var src = buffSpan[..len]; src.CopyTo(tgt); - this.PcmBufferLength += len; + this._pcmBufferLength += len; remaining -= len; buffSpan = buffSpan[len..]; - if (this.PcmBufferLength == this.PcmBuffer.Length) + if (this._pcmBufferLength == this._pcmBuffer.Length) { this.ApplyFiltersSync(pcmSpan); - this.PcmBufferLength = 0; + this._pcmBufferLength = 0; - var packet = ArrayPool.Shared.Rent(this.PcmMemory.Length); - var packetMemory = packet.AsMemory()[..this.PcmMemory.Length]; - this.PcmMemory.CopyTo(packetMemory); + var packet = ArrayPool.Shared.Rent(this._pcmMemory.Length); + var packetMemory = packet.AsMemory()[..this._pcmMemory.Length]; + this._pcmMemory.CopyTo(packetMemory); - await this.Connection.EnqueuePacketAsync(new RawVoicePacket(packetMemory, this.PcmBufferDuration, false, packet), cancellationToken).ConfigureAwait(false); + await this._connection.EnqueuePacketAsync(new RawVoicePacket(packetMemory, this._pcmBufferDuration, false, packet), cancellationToken).ConfigureAwait(false); } } } finally { - this.WriteSemaphore.Release(); + this._writeSemaphore.Release(); } } /// /// Flushes the rest of the PCM data in this buffer to VoiceNext packet queue. /// /// The token to monitor for cancellation requests. public async Task FlushAsync(CancellationToken cancellationToken = default) { - var pcm = this.PcmMemory; - Helpers.ZeroFill(pcm[this.PcmBufferLength..].Span); + var pcm = this._pcmMemory; + Helpers.ZeroFill(pcm[this._pcmBufferLength..].Span); this.ApplyFiltersSync(pcm); var packet = ArrayPool.Shared.Rent(pcm.Length); var packetMemory = packet.AsMemory()[..pcm.Length]; pcm.CopyTo(packetMemory); - await this.Connection.EnqueuePacketAsync(new RawVoicePacket(packetMemory, this.PcmBufferDuration, false, packet), cancellationToken).ConfigureAwait(false); + await this._connection.EnqueuePacketAsync(new RawVoicePacket(packetMemory, this._pcmBufferDuration, false, packet), cancellationToken).ConfigureAwait(false); } /// /// Pauses playback. /// public void Pause() - => this.Connection.Pause(); + => this._connection.Pause(); /// /// Resumes playback. /// /// public async Task ResumeAsync() - => await this.Connection.ResumeAsync().ConfigureAwait(false); + => await this._connection.ResumeAsync().ConfigureAwait(false); /// /// Gets the collection of installed PCM filters, in order of their execution. /// /// Installed PCM filters, in order of execution. public IEnumerable GetInstalledFilters() { - foreach (var filter in this.Filters) + foreach (var filter in this._filters) yield return filter; } /// /// Installs a new PCM filter, with specified execution order. /// /// Filter to install. /// Order of the new filter. This determines where the filter will be inserted in the filter pipeline. public void InstallFilter(IVoiceFilter filter, int order = int.MaxValue) { if (filter == null) throw new ArgumentNullException(nameof(filter)); if (order < 0) throw new ArgumentOutOfRangeException(nameof(order), "Filter order must be greater than or equal to 0."); - lock (this.Filters) + lock (this._filters) { - var filters = this.Filters; + var filters = this._filters; if (order >= filters.Count) filters.Add(filter); else filters.Insert(order, filter); } } /// /// Uninstalls an installed PCM filter. /// /// Filter to uninstall. /// Whether the filter was uninstalled. public bool UninstallFilter(IVoiceFilter filter) { if (filter == null) throw new ArgumentNullException(nameof(filter)); - lock (this.Filters) + lock (this._filters) { - var filters = this.Filters; + var filters = this._filters; return filters.Contains(filter) && filters.Remove(filter); } } /// /// Applies the filters sync. /// /// The pcm span. private void ApplyFiltersSync(Memory pcmSpan) { var pcm16 = MemoryMarshal.Cast(pcmSpan.Span); // pass through any filters, if applicable - lock (this.Filters) + lock (this._filters) { - if (this.Filters.Any()) + if (this._filters.Any()) { - foreach (var filter in this.Filters) - filter.Transform(pcm16, this.Connection.AudioFormat, this.SampleDuration); + foreach (var filter in this._filters) + filter.Transform(pcm16, this._connection.AudioFormat, this.SampleDuration); } } if (this.VolumeModifier != 1) { // alter volume for (var i = 0; i < pcm16.Length; i++) pcm16[i] = (short)(pcm16[i] * this.VolumeModifier); } } /// /// Disposes . /// public void Dispose() - => this.WriteSemaphore?.Dispose(); + => this._writeSemaphore?.Dispose(); } } diff --git a/DisCatSharp/Clients/DiscordClient.WebSocket.cs b/DisCatSharp/Clients/DiscordClient.WebSocket.cs index 7caa8cc99..c92e35936 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 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(); /// /// Gets the session lock. /// - private ManualResetEventSlim SessionLock { get; } = new ManualResetEventSlim(true); + private readonly ManualResetEventSlim _sessionLock = new ManualResetEventSlim(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(); + 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(); + 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)) + if (this._sessionLock.Wait(0)) { - this.SessionLock.Reset(); + 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 b11ea7d50..811ffe86f 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; /// /// Gets the connection lock. /// - private ManualResetEventSlim ConnectionLock { get; } = new ManualResetEventSlim(true); + private readonly ManualResetEventSlim _connectionLock = new ManualResetEventSlim(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)) + if (!this._connectionLock.Wait(0)) throw new InvalidOperationException("This client is already connected."); - this.ConnectionLock.Set(); + 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); + FailConnection(this._connectionLock); throw new Exception("Authentication failed. Check your token and try again.", e); } catch (PlatformNotSupportedException) { - FailConnection(this.ConnectionLock); + FailConnection(this._connectionLock); throw; } catch (NotImplementedException) { - FailConnection(this.ConnectionLock); + 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(); + 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 574a45d63..2849e699f 100644 --- a/DisCatSharp/Clients/DiscordShardedClient.cs +++ b/DisCatSharp/Clients/DiscordShardedClient.cs @@ -1,745 +1,745 @@ // 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 DiscordConfiguration Configuration { get; } + 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._configuration = config; this.ShardClients = new ReadOnlyConcurrentDictionary(this._shards); - if (this.Configuration.LoggerFactory == null) + if (this._configuration.LoggerFactory == null) { - this.Configuration.LoggerFactory = new DefaultLoggerFactory(); - this.Configuration.LoggerFactory.AddProvider(new DefaultLoggerProvider(this.Configuration.MinimumLogLevel, this.Configuration.LogTimestampFormat)); + this._configuration.LoggerFactory = new DefaultLoggerFactory(); + this._configuration.LoggerFactory.AddProvider(new DefaultLoggerProvider(this._configuration.MinimumLogLevel, this._configuration.LogTimestampFormat)); } - this.Logger = this.Configuration.LoggerFactory.CreateLogger(); + 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) + 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 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) + 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 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("Authorization", Utilities.GetFormattedToken(this._configuration)); + if (this._configuration != null && this._configuration.Override != null) { - http.DefaultRequestHeaders.TryAddWithoutValidation("x-super-properties", this.Configuration.Override); + 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(); #endregion } } diff --git a/DisCatSharp/Entities/Application/DiscordApplication.cs b/DisCatSharp/Entities/Application/DiscordApplication.cs index 3a1fc63df..17042591f 100644 --- a/DisCatSharp/Entities/Application/DiscordApplication.cs +++ b/DisCatSharp/Entities/Application/DiscordApplication.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.Globalization; using System.Threading.Tasks; using DisCatSharp.Enums; using DisCatSharp.Net; using Newtonsoft.Json; namespace DisCatSharp.Entities { /// /// Represents an OAuth2 application. /// public sealed class DiscordApplication : DiscordMessageApplication, IEquatable { /// /// Gets the application's summary. /// public string Summary { get; internal set; } /// /// Gets the application's icon. /// public override string Icon => !string.IsNullOrWhiteSpace(this.IconHash) ? $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.APP_ICONS}/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.IconHash}.png?size=1024" : null; /// /// Gets the application's icon hash. /// public string IconHash { get; internal set; } /// /// Gets the application's allowed RPC origins. /// public IReadOnlyList RpcOrigins { get; internal set; } /// /// Gets the application's flags. /// public ApplicationFlags Flags { get; internal set; } /// /// Gets the application's owners. /// public IEnumerable Owners { get; internal set; } /// /// Gets whether this application's bot user requires code grant. /// public bool? RequiresCodeGrant { get; internal set; } /// /// Gets whether this bot application is public. /// public bool? IsPublic { get; internal set; } /// /// Gets the terms of service url of the application. /// public string TermsOfServiceUrl { get; internal set; } /// /// Gets the privacy policy url of the application. /// public string PrivacyPolicyUrl { get; internal set; } /// /// Gets the team name of the application. /// public string TeamName { get; internal set; } /// /// Gets the hash of the application's cover image. /// public string CoverImageHash { get; internal set; } /// /// Gets this application's cover image URL. /// public override string CoverImageUrl => $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.APP_ICONS}/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.CoverImageHash}.png?size=1024"; /// /// Gets the team which owns this application. /// public DiscordTeam Team { get; internal set; } /// /// Gets the hex encoded key for verification in interactions and the GameSDK's GetTicket /// public string VerifyKey { get; internal set; } /// /// If this application is a game sold on Discord, this field will be the guild to which it has been linked /// public ulong? GuildId { get; internal set; } /// /// If this application is a game sold on Discord, this field will be the id of the "Game SKU" that is created, if exists /// public ulong? PrimarySkuId { get; internal set; } /// /// If this application is a game sold on Discord, this field will be the URL slug that links to the store page /// public string Slug { get; internal set; } /// /// Gets or sets a list of . /// - private IReadOnlyList Assets { get; set; } + private IReadOnlyList _assets; /// /// A custom url for the Add To Server button. /// public string CustomInstallUrl { get; internal set; } /// /// Install parameters for adding the application to a guild. /// public DiscordApplicationInstallParams InstallParams { get; internal set; } /// /// The application tags. /// Not used atm. /// public IReadOnlyList Tags { get; internal set; } /// /// Initializes a new instance of the class. /// internal DiscordApplication() { } /// /// Gets the application's cover image URL, in requested format and size. /// /// Format of the image to get. /// Maximum size of the cover image. Must be a power of two, minimum 16, maximum 2048. /// URL of the application's cover image. 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.Auto or ImageFormat.Png => "png", ImageFormat.WebP => "webp", _ => throw new ArgumentOutOfRangeException(nameof(fmt)), }; var ssize = size.ToString(CultureInfo.InvariantCulture); return !string.IsNullOrWhiteSpace(this.CoverImageHash) ? $"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.AVATARS}/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.IconHash}.{sfmt}?size={ssize}" : null; } /// /// Retrieves this application's assets. /// /// This application's assets. public async Task> GetAssetsAsync() { - if (this.Assets == null) - this.Assets = await this.Discord.ApiClient.GetApplicationAssetsAsync(this).ConfigureAwait(false); + if (this._assets == null) + this._assets = await this.Discord.ApiClient.GetApplicationAssetsAsync(this).ConfigureAwait(false); - return this.Assets; + return this._assets; } /// /// Generates an oauth url for the application. /// /// The permissions. /// OAuth Url public string GenerateBotOAuth(Permissions permissions = Permissions.None) { permissions &= PermissionMethods.FullPerms; // hey look, it's not all annoying and blue :P return new QueryUriBuilder($"{DiscordDomain.GetDomain(CoreDomain.Discord).Url}{Endpoints.OAUTH2}{Endpoints.AUTHORIZE}") .AddParameter("client_id", this.Id.ToString(CultureInfo.InvariantCulture)) .AddParameter("scope", "bot") .AddParameter("permissions", ((long)permissions).ToString(CultureInfo.InvariantCulture)) .ToString(); } /// /// 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 DiscordApplication); /// /// Checks whether this is equal to another . /// /// to compare to. /// Whether the is equal to this . public bool Equals(DiscordApplication 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 application to compare. /// Second application to compare. /// Whether the two applications are equal. public static bool operator ==(DiscordApplication e1, DiscordApplication 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 application to compare. /// Second application to compare. /// Whether the two applications are not equal. public static bool operator !=(DiscordApplication e1, DiscordApplication e2) => !(e1 == e2); } /// /// Represents an discord asset. /// public abstract class DiscordAsset { /// /// Gets the ID of this asset. /// public virtual string Id { get; set; } /// /// Gets the URL of this asset. /// public abstract Uri Url { get; } } /// /// Represents an asset for an OAuth2 application. /// public sealed class DiscordApplicationAsset : DiscordAsset, IEquatable { /// /// Gets the Discord client instance for this asset. /// internal BaseDiscordClient Discord { get; set; } /// /// Gets the asset's name. /// [JsonProperty("name")] public string Name { get; internal set; } /// /// Gets the asset's type. /// [JsonProperty("type")] public ApplicationAssetType Type { get; internal set; } /// /// Gets the application this asset belongs to. /// public DiscordApplication Application { get; internal set; } /// /// Gets the Url of this asset. /// public override Uri Url => new($"{DiscordDomain.GetDomain(CoreDomain.DiscordCdn).Url}{Endpoints.APP_ASSETS}/{this.Application.Id.ToString(CultureInfo.InvariantCulture)}/{this.Id}.png"); /// /// Initializes a new instance of the class. /// internal DiscordApplicationAsset() { } /// /// Initializes a new instance of the class. /// /// The app. internal DiscordApplicationAsset(DiscordApplication app) { this.Discord = app.Discord; } /// /// 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 DiscordApplicationAsset); /// /// Checks whether this is equal to another . /// /// to compare to. /// Whether the is equal to this . public bool Equals(DiscordApplicationAsset 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 application asset to compare. /// Second application asset to compare. /// Whether the two application assets not equal. public static bool operator ==(DiscordApplicationAsset e1, DiscordApplicationAsset 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 application asset to compare. /// Second application asset to compare. /// Whether the two application assets are not equal. public static bool operator !=(DiscordApplicationAsset e1, DiscordApplicationAsset e2) => !(e1 == e2); } /// /// Represents an spotify asset. /// public sealed class DiscordSpotifyAsset : DiscordAsset { /// /// Gets the URL of this asset. /// public override Uri Url => this._url.Value; private readonly Lazy _url; /// /// Initializes a new instance of the class. /// public DiscordSpotifyAsset() { this._url = new Lazy(() => { var ids = this.Id.Split(':'); var id = ids[1]; return new Uri($"https://i.scdn.co/image/{id}"); }); } } /// /// Determines the type of the asset attached to the application. /// public enum ApplicationAssetType : int { /// /// Unknown type. This indicates something went terribly wrong. /// Unknown = 0, /// /// This asset can be used as small image for rich presences. /// SmallImage = 1, /// /// This asset can be used as large image for rich presences. /// LargeImage = 2 } } diff --git a/DisCatSharp/Entities/Message/DiscordMessage.cs b/DisCatSharp/Entities/Message/DiscordMessage.cs index ca659b4ae..cc67e59dc 100644 --- a/DisCatSharp/Entities/Message/DiscordMessage.cs +++ b/DisCatSharp/Entities/Message/DiscordMessage.cs @@ -1,888 +1,888 @@ // 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.InternalThread.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 InternalThread + 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")}."); } /// /// 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/Logging/CompositeDefaultLogger.cs b/DisCatSharp/Logging/CompositeDefaultLogger.cs index 454d73fe1..f14fe5d88 100644 --- a/DisCatSharp/Logging/CompositeDefaultLogger.cs +++ b/DisCatSharp/Logging/CompositeDefaultLogger.cs @@ -1,78 +1,78 @@ // 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 Microsoft.Extensions.Logging; namespace DisCatSharp { /// /// Represents a composite default logger. /// internal class CompositeDefaultLogger : ILogger { /// /// Gets the loggers. /// - private IEnumerable> Loggers { get; } + private readonly IEnumerable> _loggers; /// /// Initializes a new instance of the class. /// /// The providers. public CompositeDefaultLogger(IEnumerable providers) { - this.Loggers = providers.Select(x => x.CreateLogger(typeof(BaseDiscordClient).FullName)) + this._loggers = providers.Select(x => x.CreateLogger(typeof(BaseDiscordClient).FullName)) .OfType>() .ToList(); } /// /// Whether the logger is enabled. /// /// The log level. public bool IsEnabled(LogLevel logLevel) => true; /// /// Logs an event. /// /// The log level. /// The event id. /// The state. /// The exception. /// The formatter. public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) { - foreach (var logger in this.Loggers) + foreach (var logger in this._loggers) logger.Log(logLevel, eventId, state, exception, formatter); } /// /// Begins the scope. /// /// The state. public IDisposable BeginScope(TState state) => throw new NotImplementedException(); } } diff --git a/DisCatSharp/Logging/DefaultLogger.cs b/DisCatSharp/Logging/DefaultLogger.cs index 739f63e3b..0994f996b 100644 --- a/DisCatSharp/Logging/DefaultLogger.cs +++ b/DisCatSharp/Logging/DefaultLogger.cs @@ -1,147 +1,148 @@ // 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. /// public class DefaultLogger : ILogger { private static readonly object _lock = new(); /// /// Gets the minimum log level. /// - private LogLevel MinimumLevel { get; } + private readonly LogLevel _minimumLevel; + /// /// Gets the timestamp format. /// - private string TimestampFormat { get; } + private readonly string _timestampFormat; /// /// Initializes a new instance of the class. /// /// The client. internal DefaultLogger(BaseDiscordClient client) : this(client.Configuration.MinimumLogLevel, client.Configuration.LogTimestampFormat) { } /// /// Initializes a new instance of the class. /// /// The min level. /// The timestamp format. internal DefaultLogger(LogLevel minLevel = LogLevel.Information, string timestampFormat = "yyyy-MM-dd HH:mm:ss zzz") { - this.MinimumLevel = minLevel; - this.TimestampFormat = timestampFormat; + this._minimumLevel = minLevel; + this._timestampFormat = timestampFormat; } /// /// Logs an event. /// /// The log level. /// The event id. /// The state. /// The exception. /// The formatter. public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) { if (!this.IsEnabled(logLevel)) return; lock (_lock) { var ename = eventId.Name; ename = ename?.Length > 12 ? ename?[..12] : ename; - Console.Write($"[{DateTimeOffset.Now.ToString(this.TimestampFormat)}] [{eventId.Id,-4}/{ename,-12}] "); + Console.Write($"[{DateTimeOffset.Now.ToString(this._timestampFormat)}] [{eventId.Id,-4}/{ename,-12}] "); switch (logLevel) { case LogLevel.Trace: Console.ForegroundColor = ConsoleColor.Gray; break; case LogLevel.Debug: Console.ForegroundColor = ConsoleColor.DarkMagenta; break; case LogLevel.Information: Console.ForegroundColor = ConsoleColor.DarkCyan; break; case LogLevel.Warning: Console.ForegroundColor = ConsoleColor.Yellow; break; case LogLevel.Error: Console.ForegroundColor = ConsoleColor.Red; break; case LogLevel.Critical: Console.BackgroundColor = ConsoleColor.Red; Console.ForegroundColor = ConsoleColor.Black; break; } Console.Write(logLevel switch { LogLevel.Trace => "[Trace] ", LogLevel.Debug => "[Debug] ", LogLevel.Information => "[Info ] ", LogLevel.Warning => "[Warn ] ", LogLevel.Error => "[Error] ", LogLevel.Critical => "[Crit ]", LogLevel.None => "[None ] ", _ => "[?????] " }); Console.ResetColor(); //The foreground color is off. if (logLevel == LogLevel.Critical) Console.Write(" "); var message = formatter(state, exception); Console.WriteLine(message); if (exception != null) Console.WriteLine(exception); } } /// /// Whether the logger is enabled. /// /// The log level. public bool IsEnabled(LogLevel logLevel) - => logLevel >= this.MinimumLevel; + => logLevel >= this._minimumLevel; /// /// Begins the scope. /// /// The state. /// An IDisposable. public IDisposable BeginScope(TState state) => throw new NotImplementedException(); } } diff --git a/DisCatSharp/Logging/DefaultLoggerFactory.cs b/DisCatSharp/Logging/DefaultLoggerFactory.cs index e95aae839..2151caf60 100644 --- a/DisCatSharp/Logging/DefaultLoggerFactory.cs +++ b/DisCatSharp/Logging/DefaultLoggerFactory.cs @@ -1,74 +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 System; using System.Collections.Generic; using Microsoft.Extensions.Logging; namespace DisCatSharp { /// /// Represents a default logger factory. /// internal class DefaultLoggerFactory : ILoggerFactory { /// /// Gets the providers. /// - private List Providers { get; } = new List(); + private readonly List _providers = new List(); private bool _isDisposed = false; /// /// Adds a provider. /// /// The provider to be added. - public void AddProvider(ILoggerProvider provider) => this.Providers.Add(provider); + public void AddProvider(ILoggerProvider provider) => this._providers.Add(provider); /// /// Creates the logger. /// /// The category name. public ILogger CreateLogger(string categoryName) { return 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); + : new CompositeDefaultLogger(this._providers); } /// /// Disposes the logger. /// public void Dispose() { if (this._isDisposed) return; this._isDisposed = true; - foreach (var provider in this.Providers) + foreach (var provider in this._providers) provider.Dispose(); - this.Providers.Clear(); + this._providers.Clear(); } } } diff --git a/DisCatSharp/Logging/DefaultLoggerProvider.cs b/DisCatSharp/Logging/DefaultLoggerProvider.cs index f3b8b629a..94ccb120d 100644 --- a/DisCatSharp/Logging/DefaultLoggerProvider.cs +++ b/DisCatSharp/Logging/DefaultLoggerProvider.cs @@ -1,89 +1,90 @@ // 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 LogLevel MinimumLevel { get; } + private readonly LogLevel _minimumLevel; + /// /// Gets the timestamp format. /// - private string TimestampFormat { get; } + private readonly string _timestampFormat; private bool _isDisposed = false; /// /// 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; + this._minimumLevel = minLevel; + this._timestampFormat = timestampFormat; } /// /// Creates the logger. /// /// The category name. public ILogger CreateLogger(string categoryName) { return 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); + : new DefaultLogger(this._minimumLevel, this._timestampFormat); } /// /// Disposes the logger. /// public void Dispose() => this._isDisposed = true; } } diff --git a/DisCatSharp/Logging/ShardedLoggerFactory.cs b/DisCatSharp/Logging/ShardedLoggerFactory.cs index 46242ec44..162d402c8 100644 --- a/DisCatSharp/Logging/ShardedLoggerFactory.cs +++ b/DisCatSharp/Logging/ShardedLoggerFactory.cs @@ -1,70 +1,70 @@ // 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 ILogger Logger { get; } + private readonly ILogger _logger; /// /// Initializes a new instance of the class. /// /// The instance. public ShardedLoggerFactory(ILogger instance) { - this.Logger = 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 ? throw new ArgumentException($"This factory can only provide instances of loggers for {typeof(BaseDiscordClient).FullName}.", nameof(categoryName)) - : this.Logger; + : this._logger; } /// /// Disposes the logger. /// public void Dispose() { } } } diff --git a/DisCatSharp/Net/Rest/RestClient.cs b/DisCatSharp/Net/Rest/RestClient.cs index 47ea711b9..5a30e885b 100644 --- a/DisCatSharp/Net/Rest/RestClient.cs +++ b/DisCatSharp/Net/Rest/RestClient.cs @@ -1,860 +1,860 @@ // 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_]+)"); /// /// Gets the http client. /// internal HttpClient HttpClient { get; } /// /// Gets the discord client. /// - private BaseDiscordClient Discord { get; } + private readonly BaseDiscordClient _discord; /// /// Gets a value indicating whether debug is enabled. /// internal bool Debug { get; set; } /// /// Gets the logger. /// - private ILogger Logger { get; } + private readonly ILogger _logger; /// /// Gets the routes to hashes. /// - private ConcurrentDictionary RoutesToHashes { get; } + private readonly ConcurrentDictionary _routesToHashes; /// /// Gets the hashes to buckets. /// - private ConcurrentDictionary HashesToBuckets { get; } + private readonly ConcurrentDictionary _hashesToBuckets; /// /// Gets the request queue. /// - private ConcurrentDictionary RequestQueue { get; } + private readonly ConcurrentDictionary _requestQueue; /// /// Gets the global rate limit event. /// - private AsyncManualResetEvent GlobalRateLimitEvent { get; } + private readonly AsyncManualResetEvent _globalRateLimitEvent; /// /// Gets a value indicating whether use reset after. /// - private bool UseResetAfter { get; } + 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._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; + 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)), + 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) + 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.HttpClient.DefaultRequestHeaders.TryAddWithoutValidation("x-super-properties", this._discord.Configuration.Override); } - this.RoutesToHashes = new ConcurrentDictionary(); - this.HashesToBuckets = new ConcurrentDictionary(); - this.RequestQueue = new ConcurrentDictionary(); + this._routesToHashes = new ConcurrentDictionary(); + this._hashesToBuckets = new ConcurrentDictionary(); + this._requestQueue = new ConcurrentDictionary(); - this.GlobalRateLimitEvent = new AsyncManualResetEvent(true); - this.UseResetAfter = useRelativeRatelimit; + 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)); + 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)); + 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); + _ = this._requestQueue.TryGetValue(bucketId, out var count); // Increment by one atomically due to concurrency - this.RequestQueue[bucketId] = Interlocked.Increment(ref count); + 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."); + 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); + 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()); + this._logger.LogDebug(LoggerEvents.RatelimitDiag, "Request for {0} is blocked", bucket.ToString()); var delay = bucket.Reset - now; var resetDate = bucket.Reset; - if (this.UseResetAfter) + 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"); + 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); + 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"); + .LogTaskFault(this._logger, LogLevel.Error, LoggerEvents.RestError, "Error while executing request"); return; } - this.Logger.LogDebug(LoggerEvents.RatelimitDiag, "Request for {0} is allowed", bucket.ToString()); + 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()); + 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()); + 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); + 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); + 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"); + this._logger.LogError(LoggerEvents.RatelimitHit, "Global ratelimit hit, cooling down"); try { - this.GlobalRateLimitEvent.Reset(); + 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._globalRateLimitEvent.SetAsync(); } this.ExecuteRequestAsync(request, bucket, ratelimitTcs) - .LogTaskFault(this.Logger, LogLevel.Error, LoggerEvents.RestError, "Error while retrying request"); + .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); + 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"); + .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); + 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); + _ = 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)) + if (this._requestQueue.ContainsKey(r)) { - _ = this.RequestQueue.TryRemove(r, out _); + _ = 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); + 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, ""); + 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, ""); + 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) + 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)) + 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 _); + _ = 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); + 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) => + _ = 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); + _ = 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) + foreach (var key in this._requestQueue.Keys) { - var bucket = this.HashesToBuckets.Values.FirstOrDefault(x => x.RouteHashes.Contains(key)); + 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 _); + _ = this._requestQueue.TryRemove(key, out _); } var removedBuckets = 0; StringBuilder bucketIdStrBuilder = default; - foreach (var kvp in this.HashesToBuckets) + 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) + if (this._requestQueue.ContainsKey(value.BucketId) && !value.IsUnlimited) continue; - var resetOffset = this.UseResetAfter ? value.ResetAfterOffset : value.Reset; + 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 _); + _ = 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(',', ' ')); + 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) + 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."); + this._logger.LogDebug(LoggerEvents.RestCleaner, "Bucket cleaner task stopped."); } ~RestClient() => this.Dispose(); /// /// Disposes the rest client. /// public void Dispose() { if (this._disposed) return; this._disposed = true; - this.GlobalRateLimitEvent.Reset(); + this._globalRateLimitEvent.Reset(); if (this._bucketCleanerTokenSource?.IsCancellationRequested == false) { this._bucketCleanerTokenSource?.Cancel(); - this.Logger.LogDebug(LoggerEvents.RestCleaner, "Bucket cleaner task stopped."); + 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(); + this._routesToHashes.Clear(); + this._hashesToBuckets.Clear(); + this._requestQueue.Clear(); } } } diff --git a/DisCatSharp/Net/Udp/DCSUdpClient.cs b/DisCatSharp/Net/Udp/DCSUdpClient.cs index a7b4ca946..9a3983f42 100644 --- a/DisCatSharp/Net/Udp/DCSUdpClient.cs +++ b/DisCatSharp/Net/Udp/DCSUdpClient.cs @@ -1,141 +1,142 @@ // 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.Net.Sockets; using System.Threading; using System.Threading.Tasks; namespace DisCatSharp.Net.Udp { /// /// The default, native-based UDP client implementation. /// internal class DcsUdpClient : BaseUdpClient { /// /// Gets the client. /// - private UdpClient Client { get; set; } + private UdpClient _client; /// /// Gets the end point. /// - private ConnectionEndpoint EndPoint { get; set; } + private ConnectionEndpoint _endPoint; /// /// Gets the packet queue. /// - private BlockingCollection PacketQueue { get; } + private readonly BlockingCollection _packetQueue; /// /// Gets the receiver task. /// - [System.Diagnostics.CodeAnalysis.SuppressMessage("CodeQuality", "IDE0052:Remove unread private members", Justification = "")] - private Task ReceiverTask { get; set; } + [System.Diagnostics.CodeAnalysis.SuppressMessage("CodeQuality", "IDE0052:Remove unread private members", + Justification = "")] + private Task _receiverTask; /// /// Gets the cancellation token source. /// - private CancellationTokenSource TokenSource { get; } + private readonly CancellationTokenSource _tokenSource; /// /// Gets the cancellation token. /// - private CancellationToken Token => this.TokenSource.Token; + private CancellationToken TOKEN => this._tokenSource.Token; /// /// Creates a new UDP client instance. /// public DcsUdpClient() { - this.PacketQueue = new BlockingCollection(); - this.TokenSource = new CancellationTokenSource(); + this._packetQueue = new BlockingCollection(); + this._tokenSource = new CancellationTokenSource(); } /// /// Configures the UDP client. /// /// Endpoint that the client will be communicating with. public override void Setup(ConnectionEndpoint endpoint) { - this.EndPoint = endpoint; - this.Client = new UdpClient(); - this.ReceiverTask = Task.Run(this.ReceiverLoopAsync, this.Token); + this._endPoint = endpoint; + this._client = new UdpClient(); + this._receiverTask = Task.Run(this.ReceiverLoopAsync, this.TOKEN); } /// /// Sends a datagram. /// /// Datagram. /// Length of the datagram. /// public override Task SendAsync(byte[] data, int dataLength) - => this.Client.SendAsync(data, dataLength, this.EndPoint.Hostname, this.EndPoint.Port); + => this._client.SendAsync(data, dataLength, this._endPoint.Hostname, this._endPoint.Port); /// /// Receives a datagram. /// /// The received bytes. - public override Task ReceiveAsync() => Task.FromResult(this.PacketQueue.Take(this.Token)); + public override Task ReceiveAsync() => Task.FromResult(this._packetQueue.Take(this.TOKEN)); /// /// Closes and disposes the client. /// public override void Close() { - this.TokenSource.Cancel(); + this._tokenSource.Cancel(); #if !NETSTANDARD1_3 try - { this.Client.Close(); } + { this._client.Close(); } catch (Exception) { } #endif // dequeue all the packets - this.PacketQueue.Dispose(); + this._packetQueue.Dispose(); } /// /// Receivers the loop. /// private async Task ReceiverLoopAsync() { - while (!this.Token.IsCancellationRequested) + while (!this.TOKEN.IsCancellationRequested) { try { - var packet = await this.Client.ReceiveAsync().ConfigureAwait(false); - this.PacketQueue.Add(packet.Buffer); + var packet = await this._client.ReceiveAsync().ConfigureAwait(false); + this._packetQueue.Add(packet.Buffer); } catch (Exception) { } } } /// /// Creates a new instance of . /// public static BaseUdpClient CreateNew() => new DcsUdpClient(); } } diff --git a/DisCatSharp/Net/WebSocket/PayloadDecompressor.cs b/DisCatSharp/Net/WebSocket/PayloadDecompressor.cs index b0a052422..934062272 100644 --- a/DisCatSharp/Net/WebSocket/PayloadDecompressor.cs +++ b/DisCatSharp/Net/WebSocket/PayloadDecompressor.cs @@ -1,129 +1,129 @@ // 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; using System.IO; using System.IO.Compression; namespace DisCatSharp.Net.WebSocket { /// /// Represents a payload decompressor. /// internal sealed class PayloadDecompressor : IDisposable { /// /// The zlib flush. /// private const uint ZLIB_FLUSH = 0x0000FFFF; /// /// The zlib prefix. /// private const byte ZLIB_PREFIX = 0x78; /// /// Gets the compression level. /// public GatewayCompressionLevel CompressionLevel { get; } /// /// Gets the compressed stream. /// - private MemoryStream CompressedStream { get; } + private readonly MemoryStream _compressedStream; /// /// Gets the decompressor stream. /// - private DeflateStream DecompressorStream { get; } + private readonly DeflateStream _decompressorStream; /// /// Initializes a new instance of the class. /// /// The compression level. public PayloadDecompressor(GatewayCompressionLevel compressionLevel) { if (compressionLevel == GatewayCompressionLevel.None) throw new InvalidOperationException("Decompressor requires a valid compression mode."); this.CompressionLevel = compressionLevel; - this.CompressedStream = new MemoryStream(); + this._compressedStream = new MemoryStream(); if (this.CompressionLevel == GatewayCompressionLevel.Stream) - this.DecompressorStream = new DeflateStream(this.CompressedStream, CompressionMode.Decompress); + this._decompressorStream = new DeflateStream(this._compressedStream, CompressionMode.Decompress); } /// /// Tries the decompress. /// /// The compressed bytes. /// The decompressed memory stream. public bool TryDecompress(ArraySegment compressed, MemoryStream decompressed) { var zlib = this.CompressionLevel == GatewayCompressionLevel.Stream - ? this.DecompressorStream - : new DeflateStream(this.CompressedStream, CompressionMode.Decompress, true); + ? this._decompressorStream + : new DeflateStream(this._compressedStream, CompressionMode.Decompress, true); if (compressed.Array[0] == ZLIB_PREFIX) - this.CompressedStream.Write(compressed.Array, compressed.Offset + 2, compressed.Count - 2); + this._compressedStream.Write(compressed.Array, compressed.Offset + 2, compressed.Count - 2); else - this.CompressedStream.Write(compressed.Array, compressed.Offset, compressed.Count); + this._compressedStream.Write(compressed.Array, compressed.Offset, compressed.Count); - this.CompressedStream.Flush(); - this.CompressedStream.Position = 0; + this._compressedStream.Flush(); + this._compressedStream.Position = 0; var cspan = compressed.AsSpan(); var suffix = BinaryPrimitives.ReadUInt32BigEndian(cspan[^4..]); if (this.CompressionLevel == GatewayCompressionLevel.Stream && suffix != ZLIB_FLUSH) { if (this.CompressionLevel == GatewayCompressionLevel.Payload) zlib.Dispose(); return false; } try { zlib.CopyTo(decompressed); return true; } catch { return false; } finally { - this.CompressedStream.Position = 0; - this.CompressedStream.SetLength(0); + this._compressedStream.Position = 0; + this._compressedStream.SetLength(0); if (this.CompressionLevel == GatewayCompressionLevel.Payload) zlib.Dispose(); } } /// /// Disposes the decompressor. /// public void Dispose() { - this.DecompressorStream?.Dispose(); - this.CompressedStream.Dispose(); + this._decompressorStream?.Dispose(); + this._compressedStream.Dispose(); } } } diff --git a/DisCatSharp/Net/WebSocket/SocketLock.cs b/DisCatSharp/Net/WebSocket/SocketLock.cs index 857cbae0f..5ea4ba902 100644 --- a/DisCatSharp/Net/WebSocket/SocketLock.cs +++ b/DisCatSharp/Net/WebSocket/SocketLock.cs @@ -1,139 +1,139 @@ // 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.Net.WebSocket { // Licensed from Clyde.NET /// /// Represents a socket lock. /// internal sealed class SocketLock : IDisposable { /// /// Gets the application id. /// public ulong ApplicationId { get; } /// /// Gets the lock semaphore. /// - private SemaphoreSlim LockSemaphore { get; } + private readonly SemaphoreSlim _lockSemaphore; /// /// Gets or sets the timeout cancel source. /// - private CancellationTokenSource TimeoutCancelSource { get; set; } + private CancellationTokenSource _timeoutCancelSource; /// /// Gets the cancel token. /// - private CancellationToken TimeoutCancel => this.TimeoutCancelSource.Token; + private CancellationToken TIMEOUT_CANCEL => this._timeoutCancelSource.Token; /// /// Gets or sets the unlock task. /// - private Task UnlockTask { get; set; } + private Task _unlockTask; /// /// Gets or sets the max concurrency. /// - private int MaxConcurrency { get; set; } + private readonly int _maxConcurrency; /// /// Initializes a new instance of the class. /// /// The app id. /// The max concurrency. public SocketLock(ulong appId, int maxConcurrency) { this.ApplicationId = appId; - this.TimeoutCancelSource = null; - this.MaxConcurrency = maxConcurrency; - this.LockSemaphore = new SemaphoreSlim(maxConcurrency); + this._timeoutCancelSource = null; + this._maxConcurrency = maxConcurrency; + this._lockSemaphore = new SemaphoreSlim(maxConcurrency); } /// /// Locks the socket. /// public async Task LockAsync() { - await this.LockSemaphore.WaitAsync().ConfigureAwait(false); + await this._lockSemaphore.WaitAsync().ConfigureAwait(false); - this.TimeoutCancelSource = new CancellationTokenSource(); - this.UnlockTask = Task.Delay(TimeSpan.FromSeconds(30), this.TimeoutCancel); - _ = this.UnlockTask.ContinueWith(this.InternalUnlock, TaskContinuationOptions.NotOnCanceled); + this._timeoutCancelSource = new CancellationTokenSource(); + this._unlockTask = Task.Delay(TimeSpan.FromSeconds(30), this.TIMEOUT_CANCEL); + _ = this._unlockTask.ContinueWith(this.InternalUnlock, TaskContinuationOptions.NotOnCanceled); } /// /// Unlocks the socket after a given timespan. /// /// The unlock delay. public void UnlockAfter(TimeSpan unlockDelay) { - if (this.TimeoutCancelSource == null || this.LockSemaphore.CurrentCount > 0) + if (this._timeoutCancelSource == null || this._lockSemaphore.CurrentCount > 0) return; // it's not unlockable because it's post-IDENTIFY or not locked try { - this.TimeoutCancelSource.Cancel(); - this.TimeoutCancelSource.Dispose(); + this._timeoutCancelSource.Cancel(); + this._timeoutCancelSource.Dispose(); } catch { } - this.TimeoutCancelSource = null; + this._timeoutCancelSource = null; - this.UnlockTask = Task.Delay(unlockDelay, CancellationToken.None); - _ = this.UnlockTask.ContinueWith(this.InternalUnlock); + this._unlockTask = Task.Delay(unlockDelay, CancellationToken.None); + _ = this._unlockTask.ContinueWith(this.InternalUnlock); } /// /// Waits for the socket lock. /// /// A Task. public Task WaitAsync() - => this.LockSemaphore.WaitAsync(); + => this._lockSemaphore.WaitAsync(); /// /// Disposes the socket lock. /// public void Dispose() { try { - this.TimeoutCancelSource?.Cancel(); - this.TimeoutCancelSource?.Dispose(); + this._timeoutCancelSource?.Cancel(); + this._timeoutCancelSource?.Dispose(); } catch { } } /// /// Unlocks the socket. /// /// The task. private void InternalUnlock(Task t) - => this.LockSemaphore.Release(this.MaxConcurrency); + => this._lockSemaphore.Release(this._maxConcurrency); } }