diff --git a/.editorconfig b/.editorconfig index d82e66309b..6c5221951f 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,240 +1,240 @@ -############################### -# Core EditorConfig Options # -############################### -root = true - -[obsolete/*] -generated_code = true - -# All files -[*] -tab_width = 4 -end_of_line = crlf -indent_style = space - -# XML project files -[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] -indent_size = 2 - -# XML config files -[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}] -indent_size = 2 - -############################### -# .NET Coding Conventions # -############################### -[*.{cs,vb}] - -# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1826#exclude-firstordefault-and-lastordefault-methods -dotnet_code_quality.CA1826.exclude_ordefault_methods = true - -# Collection Expressions - int[] numbers = [] -dotnet_style_collection_initializer = true:error -dotnet_style_prefer_collection_expression = true:error - -# Nullable reference types -dotnet_diagnostic.CS8600.severity = suggestion -dotnet_diagnostic.CS8601.severity = suggestion -dotnet_diagnostic.CS8602.severity = suggestion -dotnet_diagnostic.CS8603.severity = suggestion -dotnet_diagnostic.CS8604.severity = suggestion -dotnet_diagnostic.CS8618.severity = suggestion -dotnet_diagnostic.CS8619.severity = suggestion -dotnet_diagnostic.CS8620.severity = suggestion -dotnet_diagnostic.CS8625.severity = suggestion -dotnet_diagnostic.CS8629.severity = suggestion -dotnet_diagnostic.CS8632.severity = suggestion -dotnet_diagnostic.CS8764.severity = suggestion -dotnet_diagnostic.CS8765.severity = suggestion -dotnet_diagnostic.CS8767.severity = suggestion - -dotnet_diagnostic.IDE0021.severity = error -dotnet_diagnostic.IDE0022.severity = error -dotnet_diagnostic.IDE0023.severity = error -dotnet_diagnostic.IDE0024.severity = error -dotnet_diagnostic.IDE0025.severity = error -dotnet_diagnostic.IDE0026.severity = error -dotnet_diagnostic.IDE0027.severity = error - -# Suppress CS1591: Missing XML comment for publicly visible type or member -dotnet_diagnostic.CS1591.severity = suggestion - -# Suppress IDE0058: Expression value is never used -dotnet_diagnostic.IDE0058.severity = none - -# Organize usings -dotnet_sort_system_directives_first = true:error -dotnet_separate_import_directive_groups = true:error - -# Unfortunately, no java-style usings -dotnet_diagnostic.IDE0064.severity = none - -# Using directive is unnecessary. -dotnet_diagnostic.IDE0005.severity = error - -# this. preferences -dotnet_style_qualification_for_field = true:error -dotnet_style_qualification_for_property = true:error -dotnet_style_qualification_for_method = false:error -dotnet_style_qualification_for_event = true:error - -# Language keywords vs BCL types preferences -dotnet_style_predefined_type_for_locals_parameters_members = true:error -dotnet_style_predefined_type_for_member_access = true:error - -# Parentheses preferences -dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:error -dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:error -dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:error -dotnet_style_parentheses_in_other_operators = never_if_unnecessary:error - -# Modifier preferences -dotnet_style_require_accessibility_modifiers = always:error -dotnet_style_readonly_field = true:error - -# Expression-level preferences -dotnet_style_object_initializer = true:error -dotnet_style_collection_initializer = true:error -dotnet_style_explicit_tuple_names = true:error -dotnet_style_null_propagation = true:error -dotnet_style_coalesce_expression = true:error -dotnet_style_prefer_is_null_check_over_reference_equality_method = true:error -dotnet_style_prefer_inferred_tuple_names = true:error -dotnet_style_prefer_inferred_anonymous_type_member_names = true:error -dotnet_style_prefer_auto_properties = true:error -dotnet_style_prefer_conditional_expression_over_assignment = true:error -dotnet_style_prefer_conditional_expression_over_return = true:error - -# Don't force namespaces to match their folder names (DSharpPlus.Entities) -dotnet_diagnostic.IDE0130.severity = none -dotnet_style_namespace_match_folder = false - -dotnet_style_prefer_simplified_boolean_expressions = true:error -dotnet_style_operator_placement_when_wrapping = beginning_of_line:error - -############################### -# Naming Conventions # -############################### - -# Style Definitions -dotnet_naming_style.pascal_case_style.capitalization = pascal_case - -# Use PascalCase for constant fields -dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = error -dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields -dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style -dotnet_naming_symbols.constant_fields.applicable_kinds = field -dotnet_naming_symbols.constant_fields.applicable_accessibilities = * -dotnet_naming_symbols.constant_fields.required_modifiers = const - -# Interfaces should start with I -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_style.begins_with_i.required_prefix = I -dotnet_naming_style.begins_with_i.capitalization = pascal_case -dotnet_naming_symbols.interface.applicable_kinds = interface - -# Async Methods should end with Async -dotnet_naming_rule.async_methods_should_be_async_suffix.severity = error -dotnet_naming_rule.async_methods_should_be_async_suffix.symbols = async_methods -dotnet_naming_rule.async_methods_should_be_async_suffix.style = async_suffix -dotnet_naming_style.async_suffix.required_suffix = Async -dotnet_naming_style.async_suffix.capitalization = pascal_case -dotnet_naming_symbols.async_methods.applicable_kinds = method -dotnet_naming_symbols.async_methods.required_modifiers = async - -# Fields should not begin with underscores, should be camelCase and should not use word separators -dotnet_naming_rule.all_fields_notunderscored.symbols = all_fields -dotnet_naming_rule.all_fields_notunderscored.style = notunderscored -dotnet_naming_rule.all_fields_notunderscored.severity = error -dotnet_naming_style.notunderscored.capitalization = camel_case -dotnet_naming_style.notunderscored.required_prefix = -dotnet_naming_style.notunderscored.word_separator = -dotnet_naming_symbols.all_fields.applicable_kinds = field -dotnet_naming_symbols.all_fields.applicable_accessibilities = * - -############################### -# C# Coding Conventions # -############################### -[*.cs] - -# File-scoped namespaces -csharp_style_namespace_declarations = file_scoped:error - -# var preferences -csharp_style_var_for_built_in_types = false:error -csharp_style_var_when_type_is_apparent = false:error -csharp_style_var_elsewhere = false:error - -# Pattern matching preferences -csharp_style_pattern_matching_over_is_with_cast_check = true:error -csharp_style_pattern_matching_over_as_with_null_check = true:error - -# Null-checking preferences -csharp_style_throw_expression = true:error -csharp_style_conditional_delegate_call = true:error - -# Modifier preferences -csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:error - -# Expression-level preferences -csharp_prefer_braces = true:error -csharp_style_deconstructed_variable_declaration = true:error -csharp_prefer_simple_default_expression = true:error -csharp_style_pattern_local_over_anonymous_function = true:error -csharp_style_inlined_variable_declaration = true:error - -############################### -# C# Formatting Rules # -############################### - -# https://stackoverflow.com/q/63369382/10942966 -csharp_qualified_using_at_nested_scope = true:error -csharp_using_directive_placement = outside_namespace:error - -# New line preferences -csharp_new_line_before_open_brace = all -csharp_new_line_before_else = true -csharp_new_line_before_catch = true -csharp_new_line_before_finally = true -csharp_new_line_before_members_in_object_initializers = true -csharp_new_line_before_members_in_anonymous_types = true -csharp_new_line_between_query_expression_clauses = true - -# Indentation preferences -charset = utf-8 -indent_size = 4 -insert_final_newline = true -csharp_indent_case_contents = true -csharp_indent_switch_labels = true -csharp_indent_labels = flush_left - -# Space preferences -csharp_space_after_cast = false -csharp_space_after_keywords_in_control_flow_statements = true -csharp_space_between_method_call_parameter_list_parentheses = false -csharp_space_between_method_declaration_parameter_list_parentheses = false -csharp_space_between_parentheses = false -csharp_space_before_colon_in_inheritance_clause = true -csharp_space_after_colon_in_inheritance_clause = true -csharp_space_around_binary_operators = before_and_after -csharp_space_between_method_declaration_empty_parameter_list_parentheses = false -csharp_space_between_method_call_name_and_opening_parenthesis = false -csharp_space_between_method_call_empty_parameter_list_parentheses = false - -# Wrapping preferences -csharp_preserve_single_line_statements = false -csharp_preserve_single_line_blocks = true - -# Use regular constructors over primary constructors -csharp_style_prefer_primary_constructors = false:error - -# Expression-bodied members -csharp_style_expression_bodied_methods = when_on_single_line:error -csharp_style_expression_bodied_constructors = when_on_single_line:error -csharp_style_expression_bodied_operators = when_on_single_line:error -csharp_style_expression_bodied_properties = when_on_single_line:error -csharp_style_expression_bodied_indexers = when_on_single_line:error -csharp_style_expression_bodied_accessors = when_on_single_line:error +############################### +# Core EditorConfig Options # +############################### +root = true + +[obsolete/*] +generated_code = true + +# All files +[*] +tab_width = 4 +end_of_line = crlf +indent_style = space + +# XML project files +[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] +indent_size = 2 + +# XML config files +[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}] +indent_size = 2 + +############################### +# .NET Coding Conventions # +############################### +[*.{cs,vb}] + +# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1826#exclude-firstordefault-and-lastordefault-methods +dotnet_code_quality.CA1826.exclude_ordefault_methods = true + +# Collection Expressions - int[] numbers = [] +dotnet_style_collection_initializer = true:error +dotnet_style_prefer_collection_expression = true:error + +# Nullable reference types +dotnet_diagnostic.CS8600.severity = suggestion +dotnet_diagnostic.CS8601.severity = suggestion +dotnet_diagnostic.CS8602.severity = suggestion +dotnet_diagnostic.CS8603.severity = suggestion +dotnet_diagnostic.CS8604.severity = suggestion +dotnet_diagnostic.CS8618.severity = suggestion +dotnet_diagnostic.CS8619.severity = suggestion +dotnet_diagnostic.CS8620.severity = suggestion +dotnet_diagnostic.CS8625.severity = suggestion +dotnet_diagnostic.CS8629.severity = suggestion +dotnet_diagnostic.CS8632.severity = suggestion +dotnet_diagnostic.CS8764.severity = suggestion +dotnet_diagnostic.CS8765.severity = suggestion +dotnet_diagnostic.CS8767.severity = suggestion + +dotnet_diagnostic.IDE0021.severity = error +dotnet_diagnostic.IDE0022.severity = error +dotnet_diagnostic.IDE0023.severity = error +dotnet_diagnostic.IDE0024.severity = error +dotnet_diagnostic.IDE0025.severity = error +dotnet_diagnostic.IDE0026.severity = error +dotnet_diagnostic.IDE0027.severity = error + +# Suppress CS1591: Missing XML comment for publicly visible type or member +dotnet_diagnostic.CS1591.severity = suggestion + +# Suppress IDE0058: Expression value is never used +dotnet_diagnostic.IDE0058.severity = none + +# Organize usings +dotnet_sort_system_directives_first = true:error +dotnet_separate_import_directive_groups = true:error + +# Unfortunately, no java-style usings +dotnet_diagnostic.IDE0064.severity = none + +# Using directive is unnecessary. +dotnet_diagnostic.IDE0005.severity = error + +# this. preferences +dotnet_style_qualification_for_field = true:error +dotnet_style_qualification_for_property = true:error +dotnet_style_qualification_for_method = false:error +dotnet_style_qualification_for_event = true:error + +# Language keywords vs BCL types preferences +dotnet_style_predefined_type_for_locals_parameters_members = true:error +dotnet_style_predefined_type_for_member_access = true:error + +# Parentheses preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:error +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:error +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:error +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:error + +# Modifier preferences +dotnet_style_require_accessibility_modifiers = always:error +dotnet_style_readonly_field = true:error + +# Expression-level preferences +dotnet_style_object_initializer = true:error +dotnet_style_collection_initializer = true:error +dotnet_style_explicit_tuple_names = true:error +dotnet_style_null_propagation = true:error +dotnet_style_coalesce_expression = true:error +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:error +dotnet_style_prefer_inferred_tuple_names = true:error +dotnet_style_prefer_inferred_anonymous_type_member_names = true:error +dotnet_style_prefer_auto_properties = true:error +dotnet_style_prefer_conditional_expression_over_assignment = true:error +dotnet_style_prefer_conditional_expression_over_return = true:error + +# Don't force namespaces to match their folder names (DSharpPlus.Entities) +dotnet_diagnostic.IDE0130.severity = none +dotnet_style_namespace_match_folder = false + +dotnet_style_prefer_simplified_boolean_expressions = true:error +dotnet_style_operator_placement_when_wrapping = beginning_of_line:error + +############################### +# Naming Conventions # +############################### + +# Style Definitions +dotnet_naming_style.pascal_case_style.capitalization = pascal_case + +# Use PascalCase for constant fields +dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = error +dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields +dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style +dotnet_naming_symbols.constant_fields.applicable_kinds = field +dotnet_naming_symbols.constant_fields.applicable_accessibilities = * +dotnet_naming_symbols.constant_fields.required_modifiers = const + +# Interfaces should start with I +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_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.capitalization = pascal_case +dotnet_naming_symbols.interface.applicable_kinds = interface + +# Async Methods should end with Async +dotnet_naming_rule.async_methods_should_be_async_suffix.severity = error +dotnet_naming_rule.async_methods_should_be_async_suffix.symbols = async_methods +dotnet_naming_rule.async_methods_should_be_async_suffix.style = async_suffix +dotnet_naming_style.async_suffix.required_suffix = Async +dotnet_naming_style.async_suffix.capitalization = pascal_case +dotnet_naming_symbols.async_methods.applicable_kinds = method +dotnet_naming_symbols.async_methods.required_modifiers = async + +# Fields should not begin with underscores, should be camelCase and should not use word separators +dotnet_naming_rule.all_fields_notunderscored.symbols = all_fields +dotnet_naming_rule.all_fields_notunderscored.style = notunderscored +dotnet_naming_rule.all_fields_notunderscored.severity = error +dotnet_naming_style.notunderscored.capitalization = camel_case +dotnet_naming_style.notunderscored.required_prefix = +dotnet_naming_style.notunderscored.word_separator = +dotnet_naming_symbols.all_fields.applicable_kinds = field +dotnet_naming_symbols.all_fields.applicable_accessibilities = * + +############################### +# C# Coding Conventions # +############################### +[*.cs] + +# File-scoped namespaces +csharp_style_namespace_declarations = file_scoped:error + +# var preferences +csharp_style_var_for_built_in_types = false:error +csharp_style_var_when_type_is_apparent = false:error +csharp_style_var_elsewhere = false:error + +# Pattern matching preferences +csharp_style_pattern_matching_over_is_with_cast_check = true:error +csharp_style_pattern_matching_over_as_with_null_check = true:error + +# Null-checking preferences +csharp_style_throw_expression = true:error +csharp_style_conditional_delegate_call = true:error + +# Modifier preferences +csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:error + +# Expression-level preferences +csharp_prefer_braces = true:error +csharp_style_deconstructed_variable_declaration = true:error +csharp_prefer_simple_default_expression = true:error +csharp_style_pattern_local_over_anonymous_function = true:error +csharp_style_inlined_variable_declaration = true:error + +############################### +# C# Formatting Rules # +############################### + +# https://stackoverflow.com/q/63369382/10942966 +csharp_qualified_using_at_nested_scope = true:error +csharp_using_directive_placement = outside_namespace:error + +# New line preferences +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_between_query_expression_clauses = true + +# Indentation preferences +charset = utf-8 +indent_size = 4 +insert_final_newline = true +csharp_indent_case_contents = true +csharp_indent_switch_labels = true +csharp_indent_labels = flush_left + +# Space preferences +csharp_space_after_cast = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_around_binary_operators = before_and_after +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false + +# Wrapping preferences +csharp_preserve_single_line_statements = false +csharp_preserve_single_line_blocks = true + +# Use regular constructors over primary constructors +csharp_style_prefer_primary_constructors = false:error + +# Expression-bodied members +csharp_style_expression_bodied_methods = when_on_single_line:error +csharp_style_expression_bodied_constructors = when_on_single_line:error +csharp_style_expression_bodied_operators = when_on_single_line:error +csharp_style_expression_bodied_properties = when_on_single_line:error +csharp_style_expression_bodied_indexers = when_on_single_line:error +csharp_style_expression_bodied_accessors = when_on_single_line:error csharp_style_expression_bodied_lambdas = when_on_single_line:error \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..3332e4e4ad --- /dev/null +++ b/.gitattributes @@ -0,0 +1,9 @@ +*.cs text eol=crlf +*.json text eol=crlf +*.txt text eol=crlf +*.md text eol=crlf +*.yml text eol=crlf +*.sln text eol=crlf +*.csproj text eol=crlf +*.sh text eol=lf +.* text eol=crlf \ No newline at end of file diff --git a/.github/workflows/build-commit.yml b/.github/workflows/build-commit.yml index 2f09025cf6..bec933b9d0 100644 --- a/.github/workflows/build-commit.yml +++ b/.github/workflows/build-commit.yml @@ -1,112 +1,112 @@ -name: Build Commit - -on: - push: - branches: [master] - paths: - - ".github/workflows/build-commit.yml" - - "DSharpPlus*/**" - - "tools/**" - - "docs/**" - - "*.sln" - - "obsolete/DSharpPlus*/**" - workflow_dispatch: - -env: - DOTNET_NOLOGO: 1 - DOTNET_CLI_TELEMETRY_OPTOUT: 1 - DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 - DOTNET_SYSTEM_GLOBALIZATION_INVARIANT: 1 - -jobs: - package-commit: - name: Package Commit - runs-on: ubuntu-latest - if: "!contains(format('{0} {1}', github.event.head_commit.message, github.event.pull_request.title), '[ci-skip]')" - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - submodules: recursive - fetch-depth: 0 - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: 9 - - name: Build Project - run: dotnet build - - name: Test Changes - run: dotnet test --blame-crash --blame-hang --blame-hang-timeout "30s" - - name: Get Nightly Version - id: nightly - run: printf "version=%0*d" 5 $(( 1195 + 691 + ${{ github.run_number }} )) >> "$GITHUB_OUTPUT" - - name: Package Project - run: | - # We add 1195 since it's the last build number AppVeyor used. - # We add 686 since it's the last build number GitHub Actions used before the workflow was renamed. - dotnet pack -c Release -o build -p:Nightly=${{ steps.nightly.outputs.version }} - dotnet nuget push "build/*" --skip-duplicate -k ${{ secrets.NUGET_ORG_API_KEY }} -s https://api.nuget.org/v3/index.json - env: - DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }} - DISCORD_GUILD_ID: ${{ secrets.DISCORD_GUILD_ID }} - DISCORD_CHANNEL_ID: ${{ secrets.DISCORD_CHANNEL_ID }} - DISCORD_CHANNEL_TOPIC: ${{ secrets.DISCORD_CHANNEL_TOPIC }} - DISCORD_DOC_BOT_USER_ID: ${{ secrets.DISCORD_DOC_BOT_USER_ID }} - DISCORD_BOT_USAGE_CHANNEL_ID: ${{ secrets.DISCORD_BOT_USAGE_CHANNEL_ID }} - NUGET_URL: ${{ secrets.NUGET_URL }} - GITHUB_URL : ${{ github.server_url }}/${{ github.repository }} - - name: Upload Artifact - uses: actions/upload-artifact@v4 - with: - name: DSharpPlus-Nightly-${{ steps.nightly.outputs.version }}.zip - path: ./build/* - - name: Get commit message header - id: get_commit_message - run: | - commit_message_header=$(git log -1 --pretty=%B | head -n 1) - echo "message_header=${commit_message_header}" >> $GITHUB_OUTPUT - - name: Discord Webhook - uses: tsickert/discord-webhook@v4.0.0 - with: - webhook-url: ${{ secrets.BUILD_WEBHOOK }} - embed-title: DSharpPlus 5.0.0-nightly-${{ steps.nightly.outputs.version }} - embed-description: | - NuGet Link: [`5.0.0-nightly-${{ steps.nightly.outputs.version }}`](https://www.nuget.org/packages/DSharpPlus/5.0.0-nightly-${{ steps.nightly.outputs.version }}) - Commit hash: [`${{ github.sha }}`](https://github.com/${{github.repository}}/commit/${{github.sha}}) - Commit message: ``${{ steps.get_commit_message.outputs.message_header }}`` - embed-color: 7506394 - document-commit: - name: Document Commit - runs-on: ubuntu-latest - needs: package-commit - permissions: - pages: write - id-token: write - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - submodules: recursive - fetch-depth: 0 - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: 9 - - name: Build Project - run: | - dotnet build - dotnet tool update -g docfx --prerelease - docfx docs/docfx.json - - name: Upload GitHub Pages artifact - uses: actions/upload-pages-artifact@v3 - with: - path: ./docs/_site/ - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v4 - - name: Reload Discord documentation - run: | - curl -X POST -H 'Authorization: Bot ${{ secrets.DISCORD_TOKEN }}' -H 'Content-Type: application/json' -d '{"content":"<@341606460720939008> reload"}' 'https://discord.com/api/v10/channels/379379415475552276/messages' +name: Build Commit + +on: + push: + branches: [master] + paths: + - ".github/workflows/build-commit.yml" + - "DSharpPlus*/**" + - "tools/**" + - "docs/**" + - "*.sln" + - "obsolete/DSharpPlus*/**" + workflow_dispatch: + +env: + DOTNET_NOLOGO: 1 + DOTNET_CLI_TELEMETRY_OPTOUT: 1 + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 + DOTNET_SYSTEM_GLOBALIZATION_INVARIANT: 1 + +jobs: + package-commit: + name: Package Commit + runs-on: ubuntu-latest + if: "!contains(format('{0} {1}', github.event.head_commit.message, github.event.pull_request.title), '[ci-skip]')" + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: recursive + fetch-depth: 0 + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 9 + - name: Build Project + run: dotnet build + - name: Test Changes + run: dotnet test --blame-crash --blame-hang --blame-hang-timeout "30s" + - name: Get Nightly Version + id: nightly + run: printf "version=%0*d" 5 $(( 1195 + 691 + ${{ github.run_number }} )) >> "$GITHUB_OUTPUT" + - name: Package Project + run: | + # We add 1195 since it's the last build number AppVeyor used. + # We add 686 since it's the last build number GitHub Actions used before the workflow was renamed. + dotnet pack -c Release -o build -p:Nightly=${{ steps.nightly.outputs.version }} + dotnet nuget push "build/*" --skip-duplicate -k ${{ secrets.NUGET_ORG_API_KEY }} -s https://api.nuget.org/v3/index.json + env: + DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }} + DISCORD_GUILD_ID: ${{ secrets.DISCORD_GUILD_ID }} + DISCORD_CHANNEL_ID: ${{ secrets.DISCORD_CHANNEL_ID }} + DISCORD_CHANNEL_TOPIC: ${{ secrets.DISCORD_CHANNEL_TOPIC }} + DISCORD_DOC_BOT_USER_ID: ${{ secrets.DISCORD_DOC_BOT_USER_ID }} + DISCORD_BOT_USAGE_CHANNEL_ID: ${{ secrets.DISCORD_BOT_USAGE_CHANNEL_ID }} + NUGET_URL: ${{ secrets.NUGET_URL }} + GITHUB_URL : ${{ github.server_url }}/${{ github.repository }} + - name: Upload Artifact + uses: actions/upload-artifact@v4 + with: + name: DSharpPlus-Nightly-${{ steps.nightly.outputs.version }}.zip + path: ./build/* + - name: Get commit message header + id: get_commit_message + run: | + commit_message_header=$(git log -1 --pretty=%B | head -n 1) + echo "message_header=${commit_message_header}" >> $GITHUB_OUTPUT + - name: Discord Webhook + uses: tsickert/discord-webhook@v4.0.0 + with: + webhook-url: ${{ secrets.BUILD_WEBHOOK }} + embed-title: DSharpPlus 5.0.0-nightly-${{ steps.nightly.outputs.version }} + embed-description: | + NuGet Link: [`5.0.0-nightly-${{ steps.nightly.outputs.version }}`](https://www.nuget.org/packages/DSharpPlus/5.0.0-nightly-${{ steps.nightly.outputs.version }}) + Commit hash: [`${{ github.sha }}`](https://github.com/${{github.repository}}/commit/${{github.sha}}) + Commit message: ``${{ steps.get_commit_message.outputs.message_header }}`` + embed-color: 7506394 + document-commit: + name: Document Commit + runs-on: ubuntu-latest + needs: package-commit + permissions: + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: recursive + fetch-depth: 0 + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 9 + - name: Build Project + run: | + dotnet build + dotnet tool update -g docfx --prerelease + docfx docs/docfx.json + - name: Upload GitHub Pages artifact + uses: actions/upload-pages-artifact@v3 + with: + path: ./docs/_site/ + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 + - name: Reload Discord documentation + run: | + curl -X POST -H 'Authorization: Bot ${{ secrets.DISCORD_TOKEN }}' -H 'Content-Type: application/json' -d '{"content":"<@341606460720939008> reload"}' 'https://discord.com/api/v10/channels/379379415475552276/messages' diff --git a/.github/workflows/build-pr.yml b/.github/workflows/build-pr.yml index ca91c500e6..493f124b17 100644 --- a/.github/workflows/build-pr.yml +++ b/.github/workflows/build-pr.yml @@ -1,48 +1,48 @@ -name: Build PR - -on: - pull_request: - types: - - opened - - synchronize - - reopened - - ready_for_review - paths: - - ".github/workflows/build-pr.yml" - - "DSharpPlus*/**" - - "tools/**" - - "*.sln" -env: - DOTNET_NOLOGO: 1 - DOTNET_CLI_TELEMETRY_OPTOUT: 1 - DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 - DOTNET_SYSTEM_GLOBALIZATION_INVARIANT: 1 - -jobs: - build-commit: - name: "Build PR #${{ github.event.pull_request.number }}" - runs-on: ubuntu-latest - if: "!contains(format('{0} {1}', github.event.head_commit.message, github.event.pull_request.title), '[ci-skip]')" - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - submodules: recursive - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: 9 - - name: Build Project - run: dotnet build - - name: Test Changes - run: dotnet test --blame-crash --blame-hang --blame-hang-timeout "30s" - - name: Get PR Version - id: pr - run: printf "version=%0*d" 5 ${{ github.run_number }} >> "$GITHUB_OUTPUT" - - name: Build and Package Project - run: dotnet pack --include-symbols --include-source -o build -p:PR="${{ github.event.pull_request.number }}-${{ steps.pr.outputs.version }}" - - name: Upload artifacts - uses: actions/upload-artifact@v4 - with: - name: DSharpPlus-PR-${{ github.event.pull_request.number }}-${{ steps.pr.outputs.version }} +name: Build PR + +on: + pull_request: + types: + - opened + - synchronize + - reopened + - ready_for_review + paths: + - ".github/workflows/build-pr.yml" + - "DSharpPlus*/**" + - "tools/**" + - "*.sln" +env: + DOTNET_NOLOGO: 1 + DOTNET_CLI_TELEMETRY_OPTOUT: 1 + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 + DOTNET_SYSTEM_GLOBALIZATION_INVARIANT: 1 + +jobs: + build-commit: + name: "Build PR #${{ github.event.pull_request.number }}" + runs-on: ubuntu-latest + if: "!contains(format('{0} {1}', github.event.head_commit.message, github.event.pull_request.title), '[ci-skip]')" + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: recursive + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 9 + - name: Build Project + run: dotnet build + - name: Test Changes + run: dotnet test --blame-crash --blame-hang --blame-hang-timeout "30s" + - name: Get PR Version + id: pr + run: printf "version=%0*d" 5 ${{ github.run_number }} >> "$GITHUB_OUTPUT" + - name: Build and Package Project + run: dotnet pack --include-symbols --include-source -o build -p:PR="${{ github.event.pull_request.number }}-${{ steps.pr.outputs.version }}" + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: DSharpPlus-PR-${{ github.event.pull_request.number }}-${{ steps.pr.outputs.version }} path: ./build/* \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b56a758d1c..792bbd9cf1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,92 +1,92 @@ -name: Release -on: - release: - types: ["published"] - -env: - DOTNET_NOLOGO: 1 - DOTNET_CLI_TELEMETRY_OPTOUT: 1 - DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 - DOTNET_SYSTEM_GLOBALIZATION_INVARIANT: 1 - -jobs: - build-commit: - name: Build Commit - runs-on: ubuntu-latest - if: "!contains(format('{0} {1}', github.event.head_commit.message, github.event.pull_request.title), '[ci-skip]')" - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - submodules: recursive - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: 9 - - name: Build Project - run: dotnet build - package-commit: - name: Package Commit - runs-on: ubuntu-latest - needs: build-commit - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - submodules: recursive - fetch-depth: 0 - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: 8 - - name: Package Project - run: | - dotnet pack -c Release -o build - dotnet nuget push "build/*" --skip-duplicate -k ${{ secrets.NUGET_ORG_API_KEY }} -s https://api.nuget.org/v3/index.json - LATEST_STABLE_VERSION=$(git describe --abbrev=0 --tags 2>/dev/null || echo '') dotnet run --project ./tools/AutoUpdateChannelDescription - env: - DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }} - DISCORD_GUILD_ID: ${{ secrets.DISCORD_GUILD_ID }} - DISCORD_CHANNEL_ID: ${{ secrets.DISCORD_CHANNEL_ID }} - DISCORD_CHANNEL_TOPIC: ${{ secrets.DISCORD_CHANNEL_TOPIC }} - DISCORD_DOC_BOT_USER_ID: ${{ secrets.DISCORD_DOC_BOT_USER_ID }} - DISCORD_BOT_USAGE_CHANNEL_ID: ${{ secrets.DISCORD_BOT_USAGE_CHANNEL_ID }} - NUGET_URL: ${{ secrets.NUGET_URL }} - GITHUB_URL : ${{ github.server_url }}/${{ github.repository }} - - name: Upload Artifact - uses: actions/upload-artifact@v4 - with: - name: DSharpPlus.zip - path: ./build/* - document-commit: - name: Document Commit - runs-on: ubuntu-latest - needs: package-commit - permissions: - pages: write - id-token: write - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - submodules: recursive - fetch-depth: 0 - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: 8 - - name: Build Project - run: | - dotnet build - dotnet tool update -g docfx --prerelease - docfx docs/docfx.json - - name: Upload GitHub Pages artifact - uses: actions/upload-pages-artifact@v3 - with: - path: ./docs/_site/ - - name: Deploy to GitHub Pages - id: deployment +name: Release +on: + release: + types: ["published"] + +env: + DOTNET_NOLOGO: 1 + DOTNET_CLI_TELEMETRY_OPTOUT: 1 + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 + DOTNET_SYSTEM_GLOBALIZATION_INVARIANT: 1 + +jobs: + build-commit: + name: Build Commit + runs-on: ubuntu-latest + if: "!contains(format('{0} {1}', github.event.head_commit.message, github.event.pull_request.title), '[ci-skip]')" + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: recursive + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 9 + - name: Build Project + run: dotnet build + package-commit: + name: Package Commit + runs-on: ubuntu-latest + needs: build-commit + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: recursive + fetch-depth: 0 + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8 + - name: Package Project + run: | + dotnet pack -c Release -o build + dotnet nuget push "build/*" --skip-duplicate -k ${{ secrets.NUGET_ORG_API_KEY }} -s https://api.nuget.org/v3/index.json + LATEST_STABLE_VERSION=$(git describe --abbrev=0 --tags 2>/dev/null || echo '') dotnet run --project ./tools/AutoUpdateChannelDescription + env: + DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }} + DISCORD_GUILD_ID: ${{ secrets.DISCORD_GUILD_ID }} + DISCORD_CHANNEL_ID: ${{ secrets.DISCORD_CHANNEL_ID }} + DISCORD_CHANNEL_TOPIC: ${{ secrets.DISCORD_CHANNEL_TOPIC }} + DISCORD_DOC_BOT_USER_ID: ${{ secrets.DISCORD_DOC_BOT_USER_ID }} + DISCORD_BOT_USAGE_CHANNEL_ID: ${{ secrets.DISCORD_BOT_USAGE_CHANNEL_ID }} + NUGET_URL: ${{ secrets.NUGET_URL }} + GITHUB_URL : ${{ github.server_url }}/${{ github.repository }} + - name: Upload Artifact + uses: actions/upload-artifact@v4 + with: + name: DSharpPlus.zip + path: ./build/* + document-commit: + name: Document Commit + runs-on: ubuntu-latest + needs: package-commit + permissions: + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: recursive + fetch-depth: 0 + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8 + - name: Build Project + run: | + dotnet build + dotnet tool update -g docfx --prerelease + docfx docs/docfx.json + - name: Upload GitHub Pages artifact + uses: actions/upload-pages-artifact@v3 + with: + path: ./docs/_site/ + - name: Deploy to GitHub Pages + id: deployment uses: actions/deploy-pages@v4 \ No newline at end of file diff --git a/.gitignore b/.gitignore index d224448a06..ecebeb4bde 100644 --- a/.gitignore +++ b/.gitignore @@ -1,490 +1,490 @@ -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. -## -## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore - -# User-specific files -*.rsuser -*.suo -*.user -*.userosscache -*.sln.docstates - -# User-specific files (MonoDevelop/Xamarin Studio) -*.userprefs - -# Mono auto generated files -mono_crash.* - -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -[Ww][Ii][Nn]32/ -[Aa][Rr][Mm]/ -[Aa][Rr][Mm]64/ -bld/ -[Bb]in/ -[Oo]bj/ -[Ll]og/ -[Ll]ogs/ - -# Visual Studio 2015/2017 cache/options directory -.vs/ -# Uncomment if you have tasks that create the project's static files in wwwroot -#wwwroot/ - -# Visual Studio 2017 auto generated files -Generated\ Files/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -# NUnit -*.VisualState.xml -TestResult.xml -nunit-*.xml - -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -# Benchmark Results -BenchmarkDotNet.Artifacts/ - -# .NET -project.lock.json -project.fragment.lock.json -artifacts/ - -# Tye -.tye/ - -# ASP.NET Scaffolding -ScaffoldingReadMe.txt - -# StyleCop -StyleCopReport.xml - -# Files built by Visual Studio -*_i.c -*_p.c -*_h.h -*.ilk -*.meta -*.obj -*.iobj -*.pch -*.pdb -*.ipdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*_wpftmp.csproj -*.log -*.tlog -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opendb -*.opensdf -*.sdf -*.cachefile -*.VC.db -*.VC.VC.opendb - -# Visual Studio profiler -*.psess -*.vsp -*.vspx -*.sap - -# Visual Studio Trace Files -*.e2e - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper -*.DotSettings.user - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# AxoCover is a Code Coverage Tool -.axoCover/* -!.axoCover/settings.json - -# Coverlet is a free, cross platform Code Coverage Tool -coverage*.json -coverage*.xml -coverage*.info - -# Visual Studio code coverage results -*.coverage -*.coveragexml - -# NCrunch -_NCrunch_* -.*crunch*.local.xml -nCrunchTemp_* - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -# Note: Comment the next line if you want to checkin your web deploy settings, -# but database connection strings (with potential passwords) will be unencrypted -*.pubxml -*.publishproj - -# Microsoft Azure Web App publish settings. Comment the next line if you want to -# checkin your Azure Web App publish settings, but sensitive information contained -# in these scripts will be unencrypted -PublishScripts/ - -# NuGet Packages -*.nupkg -# NuGet Symbol Packages -*.snupkg -# The packages folder can be ignored because of Package Restore -**/[Pp]ackages/* -# except build/, which is used as an MSBuild target. -!**/[Pp]ackages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/[Pp]ackages/repositories.config -# NuGet v3's project.json files produces more ignorable files -*.nuget.props -*.nuget.targets - -# Microsoft Azure Build Output -csx/ -*.build.csdef - -# Microsoft Azure Emulator -ecf/ -rcf/ - -# Windows Store app package directories and files -AppPackages/ -BundleArtifacts/ -Package.StoreAssociation.xml -_pkginfo.txt -*.appx -*.appxbundle -*.appxupload - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!?*.[Cc]ache/ - -# Others -ClientBin/ -~$* -*~ -*.dbmdl -*.dbproj.schemaview -*.jfm -*.pfx -*.publishsettings -orleans.codegen.cs - -# Including strong name files can present a security risk -# (https://github.com/github/gitignore/pull/2483#issue-259490424) -#*.snk - -# Since there are multiple workflows, uncomment next line to ignore bower_components -# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) -#bower_components/ - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm -ServiceFabricBackup/ -*.rptproj.bak - -# SQL Server files -*.mdf -*.ldf -*.ndf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings -*.rptproj.rsuser -*- [Bb]ackup.rdl -*- [Bb]ackup ([0-9]).rdl -*- [Bb]ackup ([0-9][0-9]).rdl - -# Microsoft Fakes -FakesAssemblies/ - -# GhostDoc plugin setting file -*.GhostDoc.xml - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat -node_modules/ - -# Visual Studio 6 build log -*.plg - -# Visual Studio 6 workspace options file -*.opt - -# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) -*.vbw - -# Visual Studio 6 auto-generated project file (contains which files were open etc.) -*.vbp - -# Visual Studio 6 workspace and project file (working project files containing files to include in project) -*.dsw -*.dsp - -# Visual Studio 6 technical files -*.ncb -*.aps - -# Visual Studio LightSwitch build output -**/*.HTMLClient/GeneratedArtifacts -**/*.DesktopClient/GeneratedArtifacts -**/*.DesktopClient/ModelManifest.xml -**/*.Server/GeneratedArtifacts -**/*.Server/ModelManifest.xml -_Pvt_Extensions - -# Paket dependency manager -.paket/paket.exe -paket-files/ - -# FAKE - F# Make -.fake/ - -# CodeRush personal settings -.cr/personal - -# Python Tools for Visual Studio (PTVS) -__pycache__/ -*.pyc - -# Cake - Uncomment if you are using it -# tools/** -# !tools/packages.config - -# Tabs Studio -*.tss - -# Telerik's JustMock configuration file -*.jmconfig - -# BizTalk build output -*.btp.cs -*.btm.cs -*.odx.cs -*.xsd.cs - -# OpenCover UI analysis results -OpenCover/ - -# Azure Stream Analytics local run output -ASALocalRun/ - -# MSBuild Binary and Structured Log -*.binlog - -# NVidia Nsight GPU debugger configuration file -*.nvuser - -# MFractors (Xamarin productivity tool) working folder -.mfractor/ - -# Local History for Visual Studio -.localhistory/ - -# Visual Studio History (VSHistory) files -.vshistory/ - -# BeatPulse healthcheck temp database -healthchecksdb - -# Backup folder for Package Reference Convert tool in Visual Studio 2017 -MigrationBackup/ - -# Ionide (cross platform F# VS Code tools) working folder -.ionide/ - -# Fody - auto-generated XML schema -FodyWeavers.xsd - -# VS Code files for those working on multiple tools -.vscode/* -!.vscode/settings.json -!.vscode/tasks.json -!.vscode/launch.json -!.vscode/extensions.json -*.code-workspace - -# Local History for Visual Studio Code -.history/ - -# Windows Installer files from build outputs -*.cab -*.msi -*.msix -*.msm -*.msp - -# JetBrains Rider -*.sln.iml - -## -## Visual studio for Mac -## - - -# globs -Makefile.in -*.userprefs -*.usertasks -config.make -config.status -aclocal.m4 -install-sh -autom4te.cache/ -*.tar.gz -tarballs/ -test-results/ - -# Mac bundle stuff -*.dmg -*.app - -# content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore -# General -.DS_Store -.AppleDouble -.LSOverride - -# Icon must end with two \r -Icon - - -# Thumbnails -._* - -# Files that might appear in the root of a volume -.DocumentRevisions-V100 -.fseventsd -.Spotlight-V100 -.TemporaryItems -.Trashes -.VolumeIcon.icns -.com.apple.timemachine.donotpresent - -# Directories potentially created on remote AFP share -.AppleDB -.AppleDesktop -Network Trash Folder -Temporary Items -.apdisk - -# content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore -# Windows thumbnail cache files -Thumbs.db -ehthumbs.db -ehthumbs_vista.db - -# Dump file -*.stackdump - -# Folder config file -[Dd]esktop.ini - -# Recycle Bin used on file shares -$RECYCLE.BIN/ - -# Windows Installer files -*.cab -*.msi -*.msix -*.msm -*.msp - -# Windows shortcuts -*.lnk - -# Vim temporary swap files -*.swp - -# DSharpPlus changes -.idea -.vscode -*.DotSettings -*.patch -docs/_site -DSharpPlus.Test/config.json -/DSharpPlus.HttpInteractions.AspNetCore/Properties/launchSettings.json -/DSharpPlus.Http.AspNetCore/Properties/launchSettings.json +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET +project.lock.json +project.fragment.lock.json +artifacts/ + +# Tye +.tye/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml + +## +## Visual studio for Mac +## + + +# globs +Makefile.in +*.userprefs +*.usertasks +config.make +config.status +aclocal.m4 +install-sh +autom4te.cache/ +*.tar.gz +tarballs/ +test-results/ + +# Mac bundle stuff +*.dmg +*.app + +# content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# Vim temporary swap files +*.swp + +# DSharpPlus changes +.idea +.vscode +*.DotSettings +*.patch +docs/_site +DSharpPlus.Test/config.json +/DSharpPlus.HttpInteractions.AspNetCore/Properties/launchSettings.json +/DSharpPlus.Http.AspNetCore/Properties/launchSettings.json diff --git a/DSharpPlus.Commands/AbstractContext.cs b/DSharpPlus.Commands/AbstractContext.cs index 7913ea7f2f..cb7835fb7a 100644 --- a/DSharpPlus.Commands/AbstractContext.cs +++ b/DSharpPlus.Commands/AbstractContext.cs @@ -1,20 +1,20 @@ -using System; -using DSharpPlus.Commands.Trees; -using DSharpPlus.Entities; -using Microsoft.Extensions.DependencyInjection; - -namespace DSharpPlus.Commands; - -public abstract record AbstractContext -{ - public required DiscordUser User { get; init; } - public required DiscordChannel Channel { get; init; } - public required CommandsExtension Extension { get; init; } - public required Command Command { get; init; } - public required IServiceScope ServiceScope { internal get; init; } - - public DiscordGuild? Guild => this.Channel.Guild; - public DiscordMember? Member => this.User as DiscordMember; - public DiscordClient Client => this.Extension.Client; - public IServiceProvider ServiceProvider => this.ServiceScope.ServiceProvider; -} +using System; +using DSharpPlus.Commands.Trees; +using DSharpPlus.Entities; +using Microsoft.Extensions.DependencyInjection; + +namespace DSharpPlus.Commands; + +public abstract record AbstractContext +{ + public required DiscordUser User { get; init; } + public required DiscordChannel Channel { get; init; } + public required CommandsExtension Extension { get; init; } + public required Command Command { get; init; } + public required IServiceScope ServiceScope { internal get; init; } + + public DiscordGuild? Guild => this.Channel.Guild; + public DiscordMember? Member => this.User as DiscordMember; + public DiscordClient Client => this.Extension.Client; + public IServiceProvider ServiceProvider => this.ServiceScope.ServiceProvider; +} diff --git a/DSharpPlus.Commands/ArgumentModifiers/ChannelTypesAttribute.cs b/DSharpPlus.Commands/ArgumentModifiers/ChannelTypesAttribute.cs index b1e14a8fee..91a3e9e2b3 100644 --- a/DSharpPlus.Commands/ArgumentModifiers/ChannelTypesAttribute.cs +++ b/DSharpPlus.Commands/ArgumentModifiers/ChannelTypesAttribute.cs @@ -1,18 +1,18 @@ -using System; -using DSharpPlus.Commands.ContextChecks.ParameterChecks; -using DSharpPlus.Entities; - -namespace DSharpPlus.Commands.ArgumentModifiers; - -/// -/// Specifies what channel types the parameter supports. -/// -/// The required types of channels. -[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] -public sealed class ChannelTypesAttribute(params DiscordChannelType[] channelTypes) : ParameterCheckAttribute -{ - /// - /// Gets the channel types allowed for this parameter. - /// - public DiscordChannelType[] ChannelTypes { get; init; } = channelTypes; -} +using System; +using DSharpPlus.Commands.ContextChecks.ParameterChecks; +using DSharpPlus.Entities; + +namespace DSharpPlus.Commands.ArgumentModifiers; + +/// +/// Specifies what channel types the parameter supports. +/// +/// The required types of channels. +[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] +public sealed class ChannelTypesAttribute(params DiscordChannelType[] channelTypes) : ParameterCheckAttribute +{ + /// + /// Gets the channel types allowed for this parameter. + /// + public DiscordChannelType[] ChannelTypes { get; init; } = channelTypes; +} diff --git a/DSharpPlus.Commands/ArgumentModifiers/FromCode/CodeType.cs b/DSharpPlus.Commands/ArgumentModifiers/FromCode/CodeType.cs index 0932f5f8b0..e8d33790c3 100644 --- a/DSharpPlus.Commands/ArgumentModifiers/FromCode/CodeType.cs +++ b/DSharpPlus.Commands/ArgumentModifiers/FromCode/CodeType.cs @@ -1,25 +1,25 @@ -using System; - -namespace DSharpPlus.Commands.ArgumentModifiers; - -/// -/// The types of code-formatted text to accept. -/// -[Flags] -public enum CodeType -{ - /// - /// Accept inline code blocks - codeblocks that will not contain any newlines. - /// - Inline = 1 << 0, - - /// - /// Accept codeblocks - codeblocks that will contain possibly multiple newlines. - /// - Codeblock = 1 << 1, - - /// - /// Accept any type of code block. - /// - All = Inline | Codeblock -} +using System; + +namespace DSharpPlus.Commands.ArgumentModifiers; + +/// +/// The types of code-formatted text to accept. +/// +[Flags] +public enum CodeType +{ + /// + /// Accept inline code blocks - codeblocks that will not contain any newlines. + /// + Inline = 1 << 0, + + /// + /// Accept codeblocks - codeblocks that will contain possibly multiple newlines. + /// + Codeblock = 1 << 1, + + /// + /// Accept any type of code block. + /// + All = Inline | Codeblock +} diff --git a/DSharpPlus.Commands/ArgumentModifiers/FromCode/FromCodeAttribute.cs b/DSharpPlus.Commands/ArgumentModifiers/FromCode/FromCodeAttribute.cs index a330a13ca4..fefaab1c40 100644 --- a/DSharpPlus.Commands/ArgumentModifiers/FromCode/FromCodeAttribute.cs +++ b/DSharpPlus.Commands/ArgumentModifiers/FromCode/FromCodeAttribute.cs @@ -1,21 +1,21 @@ -using System; - -namespace DSharpPlus.Commands.ArgumentModifiers; - -/// -/// Removes the need to manually parse code blocks from a string. -/// -[AttributeUsage(AttributeTargets.Parameter)] -public sealed partial class FromCodeAttribute : Attribute -{ - /// - /// The type of code block to accept. - /// - public CodeType CodeType { get; init; } - - /// - /// Creates a new with the specified . - /// - /// The type of code block to accept. - public FromCodeAttribute(CodeType codeType = CodeType.All) => this.CodeType = codeType; -} +using System; + +namespace DSharpPlus.Commands.ArgumentModifiers; + +/// +/// Removes the need to manually parse code blocks from a string. +/// +[AttributeUsage(AttributeTargets.Parameter)] +public sealed partial class FromCodeAttribute : Attribute +{ + /// + /// The type of code block to accept. + /// + public CodeType CodeType { get; init; } + + /// + /// Creates a new with the specified . + /// + /// The type of code block to accept. + public FromCodeAttribute(CodeType codeType = CodeType.All) => this.CodeType = codeType; +} diff --git a/DSharpPlus.Commands/ArgumentModifiers/MinMaxLengthAttribute.cs b/DSharpPlus.Commands/ArgumentModifiers/MinMaxLengthAttribute.cs index df6a4bcd09..53e9eb6826 100644 --- a/DSharpPlus.Commands/ArgumentModifiers/MinMaxLengthAttribute.cs +++ b/DSharpPlus.Commands/ArgumentModifiers/MinMaxLengthAttribute.cs @@ -1,42 +1,42 @@ -using System; -using DSharpPlus.Commands.ContextChecks.ParameterChecks; - -namespace DSharpPlus.Commands.ArgumentModifiers; - -/// -/// Determines the minimum and maximum length that a parameter can accept. -/// -[AttributeUsage(AttributeTargets.Parameter)] -public sealed class MinMaxLengthAttribute : ParameterCheckAttribute -{ - // on text commands, we interpret 6000 as unlimited - it exceeds the message limit anyway - private const int MinLengthMinimum = 0; - private const int MinLengthMaximum = 6000; - private const int MaxLengthMinimum = 1; - private const int MaxLengthMaximum = 6000; - - /// - /// The minimum length that this parameter can accept. - /// - public int MinLength { get; private init; } - - /// - /// The maximum length that this parameter can accept. - /// - public int MaxLength { get; private init; } - - /// - /// Determines the minimum and maximum length that a parameter can accept. - /// - public MinMaxLengthAttribute(int minLength = MinLengthMinimum, int maxLength = MaxLengthMaximum) - { - ArgumentOutOfRangeException.ThrowIfLessThan(minLength, MinLengthMinimum, nameof(minLength)); - ArgumentOutOfRangeException.ThrowIfGreaterThan(minLength, MinLengthMaximum, nameof(minLength)); - ArgumentOutOfRangeException.ThrowIfLessThan(maxLength, MaxLengthMinimum, nameof(maxLength)); - ArgumentOutOfRangeException.ThrowIfGreaterThan(maxLength, MaxLengthMaximum, nameof(maxLength)); - ArgumentOutOfRangeException.ThrowIfGreaterThan(minLength, maxLength, nameof(minLength)); - - this.MinLength = minLength; - this.MaxLength = maxLength; - } -} +using System; +using DSharpPlus.Commands.ContextChecks.ParameterChecks; + +namespace DSharpPlus.Commands.ArgumentModifiers; + +/// +/// Determines the minimum and maximum length that a parameter can accept. +/// +[AttributeUsage(AttributeTargets.Parameter)] +public sealed class MinMaxLengthAttribute : ParameterCheckAttribute +{ + // on text commands, we interpret 6000 as unlimited - it exceeds the message limit anyway + private const int MinLengthMinimum = 0; + private const int MinLengthMaximum = 6000; + private const int MaxLengthMinimum = 1; + private const int MaxLengthMaximum = 6000; + + /// + /// The minimum length that this parameter can accept. + /// + public int MinLength { get; private init; } + + /// + /// The maximum length that this parameter can accept. + /// + public int MaxLength { get; private init; } + + /// + /// Determines the minimum and maximum length that a parameter can accept. + /// + public MinMaxLengthAttribute(int minLength = MinLengthMinimum, int maxLength = MaxLengthMaximum) + { + ArgumentOutOfRangeException.ThrowIfLessThan(minLength, MinLengthMinimum, nameof(minLength)); + ArgumentOutOfRangeException.ThrowIfGreaterThan(minLength, MinLengthMaximum, nameof(minLength)); + ArgumentOutOfRangeException.ThrowIfLessThan(maxLength, MaxLengthMinimum, nameof(maxLength)); + ArgumentOutOfRangeException.ThrowIfGreaterThan(maxLength, MaxLengthMaximum, nameof(maxLength)); + ArgumentOutOfRangeException.ThrowIfGreaterThan(minLength, maxLength, nameof(minLength)); + + this.MinLength = minLength; + this.MaxLength = maxLength; + } +} diff --git a/DSharpPlus.Commands/ArgumentModifiers/MinMaxValueAttribute.cs b/DSharpPlus.Commands/ArgumentModifiers/MinMaxValueAttribute.cs index 5876555122..450bed7f6e 100644 --- a/DSharpPlus.Commands/ArgumentModifiers/MinMaxValueAttribute.cs +++ b/DSharpPlus.Commands/ArgumentModifiers/MinMaxValueAttribute.cs @@ -1,60 +1,60 @@ -using System; -using DSharpPlus.Commands.ContextChecks.ParameterChecks; - -namespace DSharpPlus.Commands.ArgumentModifiers; - -/// -/// Determines the minimum and maximum values that a parameter can accept. -/// -[AttributeUsage(AttributeTargets.Parameter)] -public sealed class MinMaxValueAttribute : ParameterCheckAttribute -{ - /// - /// The minimum value that this parameter can accept. - /// - public object? MinValue { get; private init; } - - /// - /// The maximum value that this parameter can accept. - /// - public object? MaxValue { get; private init; } - - /// - /// Determines the minimum and maximum values that a parameter can accept. - /// - public MinMaxValueAttribute(object? minValue = null, object? maxValue = null) - { - this.MinValue = minValue; - this.MaxValue = maxValue; - - if (minValue is not null && maxValue is not null && minValue.GetType() != maxValue.GetType()) - { - throw new ArgumentException("The minimum and maximum values must be of the same type."); - } - - if (minValue is null || maxValue is null) - { - return; - } - - bool correctlyOrdered = minValue switch - { - byte => (byte)minValue <= (byte)maxValue, - sbyte => (sbyte)minValue <= (sbyte)maxValue, - short => (short)minValue <= (short)maxValue, - ushort => (ushort)minValue <= (ushort)maxValue, - int => (int)minValue <= (int)maxValue, - uint => (uint)minValue <= (uint)maxValue, - long => (long)minValue <= (long)maxValue, - ulong => (ulong)minValue <= (ulong)maxValue, - float => (float)minValue <= (float)maxValue, - double => (double)minValue <= (double)maxValue, - _ => throw new ArgumentException("The type of the minimum/maximum values is not supported."), - }; - - if (!correctlyOrdered) - { - throw new ArgumentException("The minimum value cannot be greater than the maximum value."); - } - } -} +using System; +using DSharpPlus.Commands.ContextChecks.ParameterChecks; + +namespace DSharpPlus.Commands.ArgumentModifiers; + +/// +/// Determines the minimum and maximum values that a parameter can accept. +/// +[AttributeUsage(AttributeTargets.Parameter)] +public sealed class MinMaxValueAttribute : ParameterCheckAttribute +{ + /// + /// The minimum value that this parameter can accept. + /// + public object? MinValue { get; private init; } + + /// + /// The maximum value that this parameter can accept. + /// + public object? MaxValue { get; private init; } + + /// + /// Determines the minimum and maximum values that a parameter can accept. + /// + public MinMaxValueAttribute(object? minValue = null, object? maxValue = null) + { + this.MinValue = minValue; + this.MaxValue = maxValue; + + if (minValue is not null && maxValue is not null && minValue.GetType() != maxValue.GetType()) + { + throw new ArgumentException("The minimum and maximum values must be of the same type."); + } + + if (minValue is null || maxValue is null) + { + return; + } + + bool correctlyOrdered = minValue switch + { + byte => (byte)minValue <= (byte)maxValue, + sbyte => (sbyte)minValue <= (sbyte)maxValue, + short => (short)minValue <= (short)maxValue, + ushort => (ushort)minValue <= (ushort)maxValue, + int => (int)minValue <= (int)maxValue, + uint => (uint)minValue <= (uint)maxValue, + long => (long)minValue <= (long)maxValue, + ulong => (ulong)minValue <= (ulong)maxValue, + float => (float)minValue <= (float)maxValue, + double => (double)minValue <= (double)maxValue, + _ => throw new ArgumentException("The type of the minimum/maximum values is not supported."), + }; + + if (!correctlyOrdered) + { + throw new ArgumentException("The minimum value cannot be greater than the maximum value."); + } + } +} diff --git a/DSharpPlus.Commands/ArgumentModifiers/RemainingTextAttribute.cs b/DSharpPlus.Commands/ArgumentModifiers/RemainingTextAttribute.cs index 5c901ce30e..4b8ce68fe3 100644 --- a/DSharpPlus.Commands/ArgumentModifiers/RemainingTextAttribute.cs +++ b/DSharpPlus.Commands/ArgumentModifiers/RemainingTextAttribute.cs @@ -1,6 +1,6 @@ -using System; - -namespace DSharpPlus.Commands.ArgumentModifiers; - -[AttributeUsage(AttributeTargets.Parameter)] -public sealed class RemainingTextAttribute : Attribute; +using System; + +namespace DSharpPlus.Commands.ArgumentModifiers; + +[AttributeUsage(AttributeTargets.Parameter)] +public sealed class RemainingTextAttribute : Attribute; diff --git a/DSharpPlus.Commands/CommandAttribute.cs b/DSharpPlus.Commands/CommandAttribute.cs index a51ba0a563..f4bc617d92 100644 --- a/DSharpPlus.Commands/CommandAttribute.cs +++ b/DSharpPlus.Commands/CommandAttribute.cs @@ -1,30 +1,30 @@ -using System; - -namespace DSharpPlus.Commands; - -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Delegate)] -public sealed class CommandAttribute : Attribute -{ - /// - /// The name of the command. - /// - public string Name { get; init; } - - /// - /// Creates a new instance of the class. - /// - /// The name of the command. - public CommandAttribute(string name) - { - if (string.IsNullOrWhiteSpace(name)) - { - throw new ArgumentNullException(nameof(name), "The name of the command cannot be null or whitespace."); - } - else if (name.Length is < 1 or > 32) - { - throw new ArgumentOutOfRangeException(nameof(name), "The name of the command must be between 1 and 32 characters."); - } - - this.Name = name; - } -} +using System; + +namespace DSharpPlus.Commands; + +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Delegate)] +public sealed class CommandAttribute : Attribute +{ + /// + /// The name of the command. + /// + public string Name { get; init; } + + /// + /// Creates a new instance of the class. + /// + /// The name of the command. + public CommandAttribute(string name) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentNullException(nameof(name), "The name of the command cannot be null or whitespace."); + } + else if (name.Length is < 1 or > 32) + { + throw new ArgumentOutOfRangeException(nameof(name), "The name of the command must be between 1 and 32 characters."); + } + + this.Name = name; + } +} diff --git a/DSharpPlus.Commands/CommandContext.cs b/DSharpPlus.Commands/CommandContext.cs index 249e728a96..256f11b2e7 100644 --- a/DSharpPlus.Commands/CommandContext.cs +++ b/DSharpPlus.Commands/CommandContext.cs @@ -1,138 +1,138 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using DSharpPlus.Commands.Trees; -using DSharpPlus.Entities; - -namespace DSharpPlus.Commands; - -/// -/// Represents a base context for application command contexts. -/// -public abstract record CommandContext : AbstractContext -{ - /// - /// The command arguments. - /// - public required IReadOnlyDictionary Arguments { get; init; } - - /// - /// The followup messages sent from this interaction. - /// - public IReadOnlyDictionary FollowupMessages => this.followupMessages; - protected Dictionary followupMessages = []; - - /// - public virtual ValueTask RespondAsync(string content) => RespondAsync(new DiscordMessageBuilder().WithContent(content)); - - /// - public virtual ValueTask RespondAsync(DiscordEmbed embed) => RespondAsync(new DiscordMessageBuilder().AddEmbed(embed)); - - /// - /// Creates a response to this interaction. - /// You must create a response within 3 seconds of this interaction being executed; if the command has the potential to take more than 3 seconds, use at the start, and edit the response later. - /// - /// Content to send in the response. - /// Embed to send in the response. - public virtual ValueTask RespondAsync(string content, DiscordEmbed embed) => RespondAsync(new DiscordMessageBuilder().WithContent(content).AddEmbed(embed)); - - /// - /// The message builder. - public abstract ValueTask RespondAsync(IDiscordMessageBuilder builder); - - /// - public virtual ValueTask EditResponseAsync(string content) => EditResponseAsync(new DiscordMessageBuilder().WithContent(content)); - - /// - public virtual ValueTask EditResponseAsync(DiscordEmbed embed) => EditResponseAsync(new DiscordMessageBuilder().AddEmbed(embed)); - - /// - /// Edits the response. - /// - /// Content to send in the response. - /// Embed to send in the response. - public virtual ValueTask EditResponseAsync(string content, DiscordEmbed embed) - => EditResponseAsync(new DiscordMessageBuilder().WithContent(content).AddEmbed(embed)); - - /// - /// The message builder. - public abstract ValueTask EditResponseAsync(IDiscordMessageBuilder builder); - - /// - /// Gets the sent response. - /// - /// The sent response. - public abstract ValueTask GetResponseAsync(); - - /// - /// Creates a deferred response to this interaction. - /// - public abstract ValueTask DeferResponseAsync(); - - /// - /// Deletes the sent response. - /// - public abstract ValueTask DeleteResponseAsync(); - - /// - public virtual ValueTask FollowupAsync(string content) => FollowupAsync(new DiscordMessageBuilder().WithContent(content)); - - /// - public virtual ValueTask FollowupAsync(DiscordEmbed embed) => FollowupAsync(new DiscordMessageBuilder().AddEmbed(embed)); - - /// - /// Creates a followup message to the interaction. - /// - /// Content to send in the followup message. - /// Embed to send in the followup message. - /// The created message. - public virtual ValueTask FollowupAsync(string content, DiscordEmbed embed) - => FollowupAsync(new DiscordMessageBuilder().WithContent(content).AddEmbed(embed)); - - /// - /// The followup message to be sent. - public abstract ValueTask FollowupAsync(IDiscordMessageBuilder builder); - - /// - public virtual ValueTask EditFollowupAsync(ulong messageId, string content) - => EditFollowupAsync(messageId, new DiscordMessageBuilder().WithContent(content)); - - /// - public virtual ValueTask EditFollowupAsync(ulong messageId, DiscordEmbed embed) - => EditFollowupAsync(messageId, new DiscordMessageBuilder().AddEmbed(embed)); - - /// - /// Edits a followup message. - /// - /// The id of the followup message to edit. - /// Content to send in the followup message. - /// Embed to send in the followup message. - /// The edited message. - public virtual ValueTask EditFollowupAsync(ulong messageId, string content, DiscordEmbed embed) - => EditFollowupAsync(messageId, new DiscordMessageBuilder().WithContent(content).AddEmbed(embed)); - - /// - /// The id of the followup message to edit. - /// The message builder. - public abstract ValueTask EditFollowupAsync(ulong messageId, IDiscordMessageBuilder builder); - - /// - /// Gets a sent followup message from this interaction. - /// - /// The id of the followup message to edit. - /// Whether to ignore the cache and fetch the message from Discord. - /// The message. - public abstract ValueTask GetFollowupAsync(ulong messageId, bool ignoreCache = false); - - /// - /// Deletes a followup message sent from this interaction. - /// - /// The id of the followup message to delete. - public abstract ValueTask DeleteFollowupAsync(ulong messageId); - - /// - /// Cast this context to a different one. - /// - /// The type to cast to. - /// This context as T. - public T As() where T : CommandContext => (T)this; -} +using System.Collections.Generic; +using System.Threading.Tasks; +using DSharpPlus.Commands.Trees; +using DSharpPlus.Entities; + +namespace DSharpPlus.Commands; + +/// +/// Represents a base context for application command contexts. +/// +public abstract record CommandContext : AbstractContext +{ + /// + /// The command arguments. + /// + public required IReadOnlyDictionary Arguments { get; init; } + + /// + /// The followup messages sent from this interaction. + /// + public IReadOnlyDictionary FollowupMessages => this.followupMessages; + protected Dictionary followupMessages = []; + + /// + public virtual ValueTask RespondAsync(string content) => RespondAsync(new DiscordMessageBuilder().WithContent(content)); + + /// + public virtual ValueTask RespondAsync(DiscordEmbed embed) => RespondAsync(new DiscordMessageBuilder().AddEmbed(embed)); + + /// + /// Creates a response to this interaction. + /// You must create a response within 3 seconds of this interaction being executed; if the command has the potential to take more than 3 seconds, use at the start, and edit the response later. + /// + /// Content to send in the response. + /// Embed to send in the response. + public virtual ValueTask RespondAsync(string content, DiscordEmbed embed) => RespondAsync(new DiscordMessageBuilder().WithContent(content).AddEmbed(embed)); + + /// + /// The message builder. + public abstract ValueTask RespondAsync(IDiscordMessageBuilder builder); + + /// + public virtual ValueTask EditResponseAsync(string content) => EditResponseAsync(new DiscordMessageBuilder().WithContent(content)); + + /// + public virtual ValueTask EditResponseAsync(DiscordEmbed embed) => EditResponseAsync(new DiscordMessageBuilder().AddEmbed(embed)); + + /// + /// Edits the response. + /// + /// Content to send in the response. + /// Embed to send in the response. + public virtual ValueTask EditResponseAsync(string content, DiscordEmbed embed) + => EditResponseAsync(new DiscordMessageBuilder().WithContent(content).AddEmbed(embed)); + + /// + /// The message builder. + public abstract ValueTask EditResponseAsync(IDiscordMessageBuilder builder); + + /// + /// Gets the sent response. + /// + /// The sent response. + public abstract ValueTask GetResponseAsync(); + + /// + /// Creates a deferred response to this interaction. + /// + public abstract ValueTask DeferResponseAsync(); + + /// + /// Deletes the sent response. + /// + public abstract ValueTask DeleteResponseAsync(); + + /// + public virtual ValueTask FollowupAsync(string content) => FollowupAsync(new DiscordMessageBuilder().WithContent(content)); + + /// + public virtual ValueTask FollowupAsync(DiscordEmbed embed) => FollowupAsync(new DiscordMessageBuilder().AddEmbed(embed)); + + /// + /// Creates a followup message to the interaction. + /// + /// Content to send in the followup message. + /// Embed to send in the followup message. + /// The created message. + public virtual ValueTask FollowupAsync(string content, DiscordEmbed embed) + => FollowupAsync(new DiscordMessageBuilder().WithContent(content).AddEmbed(embed)); + + /// + /// The followup message to be sent. + public abstract ValueTask FollowupAsync(IDiscordMessageBuilder builder); + + /// + public virtual ValueTask EditFollowupAsync(ulong messageId, string content) + => EditFollowupAsync(messageId, new DiscordMessageBuilder().WithContent(content)); + + /// + public virtual ValueTask EditFollowupAsync(ulong messageId, DiscordEmbed embed) + => EditFollowupAsync(messageId, new DiscordMessageBuilder().AddEmbed(embed)); + + /// + /// Edits a followup message. + /// + /// The id of the followup message to edit. + /// Content to send in the followup message. + /// Embed to send in the followup message. + /// The edited message. + public virtual ValueTask EditFollowupAsync(ulong messageId, string content, DiscordEmbed embed) + => EditFollowupAsync(messageId, new DiscordMessageBuilder().WithContent(content).AddEmbed(embed)); + + /// + /// The id of the followup message to edit. + /// The message builder. + public abstract ValueTask EditFollowupAsync(ulong messageId, IDiscordMessageBuilder builder); + + /// + /// Gets a sent followup message from this interaction. + /// + /// The id of the followup message to edit. + /// Whether to ignore the cache and fetch the message from Discord. + /// The message. + public abstract ValueTask GetFollowupAsync(ulong messageId, bool ignoreCache = false); + + /// + /// Deletes a followup message sent from this interaction. + /// + /// The id of the followup message to delete. + public abstract ValueTask DeleteFollowupAsync(ulong messageId); + + /// + /// Cast this context to a different one. + /// + /// The type to cast to. + /// This context as T. + public T As() where T : CommandContext => (T)this; +} diff --git a/DSharpPlus.Commands/CommandsConfiguration.cs b/DSharpPlus.Commands/CommandsConfiguration.cs index c019abb573..62b1867fdf 100644 --- a/DSharpPlus.Commands/CommandsConfiguration.cs +++ b/DSharpPlus.Commands/CommandsConfiguration.cs @@ -1,34 +1,34 @@ -namespace DSharpPlus.Commands; - -/// -/// The configuration copied to an instance of . -/// -public sealed record CommandsConfiguration -{ - /// - /// The guild id to use for debugging. Leave as 0 to disable. - /// - public ulong DebugGuildId { get; set; } - - /// - /// Whether to enable the default command error handler. - /// - public bool UseDefaultCommandErrorHandler { get; set; } = true; - - /// - /// Whether to register default command processors when they're not found in the processor list. - /// - /// - /// You may still provide your own custom processors via , - /// as this configuration option will only add the default processors if they're not found in the list. - /// - public bool RegisterDefaultCommandProcessors { get; set; } = true; - - /// - /// The command executor to use for command execution. - /// - /// - /// The command executor is responsible for executing context checks, making full use of the dependency injection system, executing the command method itself, and handling errors. - /// - public ICommandExecutor CommandExecutor { get; set; } = new DefaultCommandExecutor(); -} +namespace DSharpPlus.Commands; + +/// +/// The configuration copied to an instance of . +/// +public sealed record CommandsConfiguration +{ + /// + /// The guild id to use for debugging. Leave as 0 to disable. + /// + public ulong DebugGuildId { get; set; } + + /// + /// Whether to enable the default command error handler. + /// + public bool UseDefaultCommandErrorHandler { get; set; } = true; + + /// + /// Whether to register default command processors when they're not found in the processor list. + /// + /// + /// You may still provide your own custom processors via , + /// as this configuration option will only add the default processors if they're not found in the list. + /// + public bool RegisterDefaultCommandProcessors { get; set; } = true; + + /// + /// The command executor to use for command execution. + /// + /// + /// The command executor is responsible for executing context checks, making full use of the dependency injection system, executing the command method itself, and handling errors. + /// + public ICommandExecutor CommandExecutor { get; set; } = new DefaultCommandExecutor(); +} diff --git a/DSharpPlus.Commands/CommandsExtension.cs b/DSharpPlus.Commands/CommandsExtension.cs index e4838a0e76..867b6bdc67 100644 --- a/DSharpPlus.Commands/CommandsExtension.cs +++ b/DSharpPlus.Commands/CommandsExtension.cs @@ -1,593 +1,593 @@ -using System; -using System.Collections.Frozen; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Text; -using System.Threading.Tasks; - -using DSharpPlus.AsyncEvents; -using DSharpPlus.Commands.ContextChecks; -using DSharpPlus.Commands.ContextChecks.ParameterChecks; -using DSharpPlus.Commands.EventArgs; -using DSharpPlus.Commands.Exceptions; -using DSharpPlus.Commands.Processors; -using DSharpPlus.Commands.Processors.MessageCommands; -using DSharpPlus.Commands.Processors.SlashCommands; -using DSharpPlus.Commands.Processors.TextCommands; -using DSharpPlus.Commands.Processors.TextCommands.ContextChecks; -using DSharpPlus.Commands.Processors.UserCommands; -using DSharpPlus.Commands.Trees; -using DSharpPlus.Commands.Trees.Metadata; -using DSharpPlus.Entities; -using DSharpPlus.Exceptions; - -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; - -using CheckFunc = System.Func -< - object, - DSharpPlus.Commands.ContextChecks.ContextCheckAttribute, - DSharpPlus.Commands.CommandContext, - System.Threading.Tasks.ValueTask ->; - -using ParameterCheckFunc = System.Func -< - object, - DSharpPlus.Commands.ContextChecks.ParameterChecks.ParameterCheckAttribute, - DSharpPlus.Commands.ContextChecks.ParameterChecks.ParameterCheckInfo, - DSharpPlus.Commands.CommandContext, - System.Threading.Tasks.ValueTask ->; - -namespace DSharpPlus.Commands; - -/// -/// An all in one extension for managing commands. -/// -public sealed class CommandsExtension -{ - public DiscordClient Client { get; private set; } - - /// - public IServiceProvider ServiceProvider { get; private set; } - - /// - public ulong DebugGuildId { get; init; } - - /// - public bool UseDefaultCommandErrorHandler { get; init; } - - /// - public bool RegisterDefaultCommandProcessors { get; init; } - - public ICommandExecutor CommandExecutor { get; init; } - - /// - /// The registered commands that the users can execute. - /// - public IReadOnlyDictionary Commands { get; private set; } = new Dictionary(); - private readonly List commandBuilders = []; - - /// - /// All registered command processors. - /// - public IReadOnlyDictionary Processors => this.processors; - private readonly Dictionary processors = []; - - public IReadOnlyList Checks => this.checks; - private readonly List checks = []; - - public IReadOnlyList ParameterChecks => this.parameterChecks; - private readonly List parameterChecks = []; - - /// - /// Executed everytime a command is finished executing. - /// - public event AsyncEventHandler CommandExecuted - { - add => this.commandExecuted.Register(value); - remove => this.commandExecuted.Unregister(value); - } - - internal AsyncEvent commandExecuted; - - /// - /// Executed everytime a command has errored. - /// - public event AsyncEventHandler CommandErrored - { - add => this.commandErrored.Register(value); - remove => this.commandErrored.Unregister(value); - } - - internal AsyncEvent commandErrored; - - /// - /// Executed before commands are finalized into a read-only state. - /// - /// - /// Apply any mass-mutations to the commands or command parameters here. - /// - public event AsyncEventHandler ConfiguringCommands - { - add => this.configuringCommands.Register(value); - remove => this.configuringCommands.Unregister(value); - } - - private AsyncEvent configuringCommands; - - /// - /// Used to log messages from this extension. - /// - private ILogger logger; - - /// - /// Creates a new instance of the class. - /// - /// The configuration to use. - internal CommandsExtension(CommandsConfiguration configuration) - { - ArgumentNullException.ThrowIfNull(configuration); - - this.DebugGuildId = configuration.DebugGuildId; - this.UseDefaultCommandErrorHandler = configuration.UseDefaultCommandErrorHandler; - this.RegisterDefaultCommandProcessors = configuration.RegisterDefaultCommandProcessors; - this.CommandExecutor = configuration.CommandExecutor; - } - - /// - /// Sets up the extension to use the specified . - /// - /// The client to register our event handlers too. - public void Setup(DiscordClient client) - { - if (client is null) - { - throw new ArgumentNullException(nameof(client)); - } - else if (this.Client is not null) - { - throw new InvalidOperationException("Commands Extension is already initialized."); - } - - this.Client = client; - this.ServiceProvider = client.ServiceProvider; - this.logger = client.ServiceProvider.GetRequiredService>(); - - DefaultClientErrorHandler errorHandler = new(client.Logger); - this.commandErrored = new(errorHandler); - this.commandExecuted = new(errorHandler); - this.configuringCommands = new(errorHandler); - - // TODO: Move this to the IEventHandler system so the Commands namespace - // will have zero awareness of built-in command processors. - this.configuringCommands.Register(SlashCommandProcessor.ConfigureCommands); - if (this.UseDefaultCommandErrorHandler) - { - this.CommandErrored += DefaultCommandErrorHandlerAsync; - } - - AddCheck(); - AddCheck(); - AddCheck(); - AddCheck(); - AddCheck(); - AddCheck(); - - AddParameterCheck(); - AddParameterCheck(); - AddParameterCheck(); - AddParameterCheck(); - } - - public void AddCommand(CommandBuilder command) => this.commandBuilders.Add(command); - public void AddCommand(Delegate commandDelegate, params ulong[] guildIds) => this.commandBuilders.Add(CommandBuilder.From(commandDelegate, guildIds)); - public void AddCommand(Delegate commandDelegate) => this.commandBuilders.Add(CommandBuilder.From(commandDelegate)); - public void AddCommand(Type type, params ulong[] guildIds) => this.commandBuilders.Add(CommandBuilder.From(type, guildIds)); - public void AddCommand(Type type) => this.commandBuilders.Add(CommandBuilder.From(type)); - - // !type.IsNested || type.DeclaringType?.GetCustomAttribute() is null - // This is done to prevent nested classes from being added as commands, while still allowing non-command classes containing commands to be added. - // See https://github.com/DSharpPlus/DSharpPlus/pull/2273#discussion_r2009114568 for more information. - public void AddCommands(Assembly assembly, params ulong[] guildIds) => AddCommands(assembly.GetTypes().Where(type => - !type.IsNested || type.DeclaringType?.GetCustomAttribute() is null), guildIds); - - public void AddCommands(Assembly assembly) => AddCommands(assembly.GetTypes().Where(type => - !type.IsNested || type.DeclaringType?.GetCustomAttribute() is null)); - - public void AddCommands(IEnumerable commands) => this.commandBuilders.AddRange(commands); - public void AddCommands(IEnumerable types) => AddCommands(types, []); - public void AddCommands(params CommandBuilder[] commands) => this.commandBuilders.AddRange(commands); - public void AddCommands(Type type, params ulong[] guildIds) => AddCommands([type], guildIds); - public void AddCommands(Type type) => AddCommands([type]); - public void AddCommands() => AddCommands([typeof(T)]); - public void AddCommands(params ulong[] guildIds) => AddCommands([typeof(T)], guildIds); - public void AddCommands(IEnumerable types, params ulong[] guildIds) - { - foreach (Type type in types) - { - if (type.GetCustomAttribute() is not null) - { - this.Client.Logger.LogDebug("Adding command from type {Type}", type.FullName ?? type.Name); - this.commandBuilders.Add(CommandBuilder.From(type, guildIds)); - continue; - } - - foreach (MethodInfo method in type.GetMethods()) - { - if (method.GetCustomAttribute() is not null) - { - this.Client.Logger.LogDebug("Adding command from type {Type}", type.FullName ?? type.Name); - this.commandBuilders.Add(CommandBuilder.From(method, guildIds: guildIds)); - } - } - } - } - - /// - /// Gets a list of commands filtered for a specific command processor - /// - /// Processor which is calling this method - /// Returns a list of valid commands. This list can be empty if no commands are valid for this processor type - public IReadOnlyList GetCommandsForProcessor(ICommandProcessor processor) - { - // Those processors use a different attribute to filter and filter themself - if (processor is MessageCommandProcessor or UserCommandProcessor) - { - return this.Commands.Values.ToList(); - } - - Type contextType = processor.ContextType; - Type processorType = processor.GetType(); - List commands = new(this.Commands.Values.Count()); - foreach (Command command in this.Commands.Values) - { - Command? filteredCommand = FilterCommand(command, processorType, contextType); - if (filteredCommand is not null) - { - commands.Add(filteredCommand); - } - } - - return commands; - } - - private Command? FilterCommand(Command command, Type processorType, Type contextType) - { - AllowedProcessorsAttribute? allowedProcessorsAttribute = command.Attributes.OfType().FirstOrDefault(); - if (allowedProcessorsAttribute is not null && !allowedProcessorsAttribute.Processors.Contains(processorType)) - { - return null; - } - else if (command.Method is not null) - { - Type methodContextType = command.Method.GetParameters().First().ParameterType; - if (!methodContextType.IsAssignableTo(contextType) && methodContextType != typeof(CommandContext)) - { - return null; - } - } - - List subcommands = new(command.Subcommands.Count); - foreach (Command subcommand in command.Subcommands) - { - Command? filteredSubcommand = FilterCommand(subcommand, processorType, contextType); - if (filteredSubcommand is not null) - { - subcommands.Add(filteredSubcommand); - } - } - - return command with - { - Subcommands = subcommands, - }; - } - - public void AddProcessor(ICommandProcessor processor) => this.processors.Add(processor.GetType(), processor); - public void AddProcessor() where TProcessor : ICommandProcessor, new() => AddProcessor(new TProcessor()); - public void AddProcessors(params ICommandProcessor[] processors) => AddProcessors((IEnumerable)processors); - public void AddProcessors(IEnumerable processors) - { - foreach (ICommandProcessor processor in processors) - { - AddProcessor(processor); - } - } - - public TProcessor GetProcessor() where TProcessor : ICommandProcessor => (TProcessor)this.processors[typeof(TProcessor)]; - public bool TryGetProcessor([NotNullWhen(true)] out TProcessor? processor) where TProcessor : ICommandProcessor - { - if (this.processors.TryGetValue(typeof(TProcessor), out ICommandProcessor? baseProcessor)) - { - processor = (TProcessor)baseProcessor; - return true; - } - - processor = default; - return false; - } - - /// - /// Adds all public checks from the provided assembly to the extension. - /// - public void AddChecks(Assembly assembly) - { - foreach (Type t in assembly.GetTypes()) - { - if (t.GetInterface("DSharpPlus.Commands.ContextChecks.IContextCheck`1") is not null) - { - AddCheck(t); - } - } - } - - /// - /// Adds a new check to the extension. - /// - public void AddCheck() where T : IContextCheck => AddCheck(typeof(T)); - - /// - /// Adds a new check to the extension. - /// - public void AddCheck(Type checkType) - { - // get all implemented check interfaces, we can pretty easily handle having multiple checks in one type - foreach (Type t in checkType.GetInterfaces()) - { - if (t.Namespace != "DSharpPlus.Commands.ContextChecks" || t.Name != "IContextCheck`1") - { - continue; - } - - Type attributeType = t.GetGenericArguments()[0]; - MethodInfo method = checkType - .GetMethods(BindingFlags.Public | BindingFlags.Instance) - .First(x => x.Name == "ExecuteCheckAsync" && x.GetParameters()[0].ParameterType == attributeType); - - // create the func for invoking the check here, during startup - ParameterExpression check = Expression.Parameter(checkType); - ParameterExpression attribute = Expression.Parameter(attributeType); - ParameterExpression context = Expression.Parameter(typeof(CommandContext)); - MethodCallExpression call = Expression.Call( - instance: check, - method: method, - arg0: attribute, - arg1: context - ); - - Type delegateType = typeof(Func<,,,>).MakeGenericType( - checkType, - attributeType, - typeof(CommandContext), - typeof(ValueTask) - ); - - CheckFunc func = Unsafe.As(Expression.Lambda(delegateType, call, check, attribute, context).Compile()); - this.checks.Add(new() - { - AttributeType = attributeType, - CheckType = checkType, - ExecuteCheckAsync = func, - }); - } - } - - /// - /// Adds all parameter checks from the provided assembly to the extension. - /// - public void AddParameterChecks(Assembly assembly) - { - foreach (Type t in assembly.GetTypes()) - { - if (t.GetInterface("DSharpPlus.Commands.ContextChecks.ParameterChecks.IParameterCheck`1") is not null) - { - AddParameterCheck(t); - } - } - } - - /// - /// Adds a new check to the extension. - /// - public void AddParameterCheck() where T : IParameterCheck => AddParameterCheck(typeof(T)); - - /// - /// Adds a new check to the extension. - /// - public void AddParameterCheck(Type checkType) - { - // get all implemented check interfaces, we can pretty easily handle having multiple checks in one type - foreach (Type t in checkType.GetInterfaces()) - { - if (t.Namespace != "DSharpPlus.Commands.ContextChecks.ParameterChecks" || t.Name != "IParameterCheck`1") - { - continue; - } - - Type attributeType = t.GetGenericArguments()[0]; - MethodInfo method = checkType - .GetMethods(BindingFlags.Public | BindingFlags.Instance) - .First(x => x.Name == "ExecuteCheckAsync" && x.GetParameters()[0].ParameterType == attributeType); - - // create the func for invoking the check here, during startup - ParameterExpression check = Expression.Parameter(checkType); - ParameterExpression attribute = Expression.Parameter(attributeType); - ParameterExpression info = Expression.Parameter(typeof(ParameterCheckInfo)); - ParameterExpression context = Expression.Parameter(typeof(CommandContext)); - MethodCallExpression call = Expression.Call( - instance: check, - method: method, - arg0: attribute, - arg1: info, - arg2: context - ); - - Type delegateType = typeof(Func<,,,,>).MakeGenericType( - checkType, - attributeType, - typeof(ParameterCheckInfo), - typeof(CommandContext), - typeof(ValueTask) - ); - - ParameterCheckFunc func = Unsafe.As(Expression.Lambda(delegateType, call, check, attribute, info, context).Compile()); - this.parameterChecks.Add( - new() - { - AttributeType = attributeType, - CheckType = checkType, - ExecuteCheckAsync = func, - } - ); - } - } - - public async Task RefreshAsync() - { - await BuildCommandsAsync(); - - if (this.RegisterDefaultCommandProcessors) - { - this.processors.TryAdd(typeof(TextCommandProcessor), new TextCommandProcessor()); - this.processors.TryAdd(typeof(SlashCommandProcessor), new SlashCommandProcessor()); - this.processors.TryAdd(typeof(MessageCommandProcessor), new MessageCommandProcessor()); - this.processors.TryAdd(typeof(UserCommandProcessor), new UserCommandProcessor()); - } - - if (this.processors.TryGetValue(typeof(UserCommandProcessor), out ICommandProcessor? userProcessor)) - { - await userProcessor.ConfigureAsync(this); - } - - if (this.processors.TryGetValue(typeof(MessageCommandProcessor), out ICommandProcessor? messageProcessor)) - { - await messageProcessor.ConfigureAsync(this); - } - - foreach (ICommandProcessor processor in this.processors.Values) - { - Type type = processor.GetType(); - if (type == typeof(UserCommandProcessor) || type == typeof(MessageCommandProcessor)) - { - continue; - } - - await processor.ConfigureAsync(this); - } - } - - internal async ValueTask BuildCommandsAsync() - { - await this.configuringCommands.InvokeAsync(this, new ConfigureCommandsEventArgs() { CommandTrees = this.commandBuilders }); - - Dictionary commands = []; - foreach (CommandBuilder commandBuilder in this.commandBuilders) - { - try - { - Command command = commandBuilder.Build(); - commands.Add(command.Name, command); - } - catch (Exception error) - { - this.logger.LogError(error, "Failed to build command '{CommandBuilder}'", commandBuilder.FullName); - } - } - - this.Commands = commands.ToFrozenDictionary(); - } - - /// - /// The default command error handler. Only used if is set to true. - /// - /// The extension. - /// The event arguments containing the exception. - private static async Task DefaultCommandErrorHandlerAsync(CommandsExtension extension, CommandErroredEventArgs eventArgs) - { - StringBuilder stringBuilder = new(); - DiscordMessageBuilder messageBuilder = new(); - - // Error message - stringBuilder.Append(eventArgs.Exception switch - { - CommandNotFoundException commandNotFoundException => $"Command ``{commandNotFoundException.CommandName}`` was not found.", - CommandRegistrationFailedException => $"Application commands failed to register.", - ArgumentParseException argumentParseException when argumentParseException.ConversionResult?.Value is not null => - $"Failed to parse argument ``{argumentParseException.Parameter.Name}``: ``{argumentParseException.ConversionResult.Value.ToString() ?? ""}`` is not a valid value. {argumentParseException.Message}", - ArgumentParseException argumentParseException => - $"Failed to parse argument ``{argumentParseException.Parameter.Name}``: {argumentParseException.Message}", - ChecksFailedException checksFailedException when checksFailedException.Errors.Count == 1 => - $"The following error occurred: ``{checksFailedException.Errors[0].ErrorMessage}``", - ChecksFailedException checksFailedException => - $"The following context checks failed: ```\n{string.Join("\n- ", checksFailedException.Errors.Select(x => x.ErrorMessage)).Trim()}\n```.", - ParameterChecksFailedException checksFailedException when checksFailedException.Errors.Count == 1 => - $"The following error occurred: ``{checksFailedException.Errors[0].ErrorMessage}``", - ParameterChecksFailedException checksFailedException => - $"The following context checks failed: ```\n{string.Join("\n- ", checksFailedException.Errors.Select(x => x.ErrorMessage)).Trim()}\n```.", - DiscordException discordException when discordException.Response is not null && (int)discordException.Response.StatusCode >= 500 && (int)discordException.Response.StatusCode < 600 => - $"Discord API error {discordException.Response.StatusCode} occurred: {discordException.JsonMessage ?? "No further information was provided."}", - DiscordException discordException when discordException.Response is not null => - $"Discord API error {discordException.Response.StatusCode} occurred: {discordException.JsonMessage ?? discordException.Message}", - _ => $"An unexpected error occurred: {eventArgs.Exception.Message}", - }); - - // Stack trace - if (!string.IsNullOrWhiteSpace(eventArgs.Exception.StackTrace)) - { - // If the stack trace can fit inside a codeblock - if (8 + eventArgs.Exception.StackTrace.Length + stringBuilder.Length <= 2000) - { - stringBuilder.Append($"```\n{eventArgs.Exception.StackTrace}\n```"); - messageBuilder.WithContent(stringBuilder.ToString()); - } - // If the exception message exceeds the message character limit, cram it all into an attatched file with a simple message in the content. - else if (stringBuilder.Length >= 2000) - { - messageBuilder.WithContent( - "Exception Message exceeds character limit, see attached file." - ); - string formattedFile = - $"{stringBuilder}{Environment.NewLine}{Environment.NewLine}Stack Trace:{Environment.NewLine}{eventArgs.Exception.StackTrace}"; - messageBuilder.AddFile( - "MessageAndStackTrace.txt", - new MemoryStream(Encoding.UTF8.GetBytes(formattedFile)), - AddFileOptions.CloseStream - ); - } - // Otherwise, display the exception message in the content and the trace in an attached file - else - { - messageBuilder.WithContent(stringBuilder.ToString()); - messageBuilder.AddFile("StackTrace.txt", new MemoryStream(Encoding.UTF8.GetBytes(eventArgs.Exception.StackTrace)), AddFileOptions.CloseStream); - } - } - // If no stack trace, and the message is still too long, attatch a file with the message and use a simple message in the content. - else if (stringBuilder.Length >= 2000) - { - messageBuilder.WithContent("Exception Message exceeds character limit, see attached file."); - messageBuilder.AddFile("Message.txt", new MemoryStream(Encoding.UTF8.GetBytes(stringBuilder.ToString())), AddFileOptions.CloseStream); - } - // Otherwise, if no stack trace and the Exception message will fit, send the message as content - else - { - messageBuilder.WithContent(stringBuilder.ToString()); - } - - if (eventArgs.Context is SlashCommandContext { Interaction.ResponseState: not DiscordInteractionResponseState.Unacknowledged }) - { - await eventArgs.Context.FollowupAsync(messageBuilder); - } - else - { - await eventArgs.Context.RespondAsync(messageBuilder); - } - } -} +using System; +using System.Collections.Frozen; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading.Tasks; + +using DSharpPlus.AsyncEvents; +using DSharpPlus.Commands.ContextChecks; +using DSharpPlus.Commands.ContextChecks.ParameterChecks; +using DSharpPlus.Commands.EventArgs; +using DSharpPlus.Commands.Exceptions; +using DSharpPlus.Commands.Processors; +using DSharpPlus.Commands.Processors.MessageCommands; +using DSharpPlus.Commands.Processors.SlashCommands; +using DSharpPlus.Commands.Processors.TextCommands; +using DSharpPlus.Commands.Processors.TextCommands.ContextChecks; +using DSharpPlus.Commands.Processors.UserCommands; +using DSharpPlus.Commands.Trees; +using DSharpPlus.Commands.Trees.Metadata; +using DSharpPlus.Entities; +using DSharpPlus.Exceptions; + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +using CheckFunc = System.Func +< + object, + DSharpPlus.Commands.ContextChecks.ContextCheckAttribute, + DSharpPlus.Commands.CommandContext, + System.Threading.Tasks.ValueTask +>; + +using ParameterCheckFunc = System.Func +< + object, + DSharpPlus.Commands.ContextChecks.ParameterChecks.ParameterCheckAttribute, + DSharpPlus.Commands.ContextChecks.ParameterChecks.ParameterCheckInfo, + DSharpPlus.Commands.CommandContext, + System.Threading.Tasks.ValueTask +>; + +namespace DSharpPlus.Commands; + +/// +/// An all in one extension for managing commands. +/// +public sealed class CommandsExtension +{ + public DiscordClient Client { get; private set; } + + /// + public IServiceProvider ServiceProvider { get; private set; } + + /// + public ulong DebugGuildId { get; init; } + + /// + public bool UseDefaultCommandErrorHandler { get; init; } + + /// + public bool RegisterDefaultCommandProcessors { get; init; } + + public ICommandExecutor CommandExecutor { get; init; } + + /// + /// The registered commands that the users can execute. + /// + public IReadOnlyDictionary Commands { get; private set; } = new Dictionary(); + private readonly List commandBuilders = []; + + /// + /// All registered command processors. + /// + public IReadOnlyDictionary Processors => this.processors; + private readonly Dictionary processors = []; + + public IReadOnlyList Checks => this.checks; + private readonly List checks = []; + + public IReadOnlyList ParameterChecks => this.parameterChecks; + private readonly List parameterChecks = []; + + /// + /// Executed everytime a command is finished executing. + /// + public event AsyncEventHandler CommandExecuted + { + add => this.commandExecuted.Register(value); + remove => this.commandExecuted.Unregister(value); + } + + internal AsyncEvent commandExecuted; + + /// + /// Executed everytime a command has errored. + /// + public event AsyncEventHandler CommandErrored + { + add => this.commandErrored.Register(value); + remove => this.commandErrored.Unregister(value); + } + + internal AsyncEvent commandErrored; + + /// + /// Executed before commands are finalized into a read-only state. + /// + /// + /// Apply any mass-mutations to the commands or command parameters here. + /// + public event AsyncEventHandler ConfiguringCommands + { + add => this.configuringCommands.Register(value); + remove => this.configuringCommands.Unregister(value); + } + + private AsyncEvent configuringCommands; + + /// + /// Used to log messages from this extension. + /// + private ILogger logger; + + /// + /// Creates a new instance of the class. + /// + /// The configuration to use. + internal CommandsExtension(CommandsConfiguration configuration) + { + ArgumentNullException.ThrowIfNull(configuration); + + this.DebugGuildId = configuration.DebugGuildId; + this.UseDefaultCommandErrorHandler = configuration.UseDefaultCommandErrorHandler; + this.RegisterDefaultCommandProcessors = configuration.RegisterDefaultCommandProcessors; + this.CommandExecutor = configuration.CommandExecutor; + } + + /// + /// Sets up the extension to use the specified . + /// + /// The client to register our event handlers too. + public void Setup(DiscordClient client) + { + if (client is null) + { + throw new ArgumentNullException(nameof(client)); + } + else if (this.Client is not null) + { + throw new InvalidOperationException("Commands Extension is already initialized."); + } + + this.Client = client; + this.ServiceProvider = client.ServiceProvider; + this.logger = client.ServiceProvider.GetRequiredService>(); + + DefaultClientErrorHandler errorHandler = new(client.Logger); + this.commandErrored = new(errorHandler); + this.commandExecuted = new(errorHandler); + this.configuringCommands = new(errorHandler); + + // TODO: Move this to the IEventHandler system so the Commands namespace + // will have zero awareness of built-in command processors. + this.configuringCommands.Register(SlashCommandProcessor.ConfigureCommands); + if (this.UseDefaultCommandErrorHandler) + { + this.CommandErrored += DefaultCommandErrorHandlerAsync; + } + + AddCheck(); + AddCheck(); + AddCheck(); + AddCheck(); + AddCheck(); + AddCheck(); + + AddParameterCheck(); + AddParameterCheck(); + AddParameterCheck(); + AddParameterCheck(); + } + + public void AddCommand(CommandBuilder command) => this.commandBuilders.Add(command); + public void AddCommand(Delegate commandDelegate, params ulong[] guildIds) => this.commandBuilders.Add(CommandBuilder.From(commandDelegate, guildIds)); + public void AddCommand(Delegate commandDelegate) => this.commandBuilders.Add(CommandBuilder.From(commandDelegate)); + public void AddCommand(Type type, params ulong[] guildIds) => this.commandBuilders.Add(CommandBuilder.From(type, guildIds)); + public void AddCommand(Type type) => this.commandBuilders.Add(CommandBuilder.From(type)); + + // !type.IsNested || type.DeclaringType?.GetCustomAttribute() is null + // This is done to prevent nested classes from being added as commands, while still allowing non-command classes containing commands to be added. + // See https://github.com/DSharpPlus/DSharpPlus/pull/2273#discussion_r2009114568 for more information. + public void AddCommands(Assembly assembly, params ulong[] guildIds) => AddCommands(assembly.GetTypes().Where(type => + !type.IsNested || type.DeclaringType?.GetCustomAttribute() is null), guildIds); + + public void AddCommands(Assembly assembly) => AddCommands(assembly.GetTypes().Where(type => + !type.IsNested || type.DeclaringType?.GetCustomAttribute() is null)); + + public void AddCommands(IEnumerable commands) => this.commandBuilders.AddRange(commands); + public void AddCommands(IEnumerable types) => AddCommands(types, []); + public void AddCommands(params CommandBuilder[] commands) => this.commandBuilders.AddRange(commands); + public void AddCommands(Type type, params ulong[] guildIds) => AddCommands([type], guildIds); + public void AddCommands(Type type) => AddCommands([type]); + public void AddCommands() => AddCommands([typeof(T)]); + public void AddCommands(params ulong[] guildIds) => AddCommands([typeof(T)], guildIds); + public void AddCommands(IEnumerable types, params ulong[] guildIds) + { + foreach (Type type in types) + { + if (type.GetCustomAttribute() is not null) + { + this.Client.Logger.LogDebug("Adding command from type {Type}", type.FullName ?? type.Name); + this.commandBuilders.Add(CommandBuilder.From(type, guildIds)); + continue; + } + + foreach (MethodInfo method in type.GetMethods()) + { + if (method.GetCustomAttribute() is not null) + { + this.Client.Logger.LogDebug("Adding command from type {Type}", type.FullName ?? type.Name); + this.commandBuilders.Add(CommandBuilder.From(method, guildIds: guildIds)); + } + } + } + } + + /// + /// Gets a list of commands filtered for a specific command processor + /// + /// Processor which is calling this method + /// Returns a list of valid commands. This list can be empty if no commands are valid for this processor type + public IReadOnlyList GetCommandsForProcessor(ICommandProcessor processor) + { + // Those processors use a different attribute to filter and filter themself + if (processor is MessageCommandProcessor or UserCommandProcessor) + { + return this.Commands.Values.ToList(); + } + + Type contextType = processor.ContextType; + Type processorType = processor.GetType(); + List commands = new(this.Commands.Values.Count()); + foreach (Command command in this.Commands.Values) + { + Command? filteredCommand = FilterCommand(command, processorType, contextType); + if (filteredCommand is not null) + { + commands.Add(filteredCommand); + } + } + + return commands; + } + + private Command? FilterCommand(Command command, Type processorType, Type contextType) + { + AllowedProcessorsAttribute? allowedProcessorsAttribute = command.Attributes.OfType().FirstOrDefault(); + if (allowedProcessorsAttribute is not null && !allowedProcessorsAttribute.Processors.Contains(processorType)) + { + return null; + } + else if (command.Method is not null) + { + Type methodContextType = command.Method.GetParameters().First().ParameterType; + if (!methodContextType.IsAssignableTo(contextType) && methodContextType != typeof(CommandContext)) + { + return null; + } + } + + List subcommands = new(command.Subcommands.Count); + foreach (Command subcommand in command.Subcommands) + { + Command? filteredSubcommand = FilterCommand(subcommand, processorType, contextType); + if (filteredSubcommand is not null) + { + subcommands.Add(filteredSubcommand); + } + } + + return command with + { + Subcommands = subcommands, + }; + } + + public void AddProcessor(ICommandProcessor processor) => this.processors.Add(processor.GetType(), processor); + public void AddProcessor() where TProcessor : ICommandProcessor, new() => AddProcessor(new TProcessor()); + public void AddProcessors(params ICommandProcessor[] processors) => AddProcessors((IEnumerable)processors); + public void AddProcessors(IEnumerable processors) + { + foreach (ICommandProcessor processor in processors) + { + AddProcessor(processor); + } + } + + public TProcessor GetProcessor() where TProcessor : ICommandProcessor => (TProcessor)this.processors[typeof(TProcessor)]; + public bool TryGetProcessor([NotNullWhen(true)] out TProcessor? processor) where TProcessor : ICommandProcessor + { + if (this.processors.TryGetValue(typeof(TProcessor), out ICommandProcessor? baseProcessor)) + { + processor = (TProcessor)baseProcessor; + return true; + } + + processor = default; + return false; + } + + /// + /// Adds all public checks from the provided assembly to the extension. + /// + public void AddChecks(Assembly assembly) + { + foreach (Type t in assembly.GetTypes()) + { + if (t.GetInterface("DSharpPlus.Commands.ContextChecks.IContextCheck`1") is not null) + { + AddCheck(t); + } + } + } + + /// + /// Adds a new check to the extension. + /// + public void AddCheck() where T : IContextCheck => AddCheck(typeof(T)); + + /// + /// Adds a new check to the extension. + /// + public void AddCheck(Type checkType) + { + // get all implemented check interfaces, we can pretty easily handle having multiple checks in one type + foreach (Type t in checkType.GetInterfaces()) + { + if (t.Namespace != "DSharpPlus.Commands.ContextChecks" || t.Name != "IContextCheck`1") + { + continue; + } + + Type attributeType = t.GetGenericArguments()[0]; + MethodInfo method = checkType + .GetMethods(BindingFlags.Public | BindingFlags.Instance) + .First(x => x.Name == "ExecuteCheckAsync" && x.GetParameters()[0].ParameterType == attributeType); + + // create the func for invoking the check here, during startup + ParameterExpression check = Expression.Parameter(checkType); + ParameterExpression attribute = Expression.Parameter(attributeType); + ParameterExpression context = Expression.Parameter(typeof(CommandContext)); + MethodCallExpression call = Expression.Call( + instance: check, + method: method, + arg0: attribute, + arg1: context + ); + + Type delegateType = typeof(Func<,,,>).MakeGenericType( + checkType, + attributeType, + typeof(CommandContext), + typeof(ValueTask) + ); + + CheckFunc func = Unsafe.As(Expression.Lambda(delegateType, call, check, attribute, context).Compile()); + this.checks.Add(new() + { + AttributeType = attributeType, + CheckType = checkType, + ExecuteCheckAsync = func, + }); + } + } + + /// + /// Adds all parameter checks from the provided assembly to the extension. + /// + public void AddParameterChecks(Assembly assembly) + { + foreach (Type t in assembly.GetTypes()) + { + if (t.GetInterface("DSharpPlus.Commands.ContextChecks.ParameterChecks.IParameterCheck`1") is not null) + { + AddParameterCheck(t); + } + } + } + + /// + /// Adds a new check to the extension. + /// + public void AddParameterCheck() where T : IParameterCheck => AddParameterCheck(typeof(T)); + + /// + /// Adds a new check to the extension. + /// + public void AddParameterCheck(Type checkType) + { + // get all implemented check interfaces, we can pretty easily handle having multiple checks in one type + foreach (Type t in checkType.GetInterfaces()) + { + if (t.Namespace != "DSharpPlus.Commands.ContextChecks.ParameterChecks" || t.Name != "IParameterCheck`1") + { + continue; + } + + Type attributeType = t.GetGenericArguments()[0]; + MethodInfo method = checkType + .GetMethods(BindingFlags.Public | BindingFlags.Instance) + .First(x => x.Name == "ExecuteCheckAsync" && x.GetParameters()[0].ParameterType == attributeType); + + // create the func for invoking the check here, during startup + ParameterExpression check = Expression.Parameter(checkType); + ParameterExpression attribute = Expression.Parameter(attributeType); + ParameterExpression info = Expression.Parameter(typeof(ParameterCheckInfo)); + ParameterExpression context = Expression.Parameter(typeof(CommandContext)); + MethodCallExpression call = Expression.Call( + instance: check, + method: method, + arg0: attribute, + arg1: info, + arg2: context + ); + + Type delegateType = typeof(Func<,,,,>).MakeGenericType( + checkType, + attributeType, + typeof(ParameterCheckInfo), + typeof(CommandContext), + typeof(ValueTask) + ); + + ParameterCheckFunc func = Unsafe.As(Expression.Lambda(delegateType, call, check, attribute, info, context).Compile()); + this.parameterChecks.Add( + new() + { + AttributeType = attributeType, + CheckType = checkType, + ExecuteCheckAsync = func, + } + ); + } + } + + public async Task RefreshAsync() + { + await BuildCommandsAsync(); + + if (this.RegisterDefaultCommandProcessors) + { + this.processors.TryAdd(typeof(TextCommandProcessor), new TextCommandProcessor()); + this.processors.TryAdd(typeof(SlashCommandProcessor), new SlashCommandProcessor()); + this.processors.TryAdd(typeof(MessageCommandProcessor), new MessageCommandProcessor()); + this.processors.TryAdd(typeof(UserCommandProcessor), new UserCommandProcessor()); + } + + if (this.processors.TryGetValue(typeof(UserCommandProcessor), out ICommandProcessor? userProcessor)) + { + await userProcessor.ConfigureAsync(this); + } + + if (this.processors.TryGetValue(typeof(MessageCommandProcessor), out ICommandProcessor? messageProcessor)) + { + await messageProcessor.ConfigureAsync(this); + } + + foreach (ICommandProcessor processor in this.processors.Values) + { + Type type = processor.GetType(); + if (type == typeof(UserCommandProcessor) || type == typeof(MessageCommandProcessor)) + { + continue; + } + + await processor.ConfigureAsync(this); + } + } + + internal async ValueTask BuildCommandsAsync() + { + await this.configuringCommands.InvokeAsync(this, new ConfigureCommandsEventArgs() { CommandTrees = this.commandBuilders }); + + Dictionary commands = []; + foreach (CommandBuilder commandBuilder in this.commandBuilders) + { + try + { + Command command = commandBuilder.Build(); + commands.Add(command.Name, command); + } + catch (Exception error) + { + this.logger.LogError(error, "Failed to build command '{CommandBuilder}'", commandBuilder.FullName); + } + } + + this.Commands = commands.ToFrozenDictionary(); + } + + /// + /// The default command error handler. Only used if is set to true. + /// + /// The extension. + /// The event arguments containing the exception. + private static async Task DefaultCommandErrorHandlerAsync(CommandsExtension extension, CommandErroredEventArgs eventArgs) + { + StringBuilder stringBuilder = new(); + DiscordMessageBuilder messageBuilder = new(); + + // Error message + stringBuilder.Append(eventArgs.Exception switch + { + CommandNotFoundException commandNotFoundException => $"Command ``{commandNotFoundException.CommandName}`` was not found.", + CommandRegistrationFailedException => $"Application commands failed to register.", + ArgumentParseException argumentParseException when argumentParseException.ConversionResult?.Value is not null => + $"Failed to parse argument ``{argumentParseException.Parameter.Name}``: ``{argumentParseException.ConversionResult.Value.ToString() ?? ""}`` is not a valid value. {argumentParseException.Message}", + ArgumentParseException argumentParseException => + $"Failed to parse argument ``{argumentParseException.Parameter.Name}``: {argumentParseException.Message}", + ChecksFailedException checksFailedException when checksFailedException.Errors.Count == 1 => + $"The following error occurred: ``{checksFailedException.Errors[0].ErrorMessage}``", + ChecksFailedException checksFailedException => + $"The following context checks failed: ```\n{string.Join("\n- ", checksFailedException.Errors.Select(x => x.ErrorMessage)).Trim()}\n```.", + ParameterChecksFailedException checksFailedException when checksFailedException.Errors.Count == 1 => + $"The following error occurred: ``{checksFailedException.Errors[0].ErrorMessage}``", + ParameterChecksFailedException checksFailedException => + $"The following context checks failed: ```\n{string.Join("\n- ", checksFailedException.Errors.Select(x => x.ErrorMessage)).Trim()}\n```.", + DiscordException discordException when discordException.Response is not null && (int)discordException.Response.StatusCode >= 500 && (int)discordException.Response.StatusCode < 600 => + $"Discord API error {discordException.Response.StatusCode} occurred: {discordException.JsonMessage ?? "No further information was provided."}", + DiscordException discordException when discordException.Response is not null => + $"Discord API error {discordException.Response.StatusCode} occurred: {discordException.JsonMessage ?? discordException.Message}", + _ => $"An unexpected error occurred: {eventArgs.Exception.Message}", + }); + + // Stack trace + if (!string.IsNullOrWhiteSpace(eventArgs.Exception.StackTrace)) + { + // If the stack trace can fit inside a codeblock + if (8 + eventArgs.Exception.StackTrace.Length + stringBuilder.Length <= 2000) + { + stringBuilder.Append($"```\n{eventArgs.Exception.StackTrace}\n```"); + messageBuilder.WithContent(stringBuilder.ToString()); + } + // If the exception message exceeds the message character limit, cram it all into an attatched file with a simple message in the content. + else if (stringBuilder.Length >= 2000) + { + messageBuilder.WithContent( + "Exception Message exceeds character limit, see attached file." + ); + string formattedFile = + $"{stringBuilder}{Environment.NewLine}{Environment.NewLine}Stack Trace:{Environment.NewLine}{eventArgs.Exception.StackTrace}"; + messageBuilder.AddFile( + "MessageAndStackTrace.txt", + new MemoryStream(Encoding.UTF8.GetBytes(formattedFile)), + AddFileOptions.CloseStream + ); + } + // Otherwise, display the exception message in the content and the trace in an attached file + else + { + messageBuilder.WithContent(stringBuilder.ToString()); + messageBuilder.AddFile("StackTrace.txt", new MemoryStream(Encoding.UTF8.GetBytes(eventArgs.Exception.StackTrace)), AddFileOptions.CloseStream); + } + } + // If no stack trace, and the message is still too long, attatch a file with the message and use a simple message in the content. + else if (stringBuilder.Length >= 2000) + { + messageBuilder.WithContent("Exception Message exceeds character limit, see attached file."); + messageBuilder.AddFile("Message.txt", new MemoryStream(Encoding.UTF8.GetBytes(stringBuilder.ToString())), AddFileOptions.CloseStream); + } + // Otherwise, if no stack trace and the Exception message will fit, send the message as content + else + { + messageBuilder.WithContent(stringBuilder.ToString()); + } + + if (eventArgs.Context is SlashCommandContext { Interaction.ResponseState: not DiscordInteractionResponseState.Unacknowledged }) + { + await eventArgs.Context.FollowupAsync(messageBuilder); + } + else + { + await eventArgs.Context.RespondAsync(messageBuilder); + } + } +} diff --git a/DSharpPlus.Commands/ContextChecks/ContextCheckAttribute.cs b/DSharpPlus.Commands/ContextChecks/ContextCheckAttribute.cs index befc6002c1..92055ae916 100644 --- a/DSharpPlus.Commands/ContextChecks/ContextCheckAttribute.cs +++ b/DSharpPlus.Commands/ContextChecks/ContextCheckAttribute.cs @@ -1,6 +1,6 @@ -using System; - -namespace DSharpPlus.Commands.ContextChecks; - -[AttributeUsage(AttributeTargets.All, Inherited = false, AllowMultiple = true)] -public abstract class ContextCheckAttribute : Attribute; +using System; + +namespace DSharpPlus.Commands.ContextChecks; + +[AttributeUsage(AttributeTargets.All, Inherited = false, AllowMultiple = true)] +public abstract class ContextCheckAttribute : Attribute; diff --git a/DSharpPlus.Commands/ContextChecks/ContextCheckFailedData.cs b/DSharpPlus.Commands/ContextChecks/ContextCheckFailedData.cs index 369eb3d348..a973b09700 100644 --- a/DSharpPlus.Commands/ContextChecks/ContextCheckFailedData.cs +++ b/DSharpPlus.Commands/ContextChecks/ContextCheckFailedData.cs @@ -1,13 +1,13 @@ -using System; - -namespace DSharpPlus.Commands.ContextChecks; - -/// -/// Represents data for when a context check fails execution. -/// -public sealed class ContextCheckFailedData -{ - public required ContextCheckAttribute ContextCheckAttribute { get; init; } - public required string ErrorMessage { get; init; } - public Exception? Exception { get; init; } -} +using System; + +namespace DSharpPlus.Commands.ContextChecks; + +/// +/// Represents data for when a context check fails execution. +/// +public sealed class ContextCheckFailedData +{ + public required ContextCheckAttribute ContextCheckAttribute { get; init; } + public required string ErrorMessage { get; init; } + public Exception? Exception { get; init; } +} diff --git a/DSharpPlus.Commands/ContextChecks/ContextCheckMapEntry.cs b/DSharpPlus.Commands/ContextChecks/ContextCheckMapEntry.cs index c1bb815c44..9259c82522 100644 --- a/DSharpPlus.Commands/ContextChecks/ContextCheckMapEntry.cs +++ b/DSharpPlus.Commands/ContextChecks/ContextCheckMapEntry.cs @@ -1,18 +1,18 @@ -using System; -using System.Threading.Tasks; - -namespace DSharpPlus.Commands.ContextChecks; - -/// -/// Represents an entry in a map of attributes to check types. we can't just do this as a dictionary because one attribute may -/// key multiple different checks. -/// -public readonly record struct ContextCheckMapEntry -{ - public required Type AttributeType { get; init; } - - public required Type CheckType { get; init; } - - // we cache this here so that we don't have to deal with it every invocation. - public required Func> ExecuteCheckAsync { get; init; } -} +using System; +using System.Threading.Tasks; + +namespace DSharpPlus.Commands.ContextChecks; + +/// +/// Represents an entry in a map of attributes to check types. we can't just do this as a dictionary because one attribute may +/// key multiple different checks. +/// +public readonly record struct ContextCheckMapEntry +{ + public required Type AttributeType { get; init; } + + public required Type CheckType { get; init; } + + // we cache this here so that we don't have to deal with it every invocation. + public required Func> ExecuteCheckAsync { get; init; } +} diff --git a/DSharpPlus.Commands/ContextChecks/DirectMessageUsageAttribute.cs b/DSharpPlus.Commands/ContextChecks/DirectMessageUsageAttribute.cs index 72a568ec51..14ed3d1d1e 100644 --- a/DSharpPlus.Commands/ContextChecks/DirectMessageUsageAttribute.cs +++ b/DSharpPlus.Commands/ContextChecks/DirectMessageUsageAttribute.cs @@ -1,16 +1,16 @@ -using System; - -namespace DSharpPlus.Commands.ContextChecks; - -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Delegate)] -public class DirectMessageUsageAttribute(DirectMessageUsage usage = DirectMessageUsage.AllowDMs) : ContextCheckAttribute -{ - public DirectMessageUsage Usage { get; init; } = usage; -} - -public enum DirectMessageUsage -{ - AllowDMs, - DenyDMs, - RequireDMs -} +using System; + +namespace DSharpPlus.Commands.ContextChecks; + +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Delegate)] +public class DirectMessageUsageAttribute(DirectMessageUsage usage = DirectMessageUsage.AllowDMs) : ContextCheckAttribute +{ + public DirectMessageUsage Usage { get; init; } = usage; +} + +public enum DirectMessageUsage +{ + AllowDMs, + DenyDMs, + RequireDMs +} diff --git a/DSharpPlus.Commands/ContextChecks/DirectMessageUsageCheck.cs b/DSharpPlus.Commands/ContextChecks/DirectMessageUsageCheck.cs index 90e1806709..b1fda21970 100644 --- a/DSharpPlus.Commands/ContextChecks/DirectMessageUsageCheck.cs +++ b/DSharpPlus.Commands/ContextChecks/DirectMessageUsageCheck.cs @@ -1,31 +1,31 @@ -using System.Diagnostics; -using System.Threading.Tasks; - -namespace DSharpPlus.Commands.ContextChecks; - -internal sealed class DirectMessageUsageCheck : IContextCheck -{ - public ValueTask ExecuteCheckAsync(DirectMessageUsageAttribute attribute, CommandContext context) - { - if (context.Channel.IsPrivate && attribute.Usage is not DirectMessageUsage.DenyDMs) - { - return ValueTask.FromResult(null); - } - else if (!context.Channel.IsPrivate && attribute.Usage is not DirectMessageUsage.RequireDMs) - { - return ValueTask.FromResult(null); - } - else - { - string dmStatus = context.Channel.IsPrivate ? "inside a DM" : "outside a DM"; - string requirement = attribute.Usage switch - { - DirectMessageUsage.DenyDMs => "denies DM usage", - DirectMessageUsage.RequireDMs => "requires DM usage", - _ => throw new UnreachableException($"DirectMessageUsageCheck reached an unreachable branch: {attribute.Usage}, IsPrivate was {context.Channel.IsPrivate}") - }; - - return ValueTask.FromResult($"The executed command {requirement} but was executed {dmStatus}."); - } - } -} +using System.Diagnostics; +using System.Threading.Tasks; + +namespace DSharpPlus.Commands.ContextChecks; + +internal sealed class DirectMessageUsageCheck : IContextCheck +{ + public ValueTask ExecuteCheckAsync(DirectMessageUsageAttribute attribute, CommandContext context) + { + if (context.Channel.IsPrivate && attribute.Usage is not DirectMessageUsage.DenyDMs) + { + return ValueTask.FromResult(null); + } + else if (!context.Channel.IsPrivate && attribute.Usage is not DirectMessageUsage.RequireDMs) + { + return ValueTask.FromResult(null); + } + else + { + string dmStatus = context.Channel.IsPrivate ? "inside a DM" : "outside a DM"; + string requirement = attribute.Usage switch + { + DirectMessageUsage.DenyDMs => "denies DM usage", + DirectMessageUsage.RequireDMs => "requires DM usage", + _ => throw new UnreachableException($"DirectMessageUsageCheck reached an unreachable branch: {attribute.Usage}, IsPrivate was {context.Channel.IsPrivate}") + }; + + return ValueTask.FromResult($"The executed command {requirement} but was executed {dmStatus}."); + } + } +} diff --git a/DSharpPlus.Commands/ContextChecks/IContextCheck.cs b/DSharpPlus.Commands/ContextChecks/IContextCheck.cs index 366e7e530d..51067a5576 100644 --- a/DSharpPlus.Commands/ContextChecks/IContextCheck.cs +++ b/DSharpPlus.Commands/ContextChecks/IContextCheck.cs @@ -1,25 +1,25 @@ -using System.Threading.Tasks; - -namespace DSharpPlus.Commands.ContextChecks; - -/// -/// Marker interface for context checks, use instead. -/// -public interface IContextCheck; - -/// -/// Represents a base interface for context checks to implement. -/// -public interface IContextCheck : IContextCheck where TAttribute : ContextCheckAttribute -{ - /// - /// Executes the check given the attribute. - /// - /// - /// It is allowed for a check to access other metadata from the context. - /// - /// The attribute this command was decorated with. - /// The context this command is executed in. - /// A string containing the error message, or null if successful. - public ValueTask ExecuteCheckAsync(TAttribute attribute, CommandContext context); -} +using System.Threading.Tasks; + +namespace DSharpPlus.Commands.ContextChecks; + +/// +/// Marker interface for context checks, use instead. +/// +public interface IContextCheck; + +/// +/// Represents a base interface for context checks to implement. +/// +public interface IContextCheck : IContextCheck where TAttribute : ContextCheckAttribute +{ + /// + /// Executes the check given the attribute. + /// + /// + /// It is allowed for a check to access other metadata from the context. + /// + /// The attribute this command was decorated with. + /// The context this command is executed in. + /// A string containing the error message, or null if successful. + public ValueTask ExecuteCheckAsync(TAttribute attribute, CommandContext context); +} diff --git a/DSharpPlus.Commands/ContextChecks/ParameterChecks/IParameterCheck.cs b/DSharpPlus.Commands/ContextChecks/ParameterChecks/IParameterCheck.cs index df65e9e426..418469cb28 100644 --- a/DSharpPlus.Commands/ContextChecks/ParameterChecks/IParameterCheck.cs +++ b/DSharpPlus.Commands/ContextChecks/ParameterChecks/IParameterCheck.cs @@ -1,26 +1,26 @@ -using System.Threading.Tasks; - -namespace DSharpPlus.Commands.ContextChecks.ParameterChecks; - -/// -/// Marker interface for parameter checks. Use instead. -/// -public interface IParameterCheck; - -/// -/// Represents a base interface for parameter checks to implement. -/// -public interface IParameterCheck : IParameterCheck -{ - /// - /// Executes the check given the attribute and parameter info. - /// - /// - /// It is allowed for a check to access other metadata from the context. - /// - /// The attribute this parameter was decorated with. - /// The relevant parameters metadata representation and value. - /// The context the containing command is executed in. - /// A string containing the error message, or null if successful. - public ValueTask ExecuteCheckAsync(TAttribute attribute, ParameterCheckInfo info, CommandContext context); -} +using System.Threading.Tasks; + +namespace DSharpPlus.Commands.ContextChecks.ParameterChecks; + +/// +/// Marker interface for parameter checks. Use instead. +/// +public interface IParameterCheck; + +/// +/// Represents a base interface for parameter checks to implement. +/// +public interface IParameterCheck : IParameterCheck +{ + /// + /// Executes the check given the attribute and parameter info. + /// + /// + /// It is allowed for a check to access other metadata from the context. + /// + /// The attribute this parameter was decorated with. + /// The relevant parameters metadata representation and value. + /// The context the containing command is executed in. + /// A string containing the error message, or null if successful. + public ValueTask ExecuteCheckAsync(TAttribute attribute, ParameterCheckInfo info, CommandContext context); +} diff --git a/DSharpPlus.Commands/ContextChecks/ParameterChecks/ParameterCheckAttribute.cs b/DSharpPlus.Commands/ContextChecks/ParameterChecks/ParameterCheckAttribute.cs index 804096cee9..5aad23bff8 100644 --- a/DSharpPlus.Commands/ContextChecks/ParameterChecks/ParameterCheckAttribute.cs +++ b/DSharpPlus.Commands/ContextChecks/ParameterChecks/ParameterCheckAttribute.cs @@ -1,9 +1,9 @@ -using System; - -namespace DSharpPlus.Commands.ContextChecks.ParameterChecks; - -/// -/// Represents a base attribute for parameter check metadata attributes. -/// -[AttributeUsage(AttributeTargets.Parameter, Inherited = false, AllowMultiple = true)] -public abstract class ParameterCheckAttribute : Attribute; +using System; + +namespace DSharpPlus.Commands.ContextChecks.ParameterChecks; + +/// +/// Represents a base attribute for parameter check metadata attributes. +/// +[AttributeUsage(AttributeTargets.Parameter, Inherited = false, AllowMultiple = true)] +public abstract class ParameterCheckAttribute : Attribute; diff --git a/DSharpPlus.Commands/ContextChecks/ParameterChecks/ParameterCheckFailedData.cs b/DSharpPlus.Commands/ContextChecks/ParameterChecks/ParameterCheckFailedData.cs index 917da55df1..2d532b0188 100644 --- a/DSharpPlus.Commands/ContextChecks/ParameterChecks/ParameterCheckFailedData.cs +++ b/DSharpPlus.Commands/ContextChecks/ParameterChecks/ParameterCheckFailedData.cs @@ -1,24 +1,24 @@ -using System; - -namespace DSharpPlus.Commands.ContextChecks.ParameterChecks; - -/// -/// Contains information about a failed parameter check. -/// -public sealed class ParameterCheckFailedData -{ - /// - /// Metadata for the failed parameter check. - /// - public required ParameterCheckAttribute ParameterCheckAttribute { get; init; } - - /// - /// The error message returned by the check. - /// - public required string ErrorMessage { get; init; } - - /// - /// If applicable, the exception thrown during executing the check. - /// - public Exception? Exception { get; init; } -} +using System; + +namespace DSharpPlus.Commands.ContextChecks.ParameterChecks; + +/// +/// Contains information about a failed parameter check. +/// +public sealed class ParameterCheckFailedData +{ + /// + /// Metadata for the failed parameter check. + /// + public required ParameterCheckAttribute ParameterCheckAttribute { get; init; } + + /// + /// The error message returned by the check. + /// + public required string ErrorMessage { get; init; } + + /// + /// If applicable, the exception thrown during executing the check. + /// + public Exception? Exception { get; init; } +} diff --git a/DSharpPlus.Commands/ContextChecks/ParameterChecks/ParameterCheckInfo.cs b/DSharpPlus.Commands/ContextChecks/ParameterChecks/ParameterCheckInfo.cs index 5a66b3d572..d3c43a9e0b 100644 --- a/DSharpPlus.Commands/ContextChecks/ParameterChecks/ParameterCheckInfo.cs +++ b/DSharpPlus.Commands/ContextChecks/ParameterChecks/ParameterCheckInfo.cs @@ -1,10 +1,10 @@ -using DSharpPlus.Commands.Trees; - -namespace DSharpPlus.Commands.ContextChecks.ParameterChecks; - -/// -/// Presents information about a parameter check. -/// -/// The parameter as represented in the command tree. -/// The processed value of the parameter. -public sealed record ParameterCheckInfo(CommandParameter Parameter, object? Value); +using DSharpPlus.Commands.Trees; + +namespace DSharpPlus.Commands.ContextChecks.ParameterChecks; + +/// +/// Presents information about a parameter check. +/// +/// The parameter as represented in the command tree. +/// The processed value of the parameter. +public sealed record ParameterCheckInfo(CommandParameter Parameter, object? Value); diff --git a/DSharpPlus.Commands/ContextChecks/ParameterChecks/ParameterCheckMapEntry.cs b/DSharpPlus.Commands/ContextChecks/ParameterChecks/ParameterCheckMapEntry.cs index 2a52221a44..1b0f2c0297 100644 --- a/DSharpPlus.Commands/ContextChecks/ParameterChecks/ParameterCheckMapEntry.cs +++ b/DSharpPlus.Commands/ContextChecks/ParameterChecks/ParameterCheckMapEntry.cs @@ -1,18 +1,18 @@ -using System; -using System.Threading.Tasks; - -namespace DSharpPlus.Commands.ContextChecks.ParameterChecks; - -/// -/// Represents an entry in a map of attributes to check types. we can't just do this as a dictionary because one attribute may -/// key multiple different checks. -/// -public readonly record struct ParameterCheckMapEntry -{ - public required Type AttributeType { get; init; } - - public required Type CheckType { get; init; } - - // we cache this here so that we don't have to deal with it every invocation. - public required Func> ExecuteCheckAsync { get; init; } -} +using System; +using System.Threading.Tasks; + +namespace DSharpPlus.Commands.ContextChecks.ParameterChecks; + +/// +/// Represents an entry in a map of attributes to check types. we can't just do this as a dictionary because one attribute may +/// key multiple different checks. +/// +public readonly record struct ParameterCheckMapEntry +{ + public required Type AttributeType { get; init; } + + public required Type CheckType { get; init; } + + // we cache this here so that we don't have to deal with it every invocation. + public required Func> ExecuteCheckAsync { get; init; } +} diff --git a/DSharpPlus.Commands/ContextChecks/ParameterChecks/RequireHierarchyCheck.cs b/DSharpPlus.Commands/ContextChecks/ParameterChecks/RequireHierarchyCheck.cs index 25bd094599..153ce88587 100644 --- a/DSharpPlus.Commands/ContextChecks/ParameterChecks/RequireHierarchyCheck.cs +++ b/DSharpPlus.Commands/ContextChecks/ParameterChecks/RequireHierarchyCheck.cs @@ -1,40 +1,40 @@ -#pragma warning disable IDE0046 // no quintuple nested ternaries today -using System.Threading.Tasks; -using DSharpPlus.Entities; - -namespace DSharpPlus.Commands.ContextChecks.ParameterChecks; - -/// -/// Executes the checks for requiring a hierarchical order between the bot/executor and a parameter. -/// -public sealed class RequireHierarchyCheck : - IParameterCheck, - IParameterCheck -{ - public ValueTask ExecuteCheckAsync(RequireHigherBotHierarchyAttribute attribute, ParameterCheckInfo info, CommandContext context) - { - return info.Value switch - { - _ when context.Guild is null => ValueTask.FromResult(null), - null => ValueTask.FromResult(null), - DiscordRole role when context.Guild.CurrentMember.Hierarchy > role.Position => ValueTask.FromResult(null), - DiscordRole => ValueTask.FromResult("The provided role was higher than the highest role of the bot user."), - DiscordMember member when context.Guild.CurrentMember.Hierarchy > member.Hierarchy => ValueTask.FromResult(null), - DiscordMember => ValueTask.FromResult("The provided member's highest role was higher than the highest role of the bot user."), - _ => ValueTask.FromResult("The provided parameter was neither a role nor an user, failed to check hierarchy.") - }; - } - - public ValueTask ExecuteCheckAsync(RequireHigherUserHierarchyAttribute attribute, ParameterCheckInfo info, CommandContext context) - { - return info.Value switch - { - _ when context.Guild is null => ValueTask.FromResult(null), - DiscordRole role when context.Member!.Hierarchy > role.Position => ValueTask.FromResult(null), - DiscordRole => ValueTask.FromResult("The provided role was higher than the highest role of the executing user."), - DiscordMember member when context.Member!.Hierarchy > member.Hierarchy => ValueTask.FromResult(null), - DiscordMember => ValueTask.FromResult("The provided member's highest role was higher than the highest role of the executing user."), - _ => ValueTask.FromResult("The provided parameter was neither a role nor an user, failed to check hierarchy.") - }; - } -} +#pragma warning disable IDE0046 // no quintuple nested ternaries today +using System.Threading.Tasks; +using DSharpPlus.Entities; + +namespace DSharpPlus.Commands.ContextChecks.ParameterChecks; + +/// +/// Executes the checks for requiring a hierarchical order between the bot/executor and a parameter. +/// +public sealed class RequireHierarchyCheck : + IParameterCheck, + IParameterCheck +{ + public ValueTask ExecuteCheckAsync(RequireHigherBotHierarchyAttribute attribute, ParameterCheckInfo info, CommandContext context) + { + return info.Value switch + { + _ when context.Guild is null => ValueTask.FromResult(null), + null => ValueTask.FromResult(null), + DiscordRole role when context.Guild.CurrentMember.Hierarchy > role.Position => ValueTask.FromResult(null), + DiscordRole => ValueTask.FromResult("The provided role was higher than the highest role of the bot user."), + DiscordMember member when context.Guild.CurrentMember.Hierarchy > member.Hierarchy => ValueTask.FromResult(null), + DiscordMember => ValueTask.FromResult("The provided member's highest role was higher than the highest role of the bot user."), + _ => ValueTask.FromResult("The provided parameter was neither a role nor an user, failed to check hierarchy.") + }; + } + + public ValueTask ExecuteCheckAsync(RequireHigherUserHierarchyAttribute attribute, ParameterCheckInfo info, CommandContext context) + { + return info.Value switch + { + _ when context.Guild is null => ValueTask.FromResult(null), + DiscordRole role when context.Member!.Hierarchy > role.Position => ValueTask.FromResult(null), + DiscordRole => ValueTask.FromResult("The provided role was higher than the highest role of the executing user."), + DiscordMember member when context.Member!.Hierarchy > member.Hierarchy => ValueTask.FromResult(null), + DiscordMember => ValueTask.FromResult("The provided member's highest role was higher than the highest role of the executing user."), + _ => ValueTask.FromResult("The provided parameter was neither a role nor an user, failed to check hierarchy.") + }; + } +} diff --git a/DSharpPlus.Commands/ContextChecks/ParameterChecks/RequireHigherBotHierarchyAttribute.cs b/DSharpPlus.Commands/ContextChecks/ParameterChecks/RequireHigherBotHierarchyAttribute.cs index 7d32b5974a..4da2995924 100644 --- a/DSharpPlus.Commands/ContextChecks/ParameterChecks/RequireHigherBotHierarchyAttribute.cs +++ b/DSharpPlus.Commands/ContextChecks/ParameterChecks/RequireHigherBotHierarchyAttribute.cs @@ -1,7 +1,7 @@ -namespace DSharpPlus.Commands.ContextChecks.ParameterChecks; - - -/// -/// Instructs the extension to verify that the bot is hierarchically placed higher than the value of this parameter. -/// -public sealed class RequireHigherBotHierarchyAttribute : ParameterCheckAttribute; +namespace DSharpPlus.Commands.ContextChecks.ParameterChecks; + + +/// +/// Instructs the extension to verify that the bot is hierarchically placed higher than the value of this parameter. +/// +public sealed class RequireHigherBotHierarchyAttribute : ParameterCheckAttribute; diff --git a/DSharpPlus.Commands/ContextChecks/ParameterChecks/RequireHigherUserHierarchyAttribute.cs b/DSharpPlus.Commands/ContextChecks/ParameterChecks/RequireHigherUserHierarchyAttribute.cs index 14dd0ffa4a..a21c61445b 100644 --- a/DSharpPlus.Commands/ContextChecks/ParameterChecks/RequireHigherUserHierarchyAttribute.cs +++ b/DSharpPlus.Commands/ContextChecks/ParameterChecks/RequireHigherUserHierarchyAttribute.cs @@ -1,7 +1,7 @@ -namespace DSharpPlus.Commands.ContextChecks.ParameterChecks; - - -/// -/// Requires that the executing user is hierarchically placed higher than the value of this parameter. -/// -public sealed class RequireHigherUserHierarchyAttribute : ParameterCheckAttribute; +namespace DSharpPlus.Commands.ContextChecks.ParameterChecks; + + +/// +/// Requires that the executing user is hierarchically placed higher than the value of this parameter. +/// +public sealed class RequireHigherUserHierarchyAttribute : ParameterCheckAttribute; diff --git a/DSharpPlus.Commands/ContextChecks/RequireApplicationOwnerAttribute.cs b/DSharpPlus.Commands/ContextChecks/RequireApplicationOwnerAttribute.cs index 4fd79587ce..7f6c6e8219 100644 --- a/DSharpPlus.Commands/ContextChecks/RequireApplicationOwnerAttribute.cs +++ b/DSharpPlus.Commands/ContextChecks/RequireApplicationOwnerAttribute.cs @@ -1,6 +1,6 @@ -using System; - -namespace DSharpPlus.Commands.ContextChecks; - -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Delegate)] -public class RequireApplicationOwnerAttribute : ContextCheckAttribute; +using System; + +namespace DSharpPlus.Commands.ContextChecks; + +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Delegate)] +public class RequireApplicationOwnerAttribute : ContextCheckAttribute; diff --git a/DSharpPlus.Commands/ContextChecks/RequireApplicationOwnerCheck.cs b/DSharpPlus.Commands/ContextChecks/RequireApplicationOwnerCheck.cs index f4bb016cee..9ecfb5a519 100644 --- a/DSharpPlus.Commands/ContextChecks/RequireApplicationOwnerCheck.cs +++ b/DSharpPlus.Commands/ContextChecks/RequireApplicationOwnerCheck.cs @@ -1,13 +1,13 @@ -using System.Linq; -using System.Threading.Tasks; - -namespace DSharpPlus.Commands.ContextChecks; - -internal sealed class RequireApplicationOwnerCheck : IContextCheck -{ - public ValueTask ExecuteCheckAsync(RequireApplicationOwnerAttribute attribute, CommandContext context) => - ValueTask.FromResult(context.Client.CurrentApplication.Owners?.Contains(context.User) == true || context.User.Id == context.Client.CurrentUser.Id - ? null - : "This command must be executed by an owner of the application." - ); -} +using System.Linq; +using System.Threading.Tasks; + +namespace DSharpPlus.Commands.ContextChecks; + +internal sealed class RequireApplicationOwnerCheck : IContextCheck +{ + public ValueTask ExecuteCheckAsync(RequireApplicationOwnerAttribute attribute, CommandContext context) => + ValueTask.FromResult(context.Client.CurrentApplication.Owners?.Contains(context.User) == true || context.User.Id == context.Client.CurrentUser.Id + ? null + : "This command must be executed by an owner of the application." + ); +} diff --git a/DSharpPlus.Commands/ContextChecks/RequireGuildAttribute.cs b/DSharpPlus.Commands/ContextChecks/RequireGuildAttribute.cs index 6b4700e72a..79f2d9ab5a 100644 --- a/DSharpPlus.Commands/ContextChecks/RequireGuildAttribute.cs +++ b/DSharpPlus.Commands/ContextChecks/RequireGuildAttribute.cs @@ -1,6 +1,6 @@ -using System; - -namespace DSharpPlus.Commands.ContextChecks; - -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Delegate)] -public class RequireGuildAttribute : ContextCheckAttribute; +using System; + +namespace DSharpPlus.Commands.ContextChecks; + +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Delegate)] +public class RequireGuildAttribute : ContextCheckAttribute; diff --git a/DSharpPlus.Commands/ContextChecks/RequireGuildCheck.cs b/DSharpPlus.Commands/ContextChecks/RequireGuildCheck.cs index ac66b6e33a..b11c8ed6c6 100644 --- a/DSharpPlus.Commands/ContextChecks/RequireGuildCheck.cs +++ b/DSharpPlus.Commands/ContextChecks/RequireGuildCheck.cs @@ -1,11 +1,11 @@ -using System.Threading.Tasks; - -namespace DSharpPlus.Commands.ContextChecks; - -internal sealed class RequireGuildCheck : IContextCheck -{ - internal const string ErrorMessage = "This command must be executed in a guild."; - - public ValueTask ExecuteCheckAsync(RequireGuildAttribute attribute, CommandContext context) - => ValueTask.FromResult(context.Guild is null ? ErrorMessage : null); -} +using System.Threading.Tasks; + +namespace DSharpPlus.Commands.ContextChecks; + +internal sealed class RequireGuildCheck : IContextCheck +{ + internal const string ErrorMessage = "This command must be executed in a guild."; + + public ValueTask ExecuteCheckAsync(RequireGuildAttribute attribute, CommandContext context) + => ValueTask.FromResult(context.Guild is null ? ErrorMessage : null); +} diff --git a/DSharpPlus.Commands/ContextChecks/RequireNsfwAttribute.cs b/DSharpPlus.Commands/ContextChecks/RequireNsfwAttribute.cs index 977bc79080..ed4901cc1c 100644 --- a/DSharpPlus.Commands/ContextChecks/RequireNsfwAttribute.cs +++ b/DSharpPlus.Commands/ContextChecks/RequireNsfwAttribute.cs @@ -1,6 +1,6 @@ -using System; - -namespace DSharpPlus.Commands.ContextChecks; - -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Delegate)] -public class RequireNsfwAttribute : ContextCheckAttribute; +using System; + +namespace DSharpPlus.Commands.ContextChecks; + +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Delegate)] +public class RequireNsfwAttribute : ContextCheckAttribute; diff --git a/DSharpPlus.Commands/ContextChecks/RequireNsfwCheck.cs b/DSharpPlus.Commands/ContextChecks/RequireNsfwCheck.cs index c692a963f6..5f57fc5f4b 100644 --- a/DSharpPlus.Commands/ContextChecks/RequireNsfwCheck.cs +++ b/DSharpPlus.Commands/ContextChecks/RequireNsfwCheck.cs @@ -1,12 +1,12 @@ -using System.Threading.Tasks; - -namespace DSharpPlus.Commands.ContextChecks; - -internal sealed class RequireNsfwCheck : IContextCheck -{ - public ValueTask ExecuteCheckAsync(RequireNsfwAttribute attribute, CommandContext context) => - ValueTask.FromResult(context.Channel.IsPrivate || context.Channel.IsNSFW || (context.Guild is not null && context.Guild.IsNSFW) - ? null - : "This command must be executed in a NSFW channel." - ); -} +using System.Threading.Tasks; + +namespace DSharpPlus.Commands.ContextChecks; + +internal sealed class RequireNsfwCheck : IContextCheck +{ + public ValueTask ExecuteCheckAsync(RequireNsfwAttribute attribute, CommandContext context) => + ValueTask.FromResult(context.Channel.IsPrivate || context.Channel.IsNSFW || (context.Guild is not null && context.Guild.IsNSFW) + ? null + : "This command must be executed in a NSFW channel." + ); +} diff --git a/DSharpPlus.Commands/ContextChecks/RequirePermissionsAttribute.cs b/DSharpPlus.Commands/ContextChecks/RequirePermissionsAttribute.cs index 7cb05cd8a1..5b4cf753c2 100644 --- a/DSharpPlus.Commands/ContextChecks/RequirePermissionsAttribute.cs +++ b/DSharpPlus.Commands/ContextChecks/RequirePermissionsAttribute.cs @@ -1,19 +1,19 @@ -using System; -using System.Collections.Generic; -using DSharpPlus.Entities; - -namespace DSharpPlus.Commands.ContextChecks; - -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Delegate)] -public class RequirePermissionsAttribute : RequireGuildAttribute -{ - public DiscordPermissions BotPermissions { get; init; } - public DiscordPermissions UserPermissions { get; init; } - - public RequirePermissionsAttribute(params DiscordPermission[] permissions) => this.BotPermissions = this.UserPermissions = new((IReadOnlyList)permissions); - public RequirePermissionsAttribute(DiscordPermission[] botPermissions, DiscordPermission[] userPermissions) - { - this.BotPermissions = new((IReadOnlyList)botPermissions); - this.UserPermissions = new((IReadOnlyList)userPermissions); - } -} +using System; +using System.Collections.Generic; +using DSharpPlus.Entities; + +namespace DSharpPlus.Commands.ContextChecks; + +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Delegate)] +public class RequirePermissionsAttribute : RequireGuildAttribute +{ + public DiscordPermissions BotPermissions { get; init; } + public DiscordPermissions UserPermissions { get; init; } + + public RequirePermissionsAttribute(params DiscordPermission[] permissions) => this.BotPermissions = this.UserPermissions = new((IReadOnlyList)permissions); + public RequirePermissionsAttribute(DiscordPermission[] botPermissions, DiscordPermission[] userPermissions) + { + this.BotPermissions = new((IReadOnlyList)botPermissions); + this.UserPermissions = new((IReadOnlyList)userPermissions); + } +} diff --git a/DSharpPlus.Commands/ContextChecks/RequirePermissionsCheck.cs b/DSharpPlus.Commands/ContextChecks/RequirePermissionsCheck.cs index 111ae160ec..3cb889fb3c 100644 --- a/DSharpPlus.Commands/ContextChecks/RequirePermissionsCheck.cs +++ b/DSharpPlus.Commands/ContextChecks/RequirePermissionsCheck.cs @@ -1,37 +1,37 @@ -#pragma warning disable IDE0046 - -using System.Threading.Tasks; - -using DSharpPlus.Commands.Processors.SlashCommands; - -namespace DSharpPlus.Commands.ContextChecks; - -internal sealed class RequirePermissionsCheck : IContextCheck -{ - public ValueTask ExecuteCheckAsync(RequirePermissionsAttribute attribute, CommandContext context) - { - if (context is SlashCommandContext slashContext) - { - if (!slashContext.Interaction.AppPermissions.HasAllPermissions(attribute.BotPermissions)) - { - return ValueTask.FromResult("The bot did not have the needed permissions to execute this command."); - } - - return ValueTask.FromResult(null); - } - else if (context.Guild is null) - { - return ValueTask.FromResult(RequireGuildCheck.ErrorMessage); - } - else if (!context.Guild!.CurrentMember.PermissionsIn(context.Channel).HasAllPermissions(attribute.BotPermissions)) - { - return ValueTask.FromResult("The bot did not have the needed permissions to execute this command."); - } - else if (!context.Member!.PermissionsIn(context.Channel).HasAllPermissions(attribute.UserPermissions)) - { - return ValueTask.FromResult("The executing user did not have the needed permissions to execute this command."); - } - - return ValueTask.FromResult(null); - } -} +#pragma warning disable IDE0046 + +using System.Threading.Tasks; + +using DSharpPlus.Commands.Processors.SlashCommands; + +namespace DSharpPlus.Commands.ContextChecks; + +internal sealed class RequirePermissionsCheck : IContextCheck +{ + public ValueTask ExecuteCheckAsync(RequirePermissionsAttribute attribute, CommandContext context) + { + if (context is SlashCommandContext slashContext) + { + if (!slashContext.Interaction.AppPermissions.HasAllPermissions(attribute.BotPermissions)) + { + return ValueTask.FromResult("The bot did not have the needed permissions to execute this command."); + } + + return ValueTask.FromResult(null); + } + else if (context.Guild is null) + { + return ValueTask.FromResult(RequireGuildCheck.ErrorMessage); + } + else if (!context.Guild!.CurrentMember.PermissionsIn(context.Channel).HasAllPermissions(attribute.BotPermissions)) + { + return ValueTask.FromResult("The bot did not have the needed permissions to execute this command."); + } + else if (!context.Member!.PermissionsIn(context.Channel).HasAllPermissions(attribute.UserPermissions)) + { + return ValueTask.FromResult("The executing user did not have the needed permissions to execute this command."); + } + + return ValueTask.FromResult(null); + } +} diff --git a/DSharpPlus.Commands/ContextChecks/UnconditionalCheckAttribute.cs b/DSharpPlus.Commands/ContextChecks/UnconditionalCheckAttribute.cs index 55f4142cad..867291dfbb 100644 --- a/DSharpPlus.Commands/ContextChecks/UnconditionalCheckAttribute.cs +++ b/DSharpPlus.Commands/ContextChecks/UnconditionalCheckAttribute.cs @@ -1,9 +1,9 @@ -using System; - -namespace DSharpPlus.Commands.ContextChecks; - -/// -/// Represents a type for checks to register against that will always be executed, whether the attribute is present or not. -/// -[AttributeUsage(AttributeTargets.All, AllowMultiple = false, Inherited = true)] -public sealed class UnconditionalCheckAttribute : ContextCheckAttribute; +using System; + +namespace DSharpPlus.Commands.ContextChecks; + +/// +/// Represents a type for checks to register against that will always be executed, whether the attribute is present or not. +/// +[AttributeUsage(AttributeTargets.All, AllowMultiple = false, Inherited = true)] +public sealed class UnconditionalCheckAttribute : ContextCheckAttribute; diff --git a/DSharpPlus.Commands/Converters/BooleanConverter.cs b/DSharpPlus.Commands/Converters/BooleanConverter.cs index 181bdf75a8..ee1df0ccf0 100644 --- a/DSharpPlus.Commands/Converters/BooleanConverter.cs +++ b/DSharpPlus.Commands/Converters/BooleanConverter.cs @@ -1,21 +1,21 @@ -using System.Threading.Tasks; -using DSharpPlus.Commands.Processors.SlashCommands; -using DSharpPlus.Commands.Processors.TextCommands; -using DSharpPlus.Entities; - -namespace DSharpPlus.Commands.Converters; - -public class BooleanConverter : ISlashArgumentConverter, ITextArgumentConverter -{ - public DiscordApplicationCommandOptionType ParameterType => DiscordApplicationCommandOptionType.Boolean; - public ConverterInputType RequiresText => ConverterInputType.Always; - public string ReadableName => "Boolean (true/false)"; - - /// - public Task> ConvertAsync(ConverterContext context) => Task.FromResult(context.Argument?.ToString()?.ToLowerInvariant() switch - { - "true" or "yes" or "y" or "1" or "on" or "enable" or "enabled" or "t" => Optional.FromValue(true), - "false" or "no" or "n" or "0" or "off" or "disable" or "disabled" or "f" => Optional.FromValue(false), - _ => Optional.FromNoValue(), - }); -} +using System.Threading.Tasks; +using DSharpPlus.Commands.Processors.SlashCommands; +using DSharpPlus.Commands.Processors.TextCommands; +using DSharpPlus.Entities; + +namespace DSharpPlus.Commands.Converters; + +public class BooleanConverter : ISlashArgumentConverter, ITextArgumentConverter +{ + public DiscordApplicationCommandOptionType ParameterType => DiscordApplicationCommandOptionType.Boolean; + public ConverterInputType RequiresText => ConverterInputType.Always; + public string ReadableName => "Boolean (true/false)"; + + /// + public Task> ConvertAsync(ConverterContext context) => Task.FromResult(context.Argument?.ToString()?.ToLowerInvariant() switch + { + "true" or "yes" or "y" or "1" or "on" or "enable" or "enabled" or "t" => Optional.FromValue(true), + "false" or "no" or "n" or "0" or "off" or "disable" or "disabled" or "f" => Optional.FromValue(false), + _ => Optional.FromNoValue(), + }); +} diff --git a/DSharpPlus.Commands/Converters/ByteConverter.cs b/DSharpPlus.Commands/Converters/ByteConverter.cs index 6140b33aa5..39b04f4964 100644 --- a/DSharpPlus.Commands/Converters/ByteConverter.cs +++ b/DSharpPlus.Commands/Converters/ByteConverter.cs @@ -1,19 +1,19 @@ -using System.Globalization; -using System.Threading.Tasks; -using DSharpPlus.Commands.Processors.SlashCommands; -using DSharpPlus.Commands.Processors.TextCommands; -using DSharpPlus.Entities; - -namespace DSharpPlus.Commands.Converters; - -public class ByteConverter : ISlashArgumentConverter, ITextArgumentConverter -{ - public DiscordApplicationCommandOptionType ParameterType => DiscordApplicationCommandOptionType.Integer; - public ConverterInputType RequiresText => ConverterInputType.Always; - public string ReadableName => "Positive Tiny Integer"; - - public Task> ConvertAsync(ConverterContext context) => - byte.TryParse(context.Argument?.ToString(), CultureInfo.InvariantCulture, out byte result) - ? Task.FromResult(Optional.FromValue(result)) - : Task.FromResult(Optional.FromNoValue()); -} +using System.Globalization; +using System.Threading.Tasks; +using DSharpPlus.Commands.Processors.SlashCommands; +using DSharpPlus.Commands.Processors.TextCommands; +using DSharpPlus.Entities; + +namespace DSharpPlus.Commands.Converters; + +public class ByteConverter : ISlashArgumentConverter, ITextArgumentConverter +{ + public DiscordApplicationCommandOptionType ParameterType => DiscordApplicationCommandOptionType.Integer; + public ConverterInputType RequiresText => ConverterInputType.Always; + public string ReadableName => "Positive Tiny Integer"; + + public Task> ConvertAsync(ConverterContext context) => + byte.TryParse(context.Argument?.ToString(), CultureInfo.InvariantCulture, out byte result) + ? Task.FromResult(Optional.FromValue(result)) + : Task.FromResult(Optional.FromNoValue()); +} diff --git a/DSharpPlus.Commands/Converters/ConverterContext.cs b/DSharpPlus.Commands/Converters/ConverterContext.cs index d7f358068c..bf603dea75 100644 --- a/DSharpPlus.Commands/Converters/ConverterContext.cs +++ b/DSharpPlus.Commands/Converters/ConverterContext.cs @@ -1,83 +1,83 @@ -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using DSharpPlus.Commands.ArgumentModifiers; -using DSharpPlus.Commands.Trees; - -namespace DSharpPlus.Commands.Converters; - -/// -/// Represents context provided to argument converters. -/// -public abstract record ConverterContext : AbstractContext -{ - /// - /// The value of the current raw argument. - /// - public virtual object? Argument { get; protected set; } - - /// - /// The index of the current parameter. - /// - public int ParameterIndex { get; private set; } = -1; - - /// - /// The current parameter. - /// - public CommandParameter Parameter => this.Command.Parameters[this.ParameterIndex]; - - /// - /// The current index of the variadic-argument parameter. - /// - public int VariadicArgumentParameterIndex { get; protected set; } = -1; - - /// - /// The current variadic-argument parameter. - /// - public VariadicArgumentAttribute? VariadicArgumentAttribute { get; protected set; } - - /// - /// Advances to the next parameter, returning a value indicating whether there was another parameter. - /// - public virtual bool NextParameter() - { - if (this.ParameterIndex + 1 >= this.Command.Parameters.Count) - { - return false; - } - - this.ParameterIndex++; - this.VariadicArgumentParameterIndex = -1; - this.VariadicArgumentAttribute = this.Parameter.Attributes.FirstOrDefault(attribute => attribute is VariadicArgumentAttribute) as VariadicArgumentAttribute; - return true; - } - - /// - /// Advances to the next argument, returning a value indicating whether there was another argument. - /// - public abstract bool NextArgument(); - - /// - /// Increments the variadic-argument parameter index. - /// - /// Whether the current parameter can accept another argument or not. - [MemberNotNullWhen(true, nameof(VariadicArgumentAttribute))] - public virtual bool NextVariadicArgument() - { - if (this.VariadicArgumentAttribute is null) - { - return false; - } - else if (this.VariadicArgumentParameterIndex++ >= this.VariadicArgumentAttribute.MaximumArgumentCount) - { - this.VariadicArgumentParameterIndex--; - return false; - } - - return true; - } - - /// - /// Short-hand for converting to a more specific converter context type. - /// - public T As() where T : ConverterContext => (T)this; -} +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using DSharpPlus.Commands.ArgumentModifiers; +using DSharpPlus.Commands.Trees; + +namespace DSharpPlus.Commands.Converters; + +/// +/// Represents context provided to argument converters. +/// +public abstract record ConverterContext : AbstractContext +{ + /// + /// The value of the current raw argument. + /// + public virtual object? Argument { get; protected set; } + + /// + /// The index of the current parameter. + /// + public int ParameterIndex { get; private set; } = -1; + + /// + /// The current parameter. + /// + public CommandParameter Parameter => this.Command.Parameters[this.ParameterIndex]; + + /// + /// The current index of the variadic-argument parameter. + /// + public int VariadicArgumentParameterIndex { get; protected set; } = -1; + + /// + /// The current variadic-argument parameter. + /// + public VariadicArgumentAttribute? VariadicArgumentAttribute { get; protected set; } + + /// + /// Advances to the next parameter, returning a value indicating whether there was another parameter. + /// + public virtual bool NextParameter() + { + if (this.ParameterIndex + 1 >= this.Command.Parameters.Count) + { + return false; + } + + this.ParameterIndex++; + this.VariadicArgumentParameterIndex = -1; + this.VariadicArgumentAttribute = this.Parameter.Attributes.FirstOrDefault(attribute => attribute is VariadicArgumentAttribute) as VariadicArgumentAttribute; + return true; + } + + /// + /// Advances to the next argument, returning a value indicating whether there was another argument. + /// + public abstract bool NextArgument(); + + /// + /// Increments the variadic-argument parameter index. + /// + /// Whether the current parameter can accept another argument or not. + [MemberNotNullWhen(true, nameof(VariadicArgumentAttribute))] + public virtual bool NextVariadicArgument() + { + if (this.VariadicArgumentAttribute is null) + { + return false; + } + else if (this.VariadicArgumentParameterIndex++ >= this.VariadicArgumentAttribute.MaximumArgumentCount) + { + this.VariadicArgumentParameterIndex--; + return false; + } + + return true; + } + + /// + /// Short-hand for converting to a more specific converter context type. + /// + public T As() where T : ConverterContext => (T)this; +} diff --git a/DSharpPlus.Commands/Converters/ConverterDelegate`1.cs b/DSharpPlus.Commands/Converters/ConverterDelegate`1.cs index 9990e91b84..dd1b0756ff 100644 --- a/DSharpPlus.Commands/Converters/ConverterDelegate`1.cs +++ b/DSharpPlus.Commands/Converters/ConverterDelegate`1.cs @@ -1,6 +1,6 @@ -using System.Threading.Tasks; -using DSharpPlus.Entities; - -namespace DSharpPlus.Commands.Converters; - -public delegate ValueTask ConverterDelegate(ConverterContext context); +using System.Threading.Tasks; +using DSharpPlus.Entities; + +namespace DSharpPlus.Commands.Converters; + +public delegate ValueTask ConverterDelegate(ConverterContext context); diff --git a/DSharpPlus.Commands/Converters/DateTimeOffsetConverter.cs b/DSharpPlus.Commands/Converters/DateTimeOffsetConverter.cs index 6388b4dc8f..b3dd0932e6 100644 --- a/DSharpPlus.Commands/Converters/DateTimeOffsetConverter.cs +++ b/DSharpPlus.Commands/Converters/DateTimeOffsetConverter.cs @@ -1,20 +1,20 @@ -using System; -using System.Globalization; -using System.Threading.Tasks; -using DSharpPlus.Commands.Processors.SlashCommands; -using DSharpPlus.Commands.Processors.TextCommands; -using DSharpPlus.Entities; - -namespace DSharpPlus.Commands.Converters; - -public class DateTimeOffsetConverter : ISlashArgumentConverter, ITextArgumentConverter -{ - public DiscordApplicationCommandOptionType ParameterType => DiscordApplicationCommandOptionType.String; - public ConverterInputType RequiresText => ConverterInputType.Always; - public string ReadableName => "Date and Time"; - - public Task> ConvertAsync(ConverterContext context) => - DateTimeOffset.TryParse(context.Argument?.ToString(), CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out DateTimeOffset result) - ? Task.FromResult(Optional.FromValue(result)) - : Task.FromResult(Optional.FromNoValue()); -} +using System; +using System.Globalization; +using System.Threading.Tasks; +using DSharpPlus.Commands.Processors.SlashCommands; +using DSharpPlus.Commands.Processors.TextCommands; +using DSharpPlus.Entities; + +namespace DSharpPlus.Commands.Converters; + +public class DateTimeOffsetConverter : ISlashArgumentConverter, ITextArgumentConverter +{ + public DiscordApplicationCommandOptionType ParameterType => DiscordApplicationCommandOptionType.String; + public ConverterInputType RequiresText => ConverterInputType.Always; + public string ReadableName => "Date and Time"; + + public Task> ConvertAsync(ConverterContext context) => + DateTimeOffset.TryParse(context.Argument?.ToString(), CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out DateTimeOffset result) + ? Task.FromResult(Optional.FromValue(result)) + : Task.FromResult(Optional.FromNoValue()); +} diff --git a/DSharpPlus.Commands/Converters/DiscordAttachmentConverter.cs b/DSharpPlus.Commands/Converters/DiscordAttachmentConverter.cs index ae84909c01..82ad6b67f7 100644 --- a/DSharpPlus.Commands/Converters/DiscordAttachmentConverter.cs +++ b/DSharpPlus.Commands/Converters/DiscordAttachmentConverter.cs @@ -1,67 +1,67 @@ -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Threading.Tasks; -using DSharpPlus.Commands.ArgumentModifiers; -using DSharpPlus.Commands.Processors.SlashCommands; -using DSharpPlus.Commands.Processors.TextCommands; -using DSharpPlus.Commands.Trees; -using DSharpPlus.Entities; - -namespace DSharpPlus.Commands.Converters; - -public class AttachmentConverter : ISlashArgumentConverter, ITextArgumentConverter -{ - public DiscordApplicationCommandOptionType ParameterType => DiscordApplicationCommandOptionType.Attachment; - public ConverterInputType RequiresText => ConverterInputType.Never; - public string ReadableName => "Discord File"; - - public Task> ConvertAsync(ConverterContext context) - { - IReadOnlyList attachmentParameters = context.Command.Parameters.Where(argument => argument.Type == typeof(DiscordAttachment)).ToList(); - int currentAttachmentArgumentIndex = attachmentParameters.IndexOf(context.Parameter); - if (context is TextConverterContext textConverterContext) - { - foreach (CommandParameter attachmentParameter in attachmentParameters) - { - // Don't increase past the current attachment parameter - if (attachmentParameter == context.Parameter) - { - break; - } - else if (attachmentParameter.Attributes.FirstOrDefault(attribute => attribute is VariadicArgumentAttribute) is VariadicArgumentAttribute variadicArgumentAttribute) - { - // Increase the index by however many attachments we've already parsed - // We add by maximum argument count because the attachment converter will never fail to parse - // the attachment when it's present. - currentAttachmentArgumentIndex = variadicArgumentAttribute.MaximumArgumentCount; - } - } - - // Add the currently parsed attachment count to the index - if (context.VariadicArgumentParameterIndex != -1) - { - currentAttachmentArgumentIndex += context.VariadicArgumentParameterIndex; - } - - // Return the attachment from the original message - return textConverterContext.Message.Attachments.Count <= currentAttachmentArgumentIndex - ? Task.FromResult(Optional.FromNoValue()) - : Task.FromResult(Optional.FromValue(textConverterContext.Message.Attachments[currentAttachmentArgumentIndex])); - } - else if (context is InteractionConverterContext interactionConverterContext - // Resolved can be null on autocomplete contexts - && interactionConverterContext.Interaction.Data.Resolved is not null - // Check if we have enough attachments to fetch the current attachment - && interactionConverterContext.Interaction.Data.Options.Count(argument => argument.Type == DiscordApplicationCommandOptionType.Attachment) >= currentAttachmentArgumentIndex - // Check if we can parse the attachment ID (this should be guaranteed by Discord) - && ulong.TryParse(interactionConverterContext.Argument?.RawValue, CultureInfo.InvariantCulture, out ulong attachmentId) - // Check if the attachment exists - && interactionConverterContext.Interaction.Data.Resolved.Attachments.TryGetValue(attachmentId, out DiscordAttachment? attachment)) - { - return Task.FromResult(Optional.FromValue(attachment)); - } - - return Task.FromResult(Optional.FromNoValue()); - } -} +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; +using DSharpPlus.Commands.ArgumentModifiers; +using DSharpPlus.Commands.Processors.SlashCommands; +using DSharpPlus.Commands.Processors.TextCommands; +using DSharpPlus.Commands.Trees; +using DSharpPlus.Entities; + +namespace DSharpPlus.Commands.Converters; + +public class AttachmentConverter : ISlashArgumentConverter, ITextArgumentConverter +{ + public DiscordApplicationCommandOptionType ParameterType => DiscordApplicationCommandOptionType.Attachment; + public ConverterInputType RequiresText => ConverterInputType.Never; + public string ReadableName => "Discord File"; + + public Task> ConvertAsync(ConverterContext context) + { + IReadOnlyList attachmentParameters = context.Command.Parameters.Where(argument => argument.Type == typeof(DiscordAttachment)).ToList(); + int currentAttachmentArgumentIndex = attachmentParameters.IndexOf(context.Parameter); + if (context is TextConverterContext textConverterContext) + { + foreach (CommandParameter attachmentParameter in attachmentParameters) + { + // Don't increase past the current attachment parameter + if (attachmentParameter == context.Parameter) + { + break; + } + else if (attachmentParameter.Attributes.FirstOrDefault(attribute => attribute is VariadicArgumentAttribute) is VariadicArgumentAttribute variadicArgumentAttribute) + { + // Increase the index by however many attachments we've already parsed + // We add by maximum argument count because the attachment converter will never fail to parse + // the attachment when it's present. + currentAttachmentArgumentIndex = variadicArgumentAttribute.MaximumArgumentCount; + } + } + + // Add the currently parsed attachment count to the index + if (context.VariadicArgumentParameterIndex != -1) + { + currentAttachmentArgumentIndex += context.VariadicArgumentParameterIndex; + } + + // Return the attachment from the original message + return textConverterContext.Message.Attachments.Count <= currentAttachmentArgumentIndex + ? Task.FromResult(Optional.FromNoValue()) + : Task.FromResult(Optional.FromValue(textConverterContext.Message.Attachments[currentAttachmentArgumentIndex])); + } + else if (context is InteractionConverterContext interactionConverterContext + // Resolved can be null on autocomplete contexts + && interactionConverterContext.Interaction.Data.Resolved is not null + // Check if we have enough attachments to fetch the current attachment + && interactionConverterContext.Interaction.Data.Options.Count(argument => argument.Type == DiscordApplicationCommandOptionType.Attachment) >= currentAttachmentArgumentIndex + // Check if we can parse the attachment ID (this should be guaranteed by Discord) + && ulong.TryParse(interactionConverterContext.Argument?.RawValue, CultureInfo.InvariantCulture, out ulong attachmentId) + // Check if the attachment exists + && interactionConverterContext.Interaction.Data.Resolved.Attachments.TryGetValue(attachmentId, out DiscordAttachment? attachment)) + { + return Task.FromResult(Optional.FromValue(attachment)); + } + + return Task.FromResult(Optional.FromNoValue()); + } +} diff --git a/DSharpPlus.Commands/Converters/DiscordChannelConverter.cs b/DSharpPlus.Commands/Converters/DiscordChannelConverter.cs index bba7ad9cf4..f6417b0f0d 100644 --- a/DSharpPlus.Commands/Converters/DiscordChannelConverter.cs +++ b/DSharpPlus.Commands/Converters/DiscordChannelConverter.cs @@ -1,76 +1,76 @@ -using System; -using System.Globalization; -using System.Linq; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using DSharpPlus.Commands.Processors.SlashCommands; -using DSharpPlus.Commands.Processors.TextCommands; -using DSharpPlus.Entities; -using DSharpPlus.Exceptions; - -namespace DSharpPlus.Commands.Converters; - -public partial class DiscordChannelConverter : ISlashArgumentConverter, ITextArgumentConverter -{ - [GeneratedRegex(@"^<#(\d+)>$", RegexOptions.Compiled | RegexOptions.ECMAScript)] - public static partial Regex GetChannelMatchingRegex(); - - public DiscordApplicationCommandOptionType ParameterType => DiscordApplicationCommandOptionType.Channel; - public ConverterInputType RequiresText => ConverterInputType.Always; - public string ReadableName => "Discord Channel"; - - public async Task> ConvertAsync(ConverterContext context) - { - if (context is InteractionConverterContext interactionConverterContext - // Resolved can be null on autocomplete contexts - && interactionConverterContext.Interaction.Data.Resolved is not null - // Check if we can parse the channel ID (this should be guaranteed by Discord) - && ulong.TryParse(interactionConverterContext.Argument?.RawValue, CultureInfo.InvariantCulture, out ulong channelId) - // Check if the channel is in the resolved data - && interactionConverterContext.Interaction.Data.Resolved.Channels.TryGetValue(channelId, out DiscordChannel? channel) - ) - { - return Optional.FromValue(channel); - } - - // If the guild is null, return. - // We don't want to search for channels - // in DMs or other external guilds. - if (context.Guild is null) - { - return Optional.FromNoValue(); - } - - string? channelIdString = context.Argument?.ToString(); - if (string.IsNullOrWhiteSpace(channelIdString)) - { - return Optional.FromNoValue(); - } - - // Attempt to parse the channel id - if (!ulong.TryParse(channelIdString, CultureInfo.InvariantCulture, out channelId)) - { - // Value could be a channel mention. - Match match = GetChannelMatchingRegex().Match(channelIdString); - if (!match.Success || !ulong.TryParse(match.Groups[1].ValueSpan, NumberStyles.Number, CultureInfo.InvariantCulture, out channelId)) - { - // Try searching by name - DiscordChannel? namedChannel = context.Guild.Channels.Values.FirstOrDefault(channel => channel.Name.Equals(channelIdString, StringComparison.OrdinalIgnoreCase)); - return namedChannel is not null - ? Optional.FromValue(namedChannel) - : Optional.FromNoValue(); - } - } - - try - { - // Get channel async will search the guild cache for the channel - // or thread, if it's not found, it will fetch it from the API - return Optional.FromValue(await context.Guild.GetChannelAsync(channelId)); - } - catch (DiscordException) - { - return Optional.FromNoValue(); - } - } -} +using System; +using System.Globalization; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using DSharpPlus.Commands.Processors.SlashCommands; +using DSharpPlus.Commands.Processors.TextCommands; +using DSharpPlus.Entities; +using DSharpPlus.Exceptions; + +namespace DSharpPlus.Commands.Converters; + +public partial class DiscordChannelConverter : ISlashArgumentConverter, ITextArgumentConverter +{ + [GeneratedRegex(@"^<#(\d+)>$", RegexOptions.Compiled | RegexOptions.ECMAScript)] + public static partial Regex GetChannelMatchingRegex(); + + public DiscordApplicationCommandOptionType ParameterType => DiscordApplicationCommandOptionType.Channel; + public ConverterInputType RequiresText => ConverterInputType.Always; + public string ReadableName => "Discord Channel"; + + public async Task> ConvertAsync(ConverterContext context) + { + if (context is InteractionConverterContext interactionConverterContext + // Resolved can be null on autocomplete contexts + && interactionConverterContext.Interaction.Data.Resolved is not null + // Check if we can parse the channel ID (this should be guaranteed by Discord) + && ulong.TryParse(interactionConverterContext.Argument?.RawValue, CultureInfo.InvariantCulture, out ulong channelId) + // Check if the channel is in the resolved data + && interactionConverterContext.Interaction.Data.Resolved.Channels.TryGetValue(channelId, out DiscordChannel? channel) + ) + { + return Optional.FromValue(channel); + } + + // If the guild is null, return. + // We don't want to search for channels + // in DMs or other external guilds. + if (context.Guild is null) + { + return Optional.FromNoValue(); + } + + string? channelIdString = context.Argument?.ToString(); + if (string.IsNullOrWhiteSpace(channelIdString)) + { + return Optional.FromNoValue(); + } + + // Attempt to parse the channel id + if (!ulong.TryParse(channelIdString, CultureInfo.InvariantCulture, out channelId)) + { + // Value could be a channel mention. + Match match = GetChannelMatchingRegex().Match(channelIdString); + if (!match.Success || !ulong.TryParse(match.Groups[1].ValueSpan, NumberStyles.Number, CultureInfo.InvariantCulture, out channelId)) + { + // Try searching by name + DiscordChannel? namedChannel = context.Guild.Channels.Values.FirstOrDefault(channel => channel.Name.Equals(channelIdString, StringComparison.OrdinalIgnoreCase)); + return namedChannel is not null + ? Optional.FromValue(namedChannel) + : Optional.FromNoValue(); + } + } + + try + { + // Get channel async will search the guild cache for the channel + // or thread, if it's not found, it will fetch it from the API + return Optional.FromValue(await context.Guild.GetChannelAsync(channelId)); + } + catch (DiscordException) + { + return Optional.FromNoValue(); + } + } +} diff --git a/DSharpPlus.Commands/Converters/DiscordEmojiConverter.cs b/DSharpPlus.Commands/Converters/DiscordEmojiConverter.cs index 1ade3eabc4..a55b7ec55e 100644 --- a/DSharpPlus.Commands/Converters/DiscordEmojiConverter.cs +++ b/DSharpPlus.Commands/Converters/DiscordEmojiConverter.cs @@ -1,23 +1,23 @@ -using System.Threading.Tasks; -using DSharpPlus.Commands.Processors.SlashCommands; -using DSharpPlus.Commands.Processors.TextCommands; -using DSharpPlus.Entities; - -namespace DSharpPlus.Commands.Converters; - -public class DiscordEmojiConverter : ISlashArgumentConverter, ITextArgumentConverter -{ - public DiscordApplicationCommandOptionType ParameterType => DiscordApplicationCommandOptionType.String; - public ConverterInputType RequiresText => ConverterInputType.Always; - public string ReadableName => "Discord Emoji"; - - public Task> ConvertAsync(ConverterContext context) - { - string? value = context.Argument?.ToString(); - return !string.IsNullOrWhiteSpace(value) - // Unicode emoji's get priority - && (DiscordEmoji.TryFromUnicode(context.Client, value, out DiscordEmoji? emoji) || DiscordEmoji.TryFromName(context.Client, value, out emoji)) - ? Task.FromResult(Optional.FromValue(emoji)) - : Task.FromResult(Optional.FromNoValue()); - } -} +using System.Threading.Tasks; +using DSharpPlus.Commands.Processors.SlashCommands; +using DSharpPlus.Commands.Processors.TextCommands; +using DSharpPlus.Entities; + +namespace DSharpPlus.Commands.Converters; + +public class DiscordEmojiConverter : ISlashArgumentConverter, ITextArgumentConverter +{ + public DiscordApplicationCommandOptionType ParameterType => DiscordApplicationCommandOptionType.String; + public ConverterInputType RequiresText => ConverterInputType.Always; + public string ReadableName => "Discord Emoji"; + + public Task> ConvertAsync(ConverterContext context) + { + string? value = context.Argument?.ToString(); + return !string.IsNullOrWhiteSpace(value) + // Unicode emoji's get priority + && (DiscordEmoji.TryFromUnicode(context.Client, value, out DiscordEmoji? emoji) || DiscordEmoji.TryFromName(context.Client, value, out emoji)) + ? Task.FromResult(Optional.FromValue(emoji)) + : Task.FromResult(Optional.FromNoValue()); + } +} diff --git a/DSharpPlus.Commands/Converters/DiscordMemberConverter.cs b/DSharpPlus.Commands/Converters/DiscordMemberConverter.cs index 8cfed05fc8..4ec8abe105 100644 --- a/DSharpPlus.Commands/Converters/DiscordMemberConverter.cs +++ b/DSharpPlus.Commands/Converters/DiscordMemberConverter.cs @@ -1,76 +1,76 @@ -using System; -using System.Globalization; -using System.Linq; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using DSharpPlus.Commands.Processors.SlashCommands; -using DSharpPlus.Commands.Processors.TextCommands; -using DSharpPlus.Entities; -using DSharpPlus.Exceptions; - -namespace DSharpPlus.Commands.Converters; - -public partial class DiscordMemberConverter : ISlashArgumentConverter, ITextArgumentConverter -{ - [GeneratedRegex("""^<@!?(\d+?)>$""", RegexOptions.Compiled | RegexOptions.ECMAScript)] - public static partial Regex GetMemberRegex(); - - public DiscordApplicationCommandOptionType ParameterType => DiscordApplicationCommandOptionType.User; - public ConverterInputType RequiresText => ConverterInputType.Always; - public string ReadableName => "Discord Server Member"; - - public async Task> ConvertAsync(ConverterContext context) - { - if (context is InteractionConverterContext interactionConverterContext - // Resolved can be null on autocomplete contexts - && interactionConverterContext.Interaction.Data.Resolved is not null - // Check if we can parse the member ID (this should be guaranteed by Discord) - && ulong.TryParse(interactionConverterContext.Argument?.RawValue, CultureInfo.InvariantCulture, out ulong memberId) - // Check if the member is in the resolved data - && interactionConverterContext.Interaction.Data.Resolved.Members.TryGetValue(memberId, out DiscordMember? member)) - { - return Optional.FromValue(member); - } - - // How the fuck are we gonna get a member from a null guild. - if (context.Guild is null) - { - return Optional.FromNoValue(); - } - - string? value = context.Argument?.ToString(); - if (string.IsNullOrWhiteSpace(value)) - { - return Optional.FromNoValue(); - } - - // Try parsing by the member id - if (!ulong.TryParse(value, CultureInfo.InvariantCulture, out memberId)) - { - // Try parsing through a member mention - Match match = GetMemberRegex().Match(value); - if (!match.Success || !ulong.TryParse(match.Groups[1].ValueSpan, NumberStyles.Number, CultureInfo.InvariantCulture, out memberId)) - { - // Try to find a member by name, case sensitive. - DiscordMember? namedMember = context.Guild.Members.Values.FirstOrDefault(member => member.DisplayName.Equals(value, StringComparison.Ordinal)); - return namedMember is not null - ? Optional.FromValue(namedMember) - : Optional.FromNoValue(); - } - } - - try - { - // GetMemberAsync will search the member cache first, then fetch the member from the API if not found. - member = await context.Guild.GetMemberAsync(memberId); - return member is not null - ? Optional.FromValue(member) - : Optional.FromNoValue(); - } - catch (DiscordException) - { - // Not logging because users can intentionally give us incorrect data to intentionally spam logs. - return Optional.FromNoValue(); - } - } -} +using System; +using System.Globalization; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using DSharpPlus.Commands.Processors.SlashCommands; +using DSharpPlus.Commands.Processors.TextCommands; +using DSharpPlus.Entities; +using DSharpPlus.Exceptions; + +namespace DSharpPlus.Commands.Converters; + +public partial class DiscordMemberConverter : ISlashArgumentConverter, ITextArgumentConverter +{ + [GeneratedRegex("""^<@!?(\d+?)>$""", RegexOptions.Compiled | RegexOptions.ECMAScript)] + public static partial Regex GetMemberRegex(); + + public DiscordApplicationCommandOptionType ParameterType => DiscordApplicationCommandOptionType.User; + public ConverterInputType RequiresText => ConverterInputType.Always; + public string ReadableName => "Discord Server Member"; + + public async Task> ConvertAsync(ConverterContext context) + { + if (context is InteractionConverterContext interactionConverterContext + // Resolved can be null on autocomplete contexts + && interactionConverterContext.Interaction.Data.Resolved is not null + // Check if we can parse the member ID (this should be guaranteed by Discord) + && ulong.TryParse(interactionConverterContext.Argument?.RawValue, CultureInfo.InvariantCulture, out ulong memberId) + // Check if the member is in the resolved data + && interactionConverterContext.Interaction.Data.Resolved.Members.TryGetValue(memberId, out DiscordMember? member)) + { + return Optional.FromValue(member); + } + + // How the fuck are we gonna get a member from a null guild. + if (context.Guild is null) + { + return Optional.FromNoValue(); + } + + string? value = context.Argument?.ToString(); + if (string.IsNullOrWhiteSpace(value)) + { + return Optional.FromNoValue(); + } + + // Try parsing by the member id + if (!ulong.TryParse(value, CultureInfo.InvariantCulture, out memberId)) + { + // Try parsing through a member mention + Match match = GetMemberRegex().Match(value); + if (!match.Success || !ulong.TryParse(match.Groups[1].ValueSpan, NumberStyles.Number, CultureInfo.InvariantCulture, out memberId)) + { + // Try to find a member by name, case sensitive. + DiscordMember? namedMember = context.Guild.Members.Values.FirstOrDefault(member => member.DisplayName.Equals(value, StringComparison.Ordinal)); + return namedMember is not null + ? Optional.FromValue(namedMember) + : Optional.FromNoValue(); + } + } + + try + { + // GetMemberAsync will search the member cache first, then fetch the member from the API if not found. + member = await context.Guild.GetMemberAsync(memberId); + return member is not null + ? Optional.FromValue(member) + : Optional.FromNoValue(); + } + catch (DiscordException) + { + // Not logging because users can intentionally give us incorrect data to intentionally spam logs. + return Optional.FromNoValue(); + } + } +} diff --git a/DSharpPlus.Commands/Converters/DiscordMessageConverter.cs b/DSharpPlus.Commands/Converters/DiscordMessageConverter.cs index a6c5881db0..04f5a1fe91 100644 --- a/DSharpPlus.Commands/Converters/DiscordMessageConverter.cs +++ b/DSharpPlus.Commands/Converters/DiscordMessageConverter.cs @@ -1,114 +1,114 @@ -using System.Globalization; -using System.Linq; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using DSharpPlus.Commands.Processors.SlashCommands; -using DSharpPlus.Commands.Processors.TextCommands; -using DSharpPlus.Commands.Processors.TextCommands.ContextChecks; -using DSharpPlus.Entities; -using DSharpPlus.Exceptions; - -namespace DSharpPlus.Commands.Converters; - -public partial class DiscordMessageConverter : ISlashArgumentConverter, ITextArgumentConverter -{ - [GeneratedRegex(@"\/channels\/(?(?:\d+|@me))\/(?\d+)\/(?\d+)\/?", RegexOptions.Compiled | RegexOptions.ECMAScript)] - public static partial Regex GetMessageRegex(); - - public DiscordApplicationCommandOptionType ParameterType => DiscordApplicationCommandOptionType.String; - public ConverterInputType RequiresText => ConverterInputType.IfReplyMissing; - public string ReadableName => "Discord Message Link"; - - public async Task> ConvertAsync(ConverterContext context) - { - // Check if the parameter desires a message reply - if (context is TextConverterContext textContext - && textContext.Parameter.Attributes.OfType().FirstOrDefault() is TextMessageReplyAttribute replyAttribute) - { - // It requested a reply and we have one available. - if (textContext.Message.ReferencedMessage is not null) - { - return Optional.FromValue(textContext.Message.ReferencedMessage); - } - // It required a reply and we don't have one. - else if (replyAttribute.RequiresReply) - { - return Optional.FromNoValue(); - } - - // It requested for a reply but we don't have one. - // Now try to parse the argument as a message link. - } - - string? value = context.Argument?.ToString(); - if (string.IsNullOrWhiteSpace(value)) - { - return Optional.FromNoValue(); - } - - Match match = GetMessageRegex().Match(value); - if (!match.Success - || !ulong.TryParse(match.Groups["message"].ValueSpan, CultureInfo.InvariantCulture, out ulong messageId) - || !match.Groups.TryGetValue("channel", out Group? channelGroup) - || !ulong.TryParse(channelGroup.ValueSpan, CultureInfo.InvariantCulture, out ulong channelId)) - { - // Check to see if it's just a normal message id. If it is, try setting the channel to the current channel. - if (ulong.TryParse(value, out messageId)) - { - channelId = context.Channel.Id; - } - else - { - // Try to see if it's Discord weird "Copy Message ID" format (channelId-messageId) - string[] parts = value.Split('-'); - if (parts.Length != 2 || !ulong.TryParse(parts[0], out channelId) || !ulong.TryParse(parts[1], out messageId)) - { - return Optional.FromNoValue(); - } - } - } - - DiscordChannel? channel = null; - if (match.Groups.TryGetValue("guild", out Group? guildGroup) - && ulong.TryParse(guildGroup.ValueSpan, NumberStyles.Number, CultureInfo.InvariantCulture, out ulong guildId) - && context.Client.Guilds.TryGetValue(guildId, out DiscordGuild? guild)) - { - // Make sure the message belongs to the guild - if (guild.Id != context.Guild!.Id) - { - return Optional.FromNoValue(); - } - // guildGroup is null which means the link used @me, which means DM's. At this point, we can only get the message if the DM is with the bot. - else if (guildGroup is null && channelId == context.Client.CurrentUser.Id) - { - channel = context.Client.PrivateChannels.TryGetValue(context.User.Id, out DiscordDmChannel? dmChannel) ? dmChannel : null; - } - else if (guild.Channels.TryGetValue(channelId, out DiscordChannel? guildChannel)) - { - channel = guildChannel; - } - else if (guild.Threads.TryGetValue(channelId, out DiscordThreadChannel? threadChannel)) - { - channel = threadChannel; - } - } - - if (channel is null) - { - return Optional.FromNoValue(); - } - - DiscordMessage? message; - try - { - message = await channel.GetMessageAsync(messageId); - } - catch (DiscordException) - { - // Not logging because users can intentionally give us incorrect data to intentionally spam logs. - return Optional.FromNoValue(); - } - - return Optional.FromValue(message); - } -} +using System.Globalization; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using DSharpPlus.Commands.Processors.SlashCommands; +using DSharpPlus.Commands.Processors.TextCommands; +using DSharpPlus.Commands.Processors.TextCommands.ContextChecks; +using DSharpPlus.Entities; +using DSharpPlus.Exceptions; + +namespace DSharpPlus.Commands.Converters; + +public partial class DiscordMessageConverter : ISlashArgumentConverter, ITextArgumentConverter +{ + [GeneratedRegex(@"\/channels\/(?(?:\d+|@me))\/(?\d+)\/(?\d+)\/?", RegexOptions.Compiled | RegexOptions.ECMAScript)] + public static partial Regex GetMessageRegex(); + + public DiscordApplicationCommandOptionType ParameterType => DiscordApplicationCommandOptionType.String; + public ConverterInputType RequiresText => ConverterInputType.IfReplyMissing; + public string ReadableName => "Discord Message Link"; + + public async Task> ConvertAsync(ConverterContext context) + { + // Check if the parameter desires a message reply + if (context is TextConverterContext textContext + && textContext.Parameter.Attributes.OfType().FirstOrDefault() is TextMessageReplyAttribute replyAttribute) + { + // It requested a reply and we have one available. + if (textContext.Message.ReferencedMessage is not null) + { + return Optional.FromValue(textContext.Message.ReferencedMessage); + } + // It required a reply and we don't have one. + else if (replyAttribute.RequiresReply) + { + return Optional.FromNoValue(); + } + + // It requested for a reply but we don't have one. + // Now try to parse the argument as a message link. + } + + string? value = context.Argument?.ToString(); + if (string.IsNullOrWhiteSpace(value)) + { + return Optional.FromNoValue(); + } + + Match match = GetMessageRegex().Match(value); + if (!match.Success + || !ulong.TryParse(match.Groups["message"].ValueSpan, CultureInfo.InvariantCulture, out ulong messageId) + || !match.Groups.TryGetValue("channel", out Group? channelGroup) + || !ulong.TryParse(channelGroup.ValueSpan, CultureInfo.InvariantCulture, out ulong channelId)) + { + // Check to see if it's just a normal message id. If it is, try setting the channel to the current channel. + if (ulong.TryParse(value, out messageId)) + { + channelId = context.Channel.Id; + } + else + { + // Try to see if it's Discord weird "Copy Message ID" format (channelId-messageId) + string[] parts = value.Split('-'); + if (parts.Length != 2 || !ulong.TryParse(parts[0], out channelId) || !ulong.TryParse(parts[1], out messageId)) + { + return Optional.FromNoValue(); + } + } + } + + DiscordChannel? channel = null; + if (match.Groups.TryGetValue("guild", out Group? guildGroup) + && ulong.TryParse(guildGroup.ValueSpan, NumberStyles.Number, CultureInfo.InvariantCulture, out ulong guildId) + && context.Client.Guilds.TryGetValue(guildId, out DiscordGuild? guild)) + { + // Make sure the message belongs to the guild + if (guild.Id != context.Guild!.Id) + { + return Optional.FromNoValue(); + } + // guildGroup is null which means the link used @me, which means DM's. At this point, we can only get the message if the DM is with the bot. + else if (guildGroup is null && channelId == context.Client.CurrentUser.Id) + { + channel = context.Client.PrivateChannels.TryGetValue(context.User.Id, out DiscordDmChannel? dmChannel) ? dmChannel : null; + } + else if (guild.Channels.TryGetValue(channelId, out DiscordChannel? guildChannel)) + { + channel = guildChannel; + } + else if (guild.Threads.TryGetValue(channelId, out DiscordThreadChannel? threadChannel)) + { + channel = threadChannel; + } + } + + if (channel is null) + { + return Optional.FromNoValue(); + } + + DiscordMessage? message; + try + { + message = await channel.GetMessageAsync(messageId); + } + catch (DiscordException) + { + // Not logging because users can intentionally give us incorrect data to intentionally spam logs. + return Optional.FromNoValue(); + } + + return Optional.FromValue(message); + } +} diff --git a/DSharpPlus.Commands/Converters/DiscordRoleConverter.cs b/DSharpPlus.Commands/Converters/DiscordRoleConverter.cs index 5e1a863aa0..7caa67672f 100644 --- a/DSharpPlus.Commands/Converters/DiscordRoleConverter.cs +++ b/DSharpPlus.Commands/Converters/DiscordRoleConverter.cs @@ -1,64 +1,64 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using DSharpPlus.Commands.Processors.SlashCommands; -using DSharpPlus.Commands.Processors.TextCommands; -using DSharpPlus.Entities; - -namespace DSharpPlus.Commands.Converters; - -public partial class DiscordRoleConverter : ISlashArgumentConverter, ITextArgumentConverter -{ - [GeneratedRegex(@"^<@&(\d+?)>$", RegexOptions.Compiled | RegexOptions.ECMAScript)] - public static partial Regex GetRoleRegex(); - - public DiscordApplicationCommandOptionType ParameterType => DiscordApplicationCommandOptionType.Role; - public ConverterInputType RequiresText => ConverterInputType.Always; - public string ReadableName => "Discord Role"; - - public Task> ConvertAsync(ConverterContext context) - { - if (context is InteractionConverterContext interactionContext - && interactionContext.Interaction.Data.Resolved is not null - && ulong.TryParse(interactionContext.Argument?.RawValue, CultureInfo.InvariantCulture, out ulong roleId) - && interactionContext.Interaction.Data.Resolved.Roles is not null - && interactionContext.Interaction.Data.Resolved.Roles.TryGetValue(roleId, out DiscordRole? role)) - { - return Task.FromResult(Optional.FromValue(role)); - } - - // We can't get a role if there's not a guild to look in. - if (context.Guild is null) - { - return Task.FromResult(Optional.FromNoValue()); - } - - string? value = context.Argument?.ToString(); - if (string.IsNullOrWhiteSpace(value)) - { - return Task.FromResult(Optional.FromNoValue()); - } - - // Try parsing the value as a role id. - if (!ulong.TryParse(value, CultureInfo.InvariantCulture, out roleId)) - { - // value can be a raw channel id or a channel mention. The regex will match both. - Match match = GetRoleRegex().Match(value); - if (!match.Success || !ulong.TryParse(match.Groups[1].ValueSpan, NumberStyles.Number, CultureInfo.InvariantCulture, out roleId)) - { - // Attempt to find a role by name, case sensitive. - DiscordRole? namedRole = context.Guild.Roles.Values.FirstOrDefault(role => role.Name.Equals(value, StringComparison.Ordinal)); - return namedRole is not null - ? Task.FromResult(Optional.FromValue(namedRole)) - : Task.FromResult(Optional.FromNoValue()); - } - } - - return context.Guild.Roles.GetValueOrDefault(roleId) is DiscordRole guildRole - ? Task.FromResult(Optional.FromValue(guildRole)) - : Task.FromResult(Optional.FromNoValue()); - } -} +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using DSharpPlus.Commands.Processors.SlashCommands; +using DSharpPlus.Commands.Processors.TextCommands; +using DSharpPlus.Entities; + +namespace DSharpPlus.Commands.Converters; + +public partial class DiscordRoleConverter : ISlashArgumentConverter, ITextArgumentConverter +{ + [GeneratedRegex(@"^<@&(\d+?)>$", RegexOptions.Compiled | RegexOptions.ECMAScript)] + public static partial Regex GetRoleRegex(); + + public DiscordApplicationCommandOptionType ParameterType => DiscordApplicationCommandOptionType.Role; + public ConverterInputType RequiresText => ConverterInputType.Always; + public string ReadableName => "Discord Role"; + + public Task> ConvertAsync(ConverterContext context) + { + if (context is InteractionConverterContext interactionContext + && interactionContext.Interaction.Data.Resolved is not null + && ulong.TryParse(interactionContext.Argument?.RawValue, CultureInfo.InvariantCulture, out ulong roleId) + && interactionContext.Interaction.Data.Resolved.Roles is not null + && interactionContext.Interaction.Data.Resolved.Roles.TryGetValue(roleId, out DiscordRole? role)) + { + return Task.FromResult(Optional.FromValue(role)); + } + + // We can't get a role if there's not a guild to look in. + if (context.Guild is null) + { + return Task.FromResult(Optional.FromNoValue()); + } + + string? value = context.Argument?.ToString(); + if (string.IsNullOrWhiteSpace(value)) + { + return Task.FromResult(Optional.FromNoValue()); + } + + // Try parsing the value as a role id. + if (!ulong.TryParse(value, CultureInfo.InvariantCulture, out roleId)) + { + // value can be a raw channel id or a channel mention. The regex will match both. + Match match = GetRoleRegex().Match(value); + if (!match.Success || !ulong.TryParse(match.Groups[1].ValueSpan, NumberStyles.Number, CultureInfo.InvariantCulture, out roleId)) + { + // Attempt to find a role by name, case sensitive. + DiscordRole? namedRole = context.Guild.Roles.Values.FirstOrDefault(role => role.Name.Equals(value, StringComparison.Ordinal)); + return namedRole is not null + ? Task.FromResult(Optional.FromValue(namedRole)) + : Task.FromResult(Optional.FromNoValue()); + } + } + + return context.Guild.Roles.GetValueOrDefault(roleId) is DiscordRole guildRole + ? Task.FromResult(Optional.FromValue(guildRole)) + : Task.FromResult(Optional.FromNoValue()); + } +} diff --git a/DSharpPlus.Commands/Converters/DiscordSnowflakeObjectConverter.cs b/DSharpPlus.Commands/Converters/DiscordSnowflakeObjectConverter.cs index da3f29b79f..f32588e19f 100644 --- a/DSharpPlus.Commands/Converters/DiscordSnowflakeObjectConverter.cs +++ b/DSharpPlus.Commands/Converters/DiscordSnowflakeObjectConverter.cs @@ -1,40 +1,40 @@ -using System.Threading.Tasks; -using DSharpPlus.Commands.Processors.SlashCommands; -using DSharpPlus.Commands.Processors.TextCommands; -using DSharpPlus.Entities; - -namespace DSharpPlus.Commands.Converters; - -public partial class DiscordSnowflakeObjectConverter : ISlashArgumentConverter, ITextArgumentConverter -{ - private static readonly DiscordMemberConverter discordMemberSlashArgumentConverter = new(); - private static readonly DiscordUserConverter discordUserSlashArgumentConverter = new(); - private static readonly DiscordRoleConverter discordRoleSlashArgumentConverter = new(); - - public DiscordApplicationCommandOptionType ParameterType => DiscordApplicationCommandOptionType.Mentionable; - public ConverterInputType RequiresText => ConverterInputType.Always; - public string ReadableName => "Discord User, Discord Member, or Discord Role"; - - public async Task> ConvertAsync(ConverterContext context) - { - // Checks through existing converters - // Check if it's a role first since that converter doesn't make any Rest API calls. - if (await discordRoleSlashArgumentConverter.ConvertAsync(context) is Optional role && role.HasValue) - { - return Optional.FromValue(role.Value); - } - // Check if it's a member since it's more likely the command invoker wants to mention a member instead of a random person. - else if (await discordMemberSlashArgumentConverter.ConvertAsync(context) is Optional member && member.HasValue) - { - return Optional.FromValue(member.Value); - } - // Finally fallback to checking if it's a user. - else if (await discordUserSlashArgumentConverter.ConvertAsync(context) is Optional user && user.HasValue) - { - return Optional.FromValue(user.Value); - } - - // What the fuck. - return Optional.FromNoValue(); - } -} +using System.Threading.Tasks; +using DSharpPlus.Commands.Processors.SlashCommands; +using DSharpPlus.Commands.Processors.TextCommands; +using DSharpPlus.Entities; + +namespace DSharpPlus.Commands.Converters; + +public partial class DiscordSnowflakeObjectConverter : ISlashArgumentConverter, ITextArgumentConverter +{ + private static readonly DiscordMemberConverter discordMemberSlashArgumentConverter = new(); + private static readonly DiscordUserConverter discordUserSlashArgumentConverter = new(); + private static readonly DiscordRoleConverter discordRoleSlashArgumentConverter = new(); + + public DiscordApplicationCommandOptionType ParameterType => DiscordApplicationCommandOptionType.Mentionable; + public ConverterInputType RequiresText => ConverterInputType.Always; + public string ReadableName => "Discord User, Discord Member, or Discord Role"; + + public async Task> ConvertAsync(ConverterContext context) + { + // Checks through existing converters + // Check if it's a role first since that converter doesn't make any Rest API calls. + if (await discordRoleSlashArgumentConverter.ConvertAsync(context) is Optional role && role.HasValue) + { + return Optional.FromValue(role.Value); + } + // Check if it's a member since it's more likely the command invoker wants to mention a member instead of a random person. + else if (await discordMemberSlashArgumentConverter.ConvertAsync(context) is Optional member && member.HasValue) + { + return Optional.FromValue(member.Value); + } + // Finally fallback to checking if it's a user. + else if (await discordUserSlashArgumentConverter.ConvertAsync(context) is Optional user && user.HasValue) + { + return Optional.FromValue(user.Value); + } + + // What the fuck. + return Optional.FromNoValue(); + } +} diff --git a/DSharpPlus.Commands/Converters/DiscordThreadChannelConverter.cs b/DSharpPlus.Commands/Converters/DiscordThreadChannelConverter.cs index 354f15d609..fa789f4db3 100644 --- a/DSharpPlus.Commands/Converters/DiscordThreadChannelConverter.cs +++ b/DSharpPlus.Commands/Converters/DiscordThreadChannelConverter.cs @@ -1,62 +1,62 @@ -using System; -using System.Globalization; -using System.Linq; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using DSharpPlus.Commands.Processors.SlashCommands; -using DSharpPlus.Commands.Processors.TextCommands; -using DSharpPlus.Entities; - -namespace DSharpPlus.Commands.Converters; - -public partial class DiscordThreadChannelConverter : ISlashArgumentConverter, ITextArgumentConverter -{ - public DiscordApplicationCommandOptionType ParameterType => DiscordApplicationCommandOptionType.Channel; - public ConverterInputType RequiresText => ConverterInputType.Always; - public string ReadableName => "Discord Thread"; - - public Task> ConvertAsync(ConverterContext context) - { - if (context is InteractionConverterContext interactionConverterContext - // Resolved can be null on autocomplete contexts - && interactionConverterContext.Interaction.Data.Resolved is not null - // Check if we can parse the channel ID (this should be guaranteed by Discord) - && ulong.TryParse(interactionConverterContext.Argument?.RawValue, CultureInfo.InvariantCulture, out ulong channelId) - // Check if the channel is in the resolved data - && interactionConverterContext.Interaction.Data.Resolved.Channels.TryGetValue(channelId, out DiscordChannel? channel)) - { - return Task.FromResult(Optional.FromValue((DiscordThreadChannel)channel)); - } - - // Threads cannot exist outside of guilds. - if (context.Guild is null) - { - return Task.FromResult(Optional.FromNoValue()); - } - - string? value = context.Argument?.ToString(); - if (string.IsNullOrWhiteSpace(value)) - { - return Task.FromResult(Optional.FromNoValue()); - } - - // Try parsing by the channel id - if (!ulong.TryParse(value, CultureInfo.InvariantCulture, out channelId)) - { - // value can be a raw channel id or a channel mention. The regex will match both. - Match match = DiscordChannelConverter.GetChannelMatchingRegex().Match(value); - if (!match.Success || !ulong.TryParse(match.Captures[0].ValueSpan, NumberStyles.Number, CultureInfo.InvariantCulture, out channelId)) - { - // Attempt to find a thread channel by name, case sensitive. - DiscordThreadChannel? namedChannel = context.Guild.Threads.Values.FirstOrDefault(channel => channel.Name.Equals(value, StringComparison.Ordinal)); - return namedChannel is not null - ? Task.FromResult(Optional.FromValue(namedChannel)) - : Task.FromResult(Optional.FromNoValue()); - } - } - - return context.Guild.Threads.TryGetValue(channelId, out DiscordThreadChannel? threadChannel) && threadChannel is not null - ? Task.FromResult(Optional.FromValue(threadChannel)) - : Task.FromResult(Optional.FromNoValue()); - } -} +using System; +using System.Globalization; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using DSharpPlus.Commands.Processors.SlashCommands; +using DSharpPlus.Commands.Processors.TextCommands; +using DSharpPlus.Entities; + +namespace DSharpPlus.Commands.Converters; + +public partial class DiscordThreadChannelConverter : ISlashArgumentConverter, ITextArgumentConverter +{ + public DiscordApplicationCommandOptionType ParameterType => DiscordApplicationCommandOptionType.Channel; + public ConverterInputType RequiresText => ConverterInputType.Always; + public string ReadableName => "Discord Thread"; + + public Task> ConvertAsync(ConverterContext context) + { + if (context is InteractionConverterContext interactionConverterContext + // Resolved can be null on autocomplete contexts + && interactionConverterContext.Interaction.Data.Resolved is not null + // Check if we can parse the channel ID (this should be guaranteed by Discord) + && ulong.TryParse(interactionConverterContext.Argument?.RawValue, CultureInfo.InvariantCulture, out ulong channelId) + // Check if the channel is in the resolved data + && interactionConverterContext.Interaction.Data.Resolved.Channels.TryGetValue(channelId, out DiscordChannel? channel)) + { + return Task.FromResult(Optional.FromValue((DiscordThreadChannel)channel)); + } + + // Threads cannot exist outside of guilds. + if (context.Guild is null) + { + return Task.FromResult(Optional.FromNoValue()); + } + + string? value = context.Argument?.ToString(); + if (string.IsNullOrWhiteSpace(value)) + { + return Task.FromResult(Optional.FromNoValue()); + } + + // Try parsing by the channel id + if (!ulong.TryParse(value, CultureInfo.InvariantCulture, out channelId)) + { + // value can be a raw channel id or a channel mention. The regex will match both. + Match match = DiscordChannelConverter.GetChannelMatchingRegex().Match(value); + if (!match.Success || !ulong.TryParse(match.Captures[0].ValueSpan, NumberStyles.Number, CultureInfo.InvariantCulture, out channelId)) + { + // Attempt to find a thread channel by name, case sensitive. + DiscordThreadChannel? namedChannel = context.Guild.Threads.Values.FirstOrDefault(channel => channel.Name.Equals(value, StringComparison.Ordinal)); + return namedChannel is not null + ? Task.FromResult(Optional.FromValue(namedChannel)) + : Task.FromResult(Optional.FromNoValue()); + } + } + + return context.Guild.Threads.TryGetValue(channelId, out DiscordThreadChannel? threadChannel) && threadChannel is not null + ? Task.FromResult(Optional.FromValue(threadChannel)) + : Task.FromResult(Optional.FromNoValue()); + } +} diff --git a/DSharpPlus.Commands/Converters/DiscordUserConverter.cs b/DSharpPlus.Commands/Converters/DiscordUserConverter.cs index 3e77fc329a..2e65e5e991 100644 --- a/DSharpPlus.Commands/Converters/DiscordUserConverter.cs +++ b/DSharpPlus.Commands/Converters/DiscordUserConverter.cs @@ -1,79 +1,79 @@ -using System; -using System.Globalization; -using System.Linq; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using DSharpPlus.Commands.Processors.SlashCommands; -using DSharpPlus.Commands.Processors.TextCommands; -using DSharpPlus.Entities; -using DSharpPlus.Exceptions; - -namespace DSharpPlus.Commands.Converters; - -public partial class DiscordUserConverter : ISlashArgumentConverter, ITextArgumentConverter -{ - [GeneratedRegex("""^<@!?(\d+?)>$""", RegexOptions.Compiled | RegexOptions.ECMAScript)] - public static partial Regex GetMemberRegex(); - - public DiscordApplicationCommandOptionType ParameterType => DiscordApplicationCommandOptionType.User; - public ConverterInputType RequiresText => ConverterInputType.Always; - public string ReadableName => "Discord User"; - - public async Task> ConvertAsync(ConverterContext context) - { - if (context is InteractionConverterContext interactionContext - // Resolved can be null on autocomplete contexts - && interactionContext.Interaction.Data.Resolved is not null - // Check if we can parse the member ID (this should be guaranteed by Discord) - && ulong.TryParse(interactionContext.Argument?.RawValue, CultureInfo.InvariantCulture, out ulong memberId) - // Check if the member is in the resolved data - && interactionContext.Interaction.Data.Resolved.Users.TryGetValue(memberId, out DiscordUser? user)) - { - return Optional.FromValue(user); - } - - string? value = context.Argument?.ToString(); - if (string.IsNullOrWhiteSpace(value)) - { - return Optional.FromNoValue(); - } - - // Try parsing by the member id - if (!ulong.TryParse(value, CultureInfo.InvariantCulture, out memberId)) - { - // Try parsing through a member mention - Match match = GetMemberRegex().Match(value); - if (!match.Success || !ulong.TryParse(match.Groups[1].ValueSpan, NumberStyles.Number, CultureInfo.InvariantCulture, out memberId)) - { - // If this is invoked in a guild, try to get the member first. - if (context.Guild is not null - && context.Guild.Members.Values.FirstOrDefault(member => member.DisplayName.Equals(value, StringComparison.Ordinal)) is DiscordMember namedMember - ) - { - // Attempt to find a member by name, case sensitive. - return Optional.FromValue(namedMember); - } - - // An invalid user id was passed and we couldn't find a member by name. - return Optional.FromNoValue(); - } - } - - // Search the guild cache first. We want to allow the dev to - // try casting to a member for the most amount of information available. - if (context.Guild is not null && context.Guild.Members.TryGetValue(memberId, out DiscordMember? member)) - { - return Optional.FromValue(member); - } - - // If we didn't find the user in the guild, try to get the user from the API. - try - { - return Optional.FromValue(await context.Client.GetUserAsync(memberId)); - } - catch (DiscordException) - { - return Optional.FromNoValue(); - } - } -} +using System; +using System.Globalization; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using DSharpPlus.Commands.Processors.SlashCommands; +using DSharpPlus.Commands.Processors.TextCommands; +using DSharpPlus.Entities; +using DSharpPlus.Exceptions; + +namespace DSharpPlus.Commands.Converters; + +public partial class DiscordUserConverter : ISlashArgumentConverter, ITextArgumentConverter +{ + [GeneratedRegex("""^<@!?(\d+?)>$""", RegexOptions.Compiled | RegexOptions.ECMAScript)] + public static partial Regex GetMemberRegex(); + + public DiscordApplicationCommandOptionType ParameterType => DiscordApplicationCommandOptionType.User; + public ConverterInputType RequiresText => ConverterInputType.Always; + public string ReadableName => "Discord User"; + + public async Task> ConvertAsync(ConverterContext context) + { + if (context is InteractionConverterContext interactionContext + // Resolved can be null on autocomplete contexts + && interactionContext.Interaction.Data.Resolved is not null + // Check if we can parse the member ID (this should be guaranteed by Discord) + && ulong.TryParse(interactionContext.Argument?.RawValue, CultureInfo.InvariantCulture, out ulong memberId) + // Check if the member is in the resolved data + && interactionContext.Interaction.Data.Resolved.Users.TryGetValue(memberId, out DiscordUser? user)) + { + return Optional.FromValue(user); + } + + string? value = context.Argument?.ToString(); + if (string.IsNullOrWhiteSpace(value)) + { + return Optional.FromNoValue(); + } + + // Try parsing by the member id + if (!ulong.TryParse(value, CultureInfo.InvariantCulture, out memberId)) + { + // Try parsing through a member mention + Match match = GetMemberRegex().Match(value); + if (!match.Success || !ulong.TryParse(match.Groups[1].ValueSpan, NumberStyles.Number, CultureInfo.InvariantCulture, out memberId)) + { + // If this is invoked in a guild, try to get the member first. + if (context.Guild is not null + && context.Guild.Members.Values.FirstOrDefault(member => member.DisplayName.Equals(value, StringComparison.Ordinal)) is DiscordMember namedMember + ) + { + // Attempt to find a member by name, case sensitive. + return Optional.FromValue(namedMember); + } + + // An invalid user id was passed and we couldn't find a member by name. + return Optional.FromNoValue(); + } + } + + // Search the guild cache first. We want to allow the dev to + // try casting to a member for the most amount of information available. + if (context.Guild is not null && context.Guild.Members.TryGetValue(memberId, out DiscordMember? member)) + { + return Optional.FromValue(member); + } + + // If we didn't find the user in the guild, try to get the user from the API. + try + { + return Optional.FromValue(await context.Client.GetUserAsync(memberId)); + } + catch (DiscordException) + { + return Optional.FromNoValue(); + } + } +} diff --git a/DSharpPlus.Commands/Converters/DoubleConverter.cs b/DSharpPlus.Commands/Converters/DoubleConverter.cs index b3c0cb33db..8dd49d14fa 100644 --- a/DSharpPlus.Commands/Converters/DoubleConverter.cs +++ b/DSharpPlus.Commands/Converters/DoubleConverter.cs @@ -1,19 +1,19 @@ -using System.Globalization; -using System.Threading.Tasks; -using DSharpPlus.Commands.Processors.SlashCommands; -using DSharpPlus.Commands.Processors.TextCommands; -using DSharpPlus.Entities; - -namespace DSharpPlus.Commands.Converters; - -public class DoubleConverter : ISlashArgumentConverter, ITextArgumentConverter -{ - public DiscordApplicationCommandOptionType ParameterType => DiscordApplicationCommandOptionType.Number; - public ConverterInputType RequiresText => ConverterInputType.Always; - public string ReadableName => "Decimal Number"; - - public Task> ConvertAsync(ConverterContext context) => - double.TryParse(context.Argument?.ToString(), CultureInfo.InvariantCulture, out double result) - ? Task.FromResult(Optional.FromValue(result)) - : Task.FromResult(Optional.FromNoValue()); -} +using System.Globalization; +using System.Threading.Tasks; +using DSharpPlus.Commands.Processors.SlashCommands; +using DSharpPlus.Commands.Processors.TextCommands; +using DSharpPlus.Entities; + +namespace DSharpPlus.Commands.Converters; + +public class DoubleConverter : ISlashArgumentConverter, ITextArgumentConverter +{ + public DiscordApplicationCommandOptionType ParameterType => DiscordApplicationCommandOptionType.Number; + public ConverterInputType RequiresText => ConverterInputType.Always; + public string ReadableName => "Decimal Number"; + + public Task> ConvertAsync(ConverterContext context) => + double.TryParse(context.Argument?.ToString(), CultureInfo.InvariantCulture, out double result) + ? Task.FromResult(Optional.FromValue(result)) + : Task.FromResult(Optional.FromNoValue()); +} diff --git a/DSharpPlus.Commands/Converters/EnumArgumentConverter.cs b/DSharpPlus.Commands/Converters/EnumArgumentConverter.cs index 6951acdbb6..2f2b0fe35b 100644 --- a/DSharpPlus.Commands/Converters/EnumArgumentConverter.cs +++ b/DSharpPlus.Commands/Converters/EnumArgumentConverter.cs @@ -1,38 +1,38 @@ -using System; -using System.Globalization; -using System.Threading.Tasks; -using DSharpPlus.Commands.Processors.SlashCommands; -using DSharpPlus.Commands.Processors.TextCommands; -using DSharpPlus.Entities; - -namespace DSharpPlus.Commands.Converters; - -public class EnumConverter : ISlashArgumentConverter, ITextArgumentConverter -{ - public DiscordApplicationCommandOptionType ParameterType => DiscordApplicationCommandOptionType.Integer; - public ConverterInputType RequiresText => ConverterInputType.Always; - public string ReadableName => "Multiple Choice"; - - public Task> ConvertAsync(ConverterContext context) - { - // The parameter type could be an Enum? or an Enum[] or an Enum?[] or an Enum[][]. You get it. - Type enumType = IArgumentConverter.GetConverterFriendlyBaseType(context.Parameter.Type); - if (context.Argument is string stringArgument) - { - return Enum.TryParse(enumType, stringArgument, true, out object? result) - ? Task.FromResult(Optional.FromValue((Enum)result)) - : Task.FromResult(Optional.FromNoValue()); - } - - // Figure out what the base type of Enum actually is (int, long, byte, etc). - Type baseEnumType = Enum.GetUnderlyingType(enumType); - - // Convert the argument to the base type of the enum. If this was invoked via slash commands, - // Discord will send us the argument as a number, which STJ will convert to an unknown numeric type. - // We need to ensure that the argument is the same type as it's enum base type. - object? value = Convert.ChangeType(context.Argument, baseEnumType, CultureInfo.InvariantCulture); - return value is not null && Enum.IsDefined(enumType, value) - ? Task.FromResult(Optional.FromValue((Enum)Enum.ToObject(enumType, value))) - : Task.FromResult(Optional.FromNoValue()); - } -} +using System; +using System.Globalization; +using System.Threading.Tasks; +using DSharpPlus.Commands.Processors.SlashCommands; +using DSharpPlus.Commands.Processors.TextCommands; +using DSharpPlus.Entities; + +namespace DSharpPlus.Commands.Converters; + +public class EnumConverter : ISlashArgumentConverter, ITextArgumentConverter +{ + public DiscordApplicationCommandOptionType ParameterType => DiscordApplicationCommandOptionType.Integer; + public ConverterInputType RequiresText => ConverterInputType.Always; + public string ReadableName => "Multiple Choice"; + + public Task> ConvertAsync(ConverterContext context) + { + // The parameter type could be an Enum? or an Enum[] or an Enum?[] or an Enum[][]. You get it. + Type enumType = IArgumentConverter.GetConverterFriendlyBaseType(context.Parameter.Type); + if (context.Argument is string stringArgument) + { + return Enum.TryParse(enumType, stringArgument, true, out object? result) + ? Task.FromResult(Optional.FromValue((Enum)result)) + : Task.FromResult(Optional.FromNoValue()); + } + + // Figure out what the base type of Enum actually is (int, long, byte, etc). + Type baseEnumType = Enum.GetUnderlyingType(enumType); + + // Convert the argument to the base type of the enum. If this was invoked via slash commands, + // Discord will send us the argument as a number, which STJ will convert to an unknown numeric type. + // We need to ensure that the argument is the same type as it's enum base type. + object? value = Convert.ChangeType(context.Argument, baseEnumType, CultureInfo.InvariantCulture); + return value is not null && Enum.IsDefined(enumType, value) + ? Task.FromResult(Optional.FromValue((Enum)Enum.ToObject(enumType, value))) + : Task.FromResult(Optional.FromNoValue()); + } +} diff --git a/DSharpPlus.Commands/Converters/FloatConverter.cs b/DSharpPlus.Commands/Converters/FloatConverter.cs index b57c980797..9b0a55c2cf 100644 --- a/DSharpPlus.Commands/Converters/FloatConverter.cs +++ b/DSharpPlus.Commands/Converters/FloatConverter.cs @@ -1,19 +1,19 @@ -using System.Globalization; -using System.Threading.Tasks; -using DSharpPlus.Commands.Processors.SlashCommands; -using DSharpPlus.Commands.Processors.TextCommands; -using DSharpPlus.Entities; - -namespace DSharpPlus.Commands.Converters; - -public class FloatConverter : ISlashArgumentConverter, ITextArgumentConverter -{ - public DiscordApplicationCommandOptionType ParameterType => DiscordApplicationCommandOptionType.Number; - public ConverterInputType RequiresText => ConverterInputType.Always; - public string ReadableName => "Decimal Number"; - - public Task> ConvertAsync(ConverterContext context) => - float.TryParse(context.Argument?.ToString(), CultureInfo.InvariantCulture, out float result) - ? Task.FromResult(Optional.FromValue(result)) - : Task.FromResult(Optional.FromNoValue()); -} +using System.Globalization; +using System.Threading.Tasks; +using DSharpPlus.Commands.Processors.SlashCommands; +using DSharpPlus.Commands.Processors.TextCommands; +using DSharpPlus.Entities; + +namespace DSharpPlus.Commands.Converters; + +public class FloatConverter : ISlashArgumentConverter, ITextArgumentConverter +{ + public DiscordApplicationCommandOptionType ParameterType => DiscordApplicationCommandOptionType.Number; + public ConverterInputType RequiresText => ConverterInputType.Always; + public string ReadableName => "Decimal Number"; + + public Task> ConvertAsync(ConverterContext context) => + float.TryParse(context.Argument?.ToString(), CultureInfo.InvariantCulture, out float result) + ? Task.FromResult(Optional.FromValue(result)) + : Task.FromResult(Optional.FromNoValue()); +} diff --git a/DSharpPlus.Commands/Converters/IArgumentConverter.cs b/DSharpPlus.Commands/Converters/IArgumentConverter.cs index 4976f31311..8fffa2b905 100644 --- a/DSharpPlus.Commands/Converters/IArgumentConverter.cs +++ b/DSharpPlus.Commands/Converters/IArgumentConverter.cs @@ -1,47 +1,47 @@ -using System; -using System.Threading.Tasks; -using DSharpPlus.Entities; - -namespace DSharpPlus.Commands.Converters; - -public interface IArgumentConverter -{ - public string ReadableName { get; } - - /// - /// Finds the base type to use for converter registration. - /// - /// - /// More specifically, this methods returns the base type that can be found from , , or 's. - /// - /// The type to find the base type for. - /// The base type to use for converter registration. - public static Type GetConverterFriendlyBaseType(Type type) - { - ArgumentNullException.ThrowIfNull(type, nameof(type)); - - Type effectiveType = Nullable.GetUnderlyingType(type) ?? type; - if (effectiveType.IsArray) - { - // The type could be an array of enums or nullable - // objects or worse: an array of arrays. - return GetConverterFriendlyBaseType(effectiveType.GetElementType()!); - } - - return effectiveType; - } -} - -/// -/// Converts an argument to a desired type. -/// -/// The type to convert the argument to. -public interface IArgumentConverter : IArgumentConverter -{ - /// - /// Converts the argument to the desired type. - /// - /// The context for this conversion. - /// An optional containing the converted value, or an empty optional if the conversion failed. - public Task> ConvertAsync(ConverterContext context); -} +using System; +using System.Threading.Tasks; +using DSharpPlus.Entities; + +namespace DSharpPlus.Commands.Converters; + +public interface IArgumentConverter +{ + public string ReadableName { get; } + + /// + /// Finds the base type to use for converter registration. + /// + /// + /// More specifically, this methods returns the base type that can be found from , , or 's. + /// + /// The type to find the base type for. + /// The base type to use for converter registration. + public static Type GetConverterFriendlyBaseType(Type type) + { + ArgumentNullException.ThrowIfNull(type, nameof(type)); + + Type effectiveType = Nullable.GetUnderlyingType(type) ?? type; + if (effectiveType.IsArray) + { + // The type could be an array of enums or nullable + // objects or worse: an array of arrays. + return GetConverterFriendlyBaseType(effectiveType.GetElementType()!); + } + + return effectiveType; + } +} + +/// +/// Converts an argument to a desired type. +/// +/// The type to convert the argument to. +public interface IArgumentConverter : IArgumentConverter +{ + /// + /// Converts the argument to the desired type. + /// + /// The context for this conversion. + /// An optional containing the converted value, or an empty optional if the conversion failed. + public Task> ConvertAsync(ConverterContext context); +} diff --git a/DSharpPlus.Commands/Converters/Int16Converter.cs b/DSharpPlus.Commands/Converters/Int16Converter.cs index df34f940a6..f3088ca148 100644 --- a/DSharpPlus.Commands/Converters/Int16Converter.cs +++ b/DSharpPlus.Commands/Converters/Int16Converter.cs @@ -1,19 +1,19 @@ -using System.Globalization; -using System.Threading.Tasks; -using DSharpPlus.Commands.Processors.SlashCommands; -using DSharpPlus.Commands.Processors.TextCommands; -using DSharpPlus.Entities; - -namespace DSharpPlus.Commands.Converters; - -public class Int16Converter : ISlashArgumentConverter, ITextArgumentConverter -{ - public DiscordApplicationCommandOptionType ParameterType => DiscordApplicationCommandOptionType.Integer; - public ConverterInputType RequiresText => ConverterInputType.Always; - public string ReadableName => "Small Integer"; - - public Task> ConvertAsync(ConverterContext context) => - short.TryParse(context.Argument?.ToString(), CultureInfo.InvariantCulture, out short result) - ? Task.FromResult(Optional.FromValue(result)) - : Task.FromResult(Optional.FromNoValue()); -} +using System.Globalization; +using System.Threading.Tasks; +using DSharpPlus.Commands.Processors.SlashCommands; +using DSharpPlus.Commands.Processors.TextCommands; +using DSharpPlus.Entities; + +namespace DSharpPlus.Commands.Converters; + +public class Int16Converter : ISlashArgumentConverter, ITextArgumentConverter +{ + public DiscordApplicationCommandOptionType ParameterType => DiscordApplicationCommandOptionType.Integer; + public ConverterInputType RequiresText => ConverterInputType.Always; + public string ReadableName => "Small Integer"; + + public Task> ConvertAsync(ConverterContext context) => + short.TryParse(context.Argument?.ToString(), CultureInfo.InvariantCulture, out short result) + ? Task.FromResult(Optional.FromValue(result)) + : Task.FromResult(Optional.FromNoValue()); +} diff --git a/DSharpPlus.Commands/Converters/Int32Converter.cs b/DSharpPlus.Commands/Converters/Int32Converter.cs index 48f4011010..4008572b27 100644 --- a/DSharpPlus.Commands/Converters/Int32Converter.cs +++ b/DSharpPlus.Commands/Converters/Int32Converter.cs @@ -1,19 +1,19 @@ -using System.Globalization; -using System.Threading.Tasks; -using DSharpPlus.Commands.Processors.SlashCommands; -using DSharpPlus.Commands.Processors.TextCommands; -using DSharpPlus.Entities; - -namespace DSharpPlus.Commands.Converters; - -public class Int32Converter : ISlashArgumentConverter, ITextArgumentConverter -{ - public DiscordApplicationCommandOptionType ParameterType => DiscordApplicationCommandOptionType.Integer; - public ConverterInputType RequiresText => ConverterInputType.Always; - public string ReadableName => "Integer"; - - public Task> ConvertAsync(ConverterContext context) => - int.TryParse(context.Argument?.ToString(), CultureInfo.InvariantCulture, out int result) - ? Task.FromResult(Optional.FromValue(result)) - : Task.FromResult(Optional.FromNoValue()); -} +using System.Globalization; +using System.Threading.Tasks; +using DSharpPlus.Commands.Processors.SlashCommands; +using DSharpPlus.Commands.Processors.TextCommands; +using DSharpPlus.Entities; + +namespace DSharpPlus.Commands.Converters; + +public class Int32Converter : ISlashArgumentConverter, ITextArgumentConverter +{ + public DiscordApplicationCommandOptionType ParameterType => DiscordApplicationCommandOptionType.Integer; + public ConverterInputType RequiresText => ConverterInputType.Always; + public string ReadableName => "Integer"; + + public Task> ConvertAsync(ConverterContext context) => + int.TryParse(context.Argument?.ToString(), CultureInfo.InvariantCulture, out int result) + ? Task.FromResult(Optional.FromValue(result)) + : Task.FromResult(Optional.FromNoValue()); +} diff --git a/DSharpPlus.Commands/Converters/Int64Converter.cs b/DSharpPlus.Commands/Converters/Int64Converter.cs index ecd2ab3238..59edb15a22 100644 --- a/DSharpPlus.Commands/Converters/Int64Converter.cs +++ b/DSharpPlus.Commands/Converters/Int64Converter.cs @@ -1,22 +1,22 @@ -using System.Globalization; -using System.Threading.Tasks; -using DSharpPlus.Commands.Processors.SlashCommands; -using DSharpPlus.Commands.Processors.TextCommands; -using DSharpPlus.Entities; - -namespace DSharpPlus.Commands.Converters; - -public class Int64Converter : ISlashArgumentConverter, ITextArgumentConverter -{ - // Discord: 9,007,199,254,740,992 - // Int64.MaxValue: 9,223,372,036,854,775,807 - // The input is defined as a string to allow for the use of the "long" type. - public DiscordApplicationCommandOptionType ParameterType => DiscordApplicationCommandOptionType.String; - public ConverterInputType RequiresText => ConverterInputType.Always; - public string ReadableName => "Large Integer"; - - public Task> ConvertAsync(ConverterContext context) => - long.TryParse(context.Argument?.ToString(), CultureInfo.InvariantCulture, out long result) - ? Task.FromResult(Optional.FromValue(result)) - : Task.FromResult(Optional.FromNoValue()); -} +using System.Globalization; +using System.Threading.Tasks; +using DSharpPlus.Commands.Processors.SlashCommands; +using DSharpPlus.Commands.Processors.TextCommands; +using DSharpPlus.Entities; + +namespace DSharpPlus.Commands.Converters; + +public class Int64Converter : ISlashArgumentConverter, ITextArgumentConverter +{ + // Discord: 9,007,199,254,740,992 + // Int64.MaxValue: 9,223,372,036,854,775,807 + // The input is defined as a string to allow for the use of the "long" type. + public DiscordApplicationCommandOptionType ParameterType => DiscordApplicationCommandOptionType.String; + public ConverterInputType RequiresText => ConverterInputType.Always; + public string ReadableName => "Large Integer"; + + public Task> ConvertAsync(ConverterContext context) => + long.TryParse(context.Argument?.ToString(), CultureInfo.InvariantCulture, out long result) + ? Task.FromResult(Optional.FromValue(result)) + : Task.FromResult(Optional.FromNoValue()); +} diff --git a/DSharpPlus.Commands/Converters/Results/ArgumentFailedConversionResult.cs b/DSharpPlus.Commands/Converters/Results/ArgumentFailedConversionResult.cs index 1c1c7f0f1b..a263c238af 100644 --- a/DSharpPlus.Commands/Converters/Results/ArgumentFailedConversionResult.cs +++ b/DSharpPlus.Commands/Converters/Results/ArgumentFailedConversionResult.cs @@ -1,19 +1,19 @@ -using System; - -namespace DSharpPlus.Commands.Converters.Results; - -/// -/// This class represents an argument that failed to convert during the argument conversion process. -/// -public class ArgumentFailedConversionResult -{ - /// - /// The exception that occurred during conversion, if any. - /// - public Exception? Error { get; init; } - - /// - /// The value that failed to convert. - /// - public object? Value { get; init; } -} +using System; + +namespace DSharpPlus.Commands.Converters.Results; + +/// +/// This class represents an argument that failed to convert during the argument conversion process. +/// +public class ArgumentFailedConversionResult +{ + /// + /// The exception that occurred during conversion, if any. + /// + public Exception? Error { get; init; } + + /// + /// The value that failed to convert. + /// + public object? Value { get; init; } +} diff --git a/DSharpPlus.Commands/Converters/Results/ArgumentNotParsedResult.cs b/DSharpPlus.Commands/Converters/Results/ArgumentNotParsedResult.cs index 50ca2ff504..205b443f43 100644 --- a/DSharpPlus.Commands/Converters/Results/ArgumentNotParsedResult.cs +++ b/DSharpPlus.Commands/Converters/Results/ArgumentNotParsedResult.cs @@ -1,6 +1,6 @@ -namespace DSharpPlus.Commands.Converters.Results; - -/// -/// This class represents an argument that was not parsed during the argument conversion process. -/// -public class ArgumentNotParsedResult; +namespace DSharpPlus.Commands.Converters.Results; + +/// +/// This class represents an argument that was not parsed during the argument conversion process. +/// +public class ArgumentNotParsedResult; diff --git a/DSharpPlus.Commands/Converters/SByteConverter.cs b/DSharpPlus.Commands/Converters/SByteConverter.cs index bcf2ec8eef..7557e0caa8 100644 --- a/DSharpPlus.Commands/Converters/SByteConverter.cs +++ b/DSharpPlus.Commands/Converters/SByteConverter.cs @@ -1,19 +1,19 @@ -using System.Globalization; -using System.Threading.Tasks; -using DSharpPlus.Commands.Processors.SlashCommands; -using DSharpPlus.Commands.Processors.TextCommands; -using DSharpPlus.Entities; - -namespace DSharpPlus.Commands.Converters; - -public class SByteConverter : ISlashArgumentConverter, ITextArgumentConverter -{ - public DiscordApplicationCommandOptionType ParameterType => DiscordApplicationCommandOptionType.Integer; - public ConverterInputType RequiresText => ConverterInputType.Always; - public string ReadableName => "Tiny Integer"; - - public Task> ConvertAsync(ConverterContext context) => - sbyte.TryParse(context.Argument?.ToString(), CultureInfo.InvariantCulture, out sbyte result) - ? Task.FromResult(Optional.FromValue(result)) - : Task.FromResult(Optional.FromNoValue()); -} +using System.Globalization; +using System.Threading.Tasks; +using DSharpPlus.Commands.Processors.SlashCommands; +using DSharpPlus.Commands.Processors.TextCommands; +using DSharpPlus.Entities; + +namespace DSharpPlus.Commands.Converters; + +public class SByteConverter : ISlashArgumentConverter, ITextArgumentConverter +{ + public DiscordApplicationCommandOptionType ParameterType => DiscordApplicationCommandOptionType.Integer; + public ConverterInputType RequiresText => ConverterInputType.Always; + public string ReadableName => "Tiny Integer"; + + public Task> ConvertAsync(ConverterContext context) => + sbyte.TryParse(context.Argument?.ToString(), CultureInfo.InvariantCulture, out sbyte result) + ? Task.FromResult(Optional.FromValue(result)) + : Task.FromResult(Optional.FromNoValue()); +} diff --git a/DSharpPlus.Commands/Converters/StringConverter.cs b/DSharpPlus.Commands/Converters/StringConverter.cs index 7d27f5ede1..2533e27d66 100644 --- a/DSharpPlus.Commands/Converters/StringConverter.cs +++ b/DSharpPlus.Commands/Converters/StringConverter.cs @@ -1,64 +1,64 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using System.Threading.Tasks; -using DSharpPlus.Commands.ArgumentModifiers; -using DSharpPlus.Commands.Processors.SlashCommands; -using DSharpPlus.Commands.Processors.TextCommands; -using DSharpPlus.Entities; - -namespace DSharpPlus.Commands.Converters; - -public class StringConverter : ISlashArgumentConverter, ITextArgumentConverter -{ - public DiscordApplicationCommandOptionType ParameterType => DiscordApplicationCommandOptionType.String; - public ConverterInputType RequiresText => ConverterInputType.Always; - public string ReadableName => "Text"; - - public Task> ConvertAsync(ConverterContext context) - { - string argument = context.Argument?.ToString() ?? ""; - foreach (Attribute attribute in context.Parameter.Attributes) - { - if (attribute is RemainingTextAttribute && context is TextConverterContext textConverterContext) - { - return Task.FromResult(Optional.FromValue(textConverterContext.RawArguments[textConverterContext.CurrentArgumentIndex..].TrimStart())); - } - else if (attribute is FromCodeAttribute codeAttribute) - { - return TryGetCodeBlock(argument, codeAttribute.CodeType, out string? code) - ? Task.FromResult(Optional.FromValue(code)) - : Task.FromResult(Optional.FromNoValue()); - } - } - - return Task.FromResult(Optional.FromValue(argument)); - } - - private static bool TryGetCodeBlock(string input, CodeType expectedCodeType, [NotNullWhen(true)] out string? code) - { - code = null; - ReadOnlySpan inputSpan = input.AsSpan(); - if (inputSpan.Length > 6 && inputSpan.StartsWith("```") && inputSpan.EndsWith("```") && expectedCodeType.HasFlag(CodeType.Codeblock)) - { - int index = inputSpan.IndexOf('\n'); - if (index == -1 || !FromCodeAttribute.CodeBlockLanguages.Contains(inputSpan[3..index].ToString())) - { - code = input[3..^3]; - return true; - } - - code = input[(index + 1)..^3]; - return true; - } - else if (inputSpan.Length > 4 && inputSpan.StartsWith("``") && inputSpan.EndsWith("``") && expectedCodeType.HasFlag(CodeType.Inline)) - { - code = input[2..^2]; - } - else if (inputSpan.Length > 2 && inputSpan.StartsWith("`") && inputSpan.EndsWith("`") && expectedCodeType.HasFlag(CodeType.Inline)) - { - code = input[1..^1]; - } - - return code is not null; - } -} +using System; +using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; +using DSharpPlus.Commands.ArgumentModifiers; +using DSharpPlus.Commands.Processors.SlashCommands; +using DSharpPlus.Commands.Processors.TextCommands; +using DSharpPlus.Entities; + +namespace DSharpPlus.Commands.Converters; + +public class StringConverter : ISlashArgumentConverter, ITextArgumentConverter +{ + public DiscordApplicationCommandOptionType ParameterType => DiscordApplicationCommandOptionType.String; + public ConverterInputType RequiresText => ConverterInputType.Always; + public string ReadableName => "Text"; + + public Task> ConvertAsync(ConverterContext context) + { + string argument = context.Argument?.ToString() ?? ""; + foreach (Attribute attribute in context.Parameter.Attributes) + { + if (attribute is RemainingTextAttribute && context is TextConverterContext textConverterContext) + { + return Task.FromResult(Optional.FromValue(textConverterContext.RawArguments[textConverterContext.CurrentArgumentIndex..].TrimStart())); + } + else if (attribute is FromCodeAttribute codeAttribute) + { + return TryGetCodeBlock(argument, codeAttribute.CodeType, out string? code) + ? Task.FromResult(Optional.FromValue(code)) + : Task.FromResult(Optional.FromNoValue()); + } + } + + return Task.FromResult(Optional.FromValue(argument)); + } + + private static bool TryGetCodeBlock(string input, CodeType expectedCodeType, [NotNullWhen(true)] out string? code) + { + code = null; + ReadOnlySpan inputSpan = input.AsSpan(); + if (inputSpan.Length > 6 && inputSpan.StartsWith("```") && inputSpan.EndsWith("```") && expectedCodeType.HasFlag(CodeType.Codeblock)) + { + int index = inputSpan.IndexOf('\n'); + if (index == -1 || !FromCodeAttribute.CodeBlockLanguages.Contains(inputSpan[3..index].ToString())) + { + code = input[3..^3]; + return true; + } + + code = input[(index + 1)..^3]; + return true; + } + else if (inputSpan.Length > 4 && inputSpan.StartsWith("``") && inputSpan.EndsWith("``") && expectedCodeType.HasFlag(CodeType.Inline)) + { + code = input[2..^2]; + } + else if (inputSpan.Length > 2 && inputSpan.StartsWith("`") && inputSpan.EndsWith("`") && expectedCodeType.HasFlag(CodeType.Inline)) + { + code = input[1..^1]; + } + + return code is not null; + } +} diff --git a/DSharpPlus.Commands/Converters/TimeSpanConverter.cs b/DSharpPlus.Commands/Converters/TimeSpanConverter.cs index 4ed90f4a66..4635d667e0 100644 --- a/DSharpPlus.Commands/Converters/TimeSpanConverter.cs +++ b/DSharpPlus.Commands/Converters/TimeSpanConverter.cs @@ -1,76 +1,76 @@ -using System; -using System.Globalization; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using DSharpPlus.Commands.Processors.SlashCommands; -using DSharpPlus.Commands.Processors.TextCommands; -using DSharpPlus.Entities; - -namespace DSharpPlus.Commands.Converters; - -public partial class TimeSpanConverter : ISlashArgumentConverter, ITextArgumentConverter -{ - [GeneratedRegex( - @"^((?\d+)y\s*)?((?\d+)mo\s*)?((?\d+)w\s*)?((?\d+)d\s*)?((?\d+)h\s*)?((?\d+)m\s*)?((?\d+)s\s*)?((?\d+)ms\s*)?((?\d+)(µs|us)\s*)?((?\d+)ns\s*)?$", - RegexOptions.ExplicitCapture | RegexOptions.Compiled | RegexOptions.RightToLeft | RegexOptions.CultureInvariant - )] - private static partial Regex GetTimeSpanRegex(); - - public DiscordApplicationCommandOptionType ParameterType => DiscordApplicationCommandOptionType.String; - public ConverterInputType RequiresText => ConverterInputType.Always; - public string ReadableName => "Duration"; - - public Task> ConvertAsync(ConverterContext context) - { - string? value = context.Argument?.ToString(); - if (string.IsNullOrWhiteSpace(value)) - { - return Task.FromResult(Optional.FromNoValue()); - } - else if (value == "0") - { - return Task.FromResult(Optional.FromValue(TimeSpan.Zero)); - } - else if (int.TryParse(value, CultureInfo.InvariantCulture, out _)) - { - return Task.FromResult(Optional.FromNoValue()); - } - else if (TimeSpan.TryParse(value, CultureInfo.InvariantCulture, out TimeSpan result)) - { - return Task.FromResult(Optional.FromValue(result)); - } - else - { - Match match = GetTimeSpanRegex().Match(value); - if (!match.Success) - { - return Task.FromResult(Optional.FromNoValue()); - } - - int years = match.Groups["years"].Success ? int.Parse(match.Groups["years"].Value, CultureInfo.InvariantCulture) : 0; - int months = match.Groups["months"].Success ? int.Parse(match.Groups["months"].Value, CultureInfo.InvariantCulture) : 0; - int weeks = match.Groups["weeks"].Success ? int.Parse(match.Groups["weeks"].Value, CultureInfo.InvariantCulture) : 0; - int days = match.Groups["days"].Success ? int.Parse(match.Groups["days"].Value, CultureInfo.InvariantCulture) : 0; - int hours = match.Groups["hours"].Success ? int.Parse(match.Groups["hours"].Value, CultureInfo.InvariantCulture) : 0; - int minutes = match.Groups["minutes"].Success ? int.Parse(match.Groups["minutes"].Value, CultureInfo.InvariantCulture) : 0; - int seconds = match.Groups["seconds"].Success ? int.Parse(match.Groups["seconds"].Value, CultureInfo.InvariantCulture) : 0; - int milliseconds = match.Groups["milliseconds"].Success ? int.Parse(match.Groups["milliseconds"].Value, CultureInfo.InvariantCulture) : 0; - int microseconds = match.Groups["microseconds"].Success ? int.Parse(match.Groups["microseconds"].Value, CultureInfo.InvariantCulture) : 0; - int nanoseconds = match.Groups["nanoseconds"].Success ? int.Parse(match.Groups["nanoseconds"].Value, CultureInfo.InvariantCulture) : 0; - result = new TimeSpan( - ticks: (years * TimeSpan.TicksPerDay * 365) - + (months * TimeSpan.TicksPerDay * 30) - + (weeks * TimeSpan.TicksPerDay * 7) - + (days * TimeSpan.TicksPerDay) - + (hours * TimeSpan.TicksPerHour) - + (minutes * TimeSpan.TicksPerMinute) - + (seconds * TimeSpan.TicksPerSecond) - + (milliseconds * TimeSpan.TicksPerMillisecond) - + (microseconds * TimeSpan.TicksPerMicrosecond) - + (nanoseconds * TimeSpan.NanosecondsPerTick) - ); - - return Task.FromResult(Optional.FromValue(result)); - } - } -} +using System; +using System.Globalization; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using DSharpPlus.Commands.Processors.SlashCommands; +using DSharpPlus.Commands.Processors.TextCommands; +using DSharpPlus.Entities; + +namespace DSharpPlus.Commands.Converters; + +public partial class TimeSpanConverter : ISlashArgumentConverter, ITextArgumentConverter +{ + [GeneratedRegex( + @"^((?\d+)y\s*)?((?\d+)mo\s*)?((?\d+)w\s*)?((?\d+)d\s*)?((?\d+)h\s*)?((?\d+)m\s*)?((?\d+)s\s*)?((?\d+)ms\s*)?((?\d+)(µs|us)\s*)?((?\d+)ns\s*)?$", + RegexOptions.ExplicitCapture | RegexOptions.Compiled | RegexOptions.RightToLeft | RegexOptions.CultureInvariant + )] + private static partial Regex GetTimeSpanRegex(); + + public DiscordApplicationCommandOptionType ParameterType => DiscordApplicationCommandOptionType.String; + public ConverterInputType RequiresText => ConverterInputType.Always; + public string ReadableName => "Duration"; + + public Task> ConvertAsync(ConverterContext context) + { + string? value = context.Argument?.ToString(); + if (string.IsNullOrWhiteSpace(value)) + { + return Task.FromResult(Optional.FromNoValue()); + } + else if (value == "0") + { + return Task.FromResult(Optional.FromValue(TimeSpan.Zero)); + } + else if (int.TryParse(value, CultureInfo.InvariantCulture, out _)) + { + return Task.FromResult(Optional.FromNoValue()); + } + else if (TimeSpan.TryParse(value, CultureInfo.InvariantCulture, out TimeSpan result)) + { + return Task.FromResult(Optional.FromValue(result)); + } + else + { + Match match = GetTimeSpanRegex().Match(value); + if (!match.Success) + { + return Task.FromResult(Optional.FromNoValue()); + } + + int years = match.Groups["years"].Success ? int.Parse(match.Groups["years"].Value, CultureInfo.InvariantCulture) : 0; + int months = match.Groups["months"].Success ? int.Parse(match.Groups["months"].Value, CultureInfo.InvariantCulture) : 0; + int weeks = match.Groups["weeks"].Success ? int.Parse(match.Groups["weeks"].Value, CultureInfo.InvariantCulture) : 0; + int days = match.Groups["days"].Success ? int.Parse(match.Groups["days"].Value, CultureInfo.InvariantCulture) : 0; + int hours = match.Groups["hours"].Success ? int.Parse(match.Groups["hours"].Value, CultureInfo.InvariantCulture) : 0; + int minutes = match.Groups["minutes"].Success ? int.Parse(match.Groups["minutes"].Value, CultureInfo.InvariantCulture) : 0; + int seconds = match.Groups["seconds"].Success ? int.Parse(match.Groups["seconds"].Value, CultureInfo.InvariantCulture) : 0; + int milliseconds = match.Groups["milliseconds"].Success ? int.Parse(match.Groups["milliseconds"].Value, CultureInfo.InvariantCulture) : 0; + int microseconds = match.Groups["microseconds"].Success ? int.Parse(match.Groups["microseconds"].Value, CultureInfo.InvariantCulture) : 0; + int nanoseconds = match.Groups["nanoseconds"].Success ? int.Parse(match.Groups["nanoseconds"].Value, CultureInfo.InvariantCulture) : 0; + result = new TimeSpan( + ticks: (years * TimeSpan.TicksPerDay * 365) + + (months * TimeSpan.TicksPerDay * 30) + + (weeks * TimeSpan.TicksPerDay * 7) + + (days * TimeSpan.TicksPerDay) + + (hours * TimeSpan.TicksPerHour) + + (minutes * TimeSpan.TicksPerMinute) + + (seconds * TimeSpan.TicksPerSecond) + + (milliseconds * TimeSpan.TicksPerMillisecond) + + (microseconds * TimeSpan.TicksPerMicrosecond) + + (nanoseconds * TimeSpan.NanosecondsPerTick) + ); + + return Task.FromResult(Optional.FromValue(result)); + } + } +} diff --git a/DSharpPlus.Commands/Converters/UInt16Converter.cs b/DSharpPlus.Commands/Converters/UInt16Converter.cs index b6945b8cb6..20e6b6cf26 100644 --- a/DSharpPlus.Commands/Converters/UInt16Converter.cs +++ b/DSharpPlus.Commands/Converters/UInt16Converter.cs @@ -1,19 +1,19 @@ -using System.Globalization; -using System.Threading.Tasks; -using DSharpPlus.Commands.Processors.SlashCommands; -using DSharpPlus.Commands.Processors.TextCommands; -using DSharpPlus.Entities; - -namespace DSharpPlus.Commands.Converters; - -public class UInt16Converter : ISlashArgumentConverter, ITextArgumentConverter -{ - public DiscordApplicationCommandOptionType ParameterType => DiscordApplicationCommandOptionType.Integer; - public ConverterInputType RequiresText => ConverterInputType.Always; - public string ReadableName => "Positive Small Integer"; - - public Task> ConvertAsync(ConverterContext context) => - ushort.TryParse(context.Argument?.ToString(), CultureInfo.InvariantCulture, out ushort result) - ? Task.FromResult(Optional.FromValue(result)) - : Task.FromResult(Optional.FromNoValue()); -} +using System.Globalization; +using System.Threading.Tasks; +using DSharpPlus.Commands.Processors.SlashCommands; +using DSharpPlus.Commands.Processors.TextCommands; +using DSharpPlus.Entities; + +namespace DSharpPlus.Commands.Converters; + +public class UInt16Converter : ISlashArgumentConverter, ITextArgumentConverter +{ + public DiscordApplicationCommandOptionType ParameterType => DiscordApplicationCommandOptionType.Integer; + public ConverterInputType RequiresText => ConverterInputType.Always; + public string ReadableName => "Positive Small Integer"; + + public Task> ConvertAsync(ConverterContext context) => + ushort.TryParse(context.Argument?.ToString(), CultureInfo.InvariantCulture, out ushort result) + ? Task.FromResult(Optional.FromValue(result)) + : Task.FromResult(Optional.FromNoValue()); +} diff --git a/DSharpPlus.Commands/Converters/UInt32Converter.cs b/DSharpPlus.Commands/Converters/UInt32Converter.cs index e83cb0f13a..b8c31ca547 100644 --- a/DSharpPlus.Commands/Converters/UInt32Converter.cs +++ b/DSharpPlus.Commands/Converters/UInt32Converter.cs @@ -1,19 +1,19 @@ -using System.Globalization; -using System.Threading.Tasks; -using DSharpPlus.Commands.Processors.SlashCommands; -using DSharpPlus.Commands.Processors.TextCommands; -using DSharpPlus.Entities; - -namespace DSharpPlus.Commands.Converters; - -public class UInt32Converter : ISlashArgumentConverter, ITextArgumentConverter -{ - public DiscordApplicationCommandOptionType ParameterType => DiscordApplicationCommandOptionType.Integer; - public ConverterInputType RequiresText => ConverterInputType.Always; - public string ReadableName => "Positive Integer"; - - public Task> ConvertAsync(ConverterContext context) => - uint.TryParse(context.Argument?.ToString(), CultureInfo.InvariantCulture, out uint result) - ? Task.FromResult(Optional.FromValue(result)) - : Task.FromResult(Optional.FromNoValue()); -} +using System.Globalization; +using System.Threading.Tasks; +using DSharpPlus.Commands.Processors.SlashCommands; +using DSharpPlus.Commands.Processors.TextCommands; +using DSharpPlus.Entities; + +namespace DSharpPlus.Commands.Converters; + +public class UInt32Converter : ISlashArgumentConverter, ITextArgumentConverter +{ + public DiscordApplicationCommandOptionType ParameterType => DiscordApplicationCommandOptionType.Integer; + public ConverterInputType RequiresText => ConverterInputType.Always; + public string ReadableName => "Positive Integer"; + + public Task> ConvertAsync(ConverterContext context) => + uint.TryParse(context.Argument?.ToString(), CultureInfo.InvariantCulture, out uint result) + ? Task.FromResult(Optional.FromValue(result)) + : Task.FromResult(Optional.FromNoValue()); +} diff --git a/DSharpPlus.Commands/Converters/UInt64Converter.cs b/DSharpPlus.Commands/Converters/UInt64Converter.cs index ce92697bf3..47fec6ce02 100644 --- a/DSharpPlus.Commands/Converters/UInt64Converter.cs +++ b/DSharpPlus.Commands/Converters/UInt64Converter.cs @@ -1,22 +1,22 @@ -using System.Globalization; -using System.Threading.Tasks; -using DSharpPlus.Commands.Processors.SlashCommands; -using DSharpPlus.Commands.Processors.TextCommands; -using DSharpPlus.Entities; - -namespace DSharpPlus.Commands.Converters; - -public class UInt64Converter : ISlashArgumentConverter, ITextArgumentConverter -{ - // Discord: 9,007,199,254,740,992 - // UInt64.MaxValue: 18,446,744,073,709,551,615 - // The input is defined as a string to allow for the use of the "ulong" type. - public DiscordApplicationCommandOptionType ParameterType => DiscordApplicationCommandOptionType.String; - public ConverterInputType RequiresText => ConverterInputType.Always; - public string ReadableName => "Positive Large Integer"; - - public Task> ConvertAsync(ConverterContext context) => - ulong.TryParse(context.Argument?.ToString(), CultureInfo.InvariantCulture, out ulong result) - ? Task.FromResult(Optional.FromValue(result)) - : Task.FromResult(Optional.FromNoValue()); -} +using System.Globalization; +using System.Threading.Tasks; +using DSharpPlus.Commands.Processors.SlashCommands; +using DSharpPlus.Commands.Processors.TextCommands; +using DSharpPlus.Entities; + +namespace DSharpPlus.Commands.Converters; + +public class UInt64Converter : ISlashArgumentConverter, ITextArgumentConverter +{ + // Discord: 9,007,199,254,740,992 + // UInt64.MaxValue: 18,446,744,073,709,551,615 + // The input is defined as a string to allow for the use of the "ulong" type. + public DiscordApplicationCommandOptionType ParameterType => DiscordApplicationCommandOptionType.String; + public ConverterInputType RequiresText => ConverterInputType.Always; + public string ReadableName => "Positive Large Integer"; + + public Task> ConvertAsync(ConverterContext context) => + ulong.TryParse(context.Argument?.ToString(), CultureInfo.InvariantCulture, out ulong result) + ? Task.FromResult(Optional.FromValue(result)) + : Task.FromResult(Optional.FromNoValue()); +} diff --git a/DSharpPlus.Commands/DefaultCommandExecutor.cs b/DSharpPlus.Commands/DefaultCommandExecutor.cs index 23af852b02..99e1d51561 100644 --- a/DSharpPlus.Commands/DefaultCommandExecutor.cs +++ b/DSharpPlus.Commands/DefaultCommandExecutor.cs @@ -1,366 +1,366 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Reflection; -using System.Runtime.ExceptionServices; -using System.Threading; -using System.Threading.Tasks; -using DSharpPlus.Commands.ContextChecks; -using DSharpPlus.Commands.ContextChecks.ParameterChecks; -using DSharpPlus.Commands.EventArgs; -using DSharpPlus.Commands.Exceptions; -using DSharpPlus.Commands.Invocation; -using DSharpPlus.Commands.Trees; -using Microsoft.Extensions.DependencyInjection; - -namespace DSharpPlus.Commands; - -public class DefaultCommandExecutor : ICommandExecutor -{ - /// - /// This dictionary contains all of the command wrappers intended to be used for bypassing the overhead of reflection and Task/ValueTask handling. - /// - protected readonly ConcurrentDictionary> commandWrappers = new(); - - /// - /// This method will ensure that the command is executable, execute all context checks, and then execute the command, and invoke the appropriate events. - /// - /// - /// If any exceptions caused by the command were to occur, they will be delegated to the event. - /// - /// The context of the command being executed. - /// The cancellation token to cancel the command execution. - public virtual async ValueTask ExecuteAsync(CommandContext context, CancellationToken cancellationToken = default) - { - // Do some safety checks - if (!IsCommandExecutable(context, out string? errorMessage)) - { - await InvokeCommandErroredEventAsync(context.Extension, new CommandErroredEventArgs() - { - Context = context, - Exception = new CommandNotExecutableException(context.Command, errorMessage), - CommandObject = null - }); - - return; - } - - // Execute all context checks and return any that failed. - IReadOnlyList failedChecks = await ExecuteContextChecksAsync(context); - if (failedChecks.Count > 0) - { - await InvokeCommandErroredEventAsync(context.Extension, new CommandErroredEventArgs() - { - Context = context, - Exception = new ChecksFailedException(failedChecks, context.Command), - CommandObject = null - }); - - return; - } - - IReadOnlyList failedParameterChecks = await ExecuteParameterChecksAsync(context); - if (failedParameterChecks.Count > 0) - { - await InvokeCommandErroredEventAsync(context.Extension, new CommandErroredEventArgs() - { - Context = context, - Exception = new ParameterChecksFailedException(failedParameterChecks, context.Command), - CommandObject = null - }); - - return; - } - - // Execute the command - (object? commandObject, Exception? error) = await ExecuteCoreAsync(context); - - // If the command threw an exception, invoke the CommandErrored event. - if (error is not null) - { - await InvokeCommandErroredEventAsync(context.Extension, new CommandErroredEventArgs() - { - Context = context, - Exception = error, - CommandObject = commandObject - }); - } - // Otherwise, invoke the CommandExecuted event. - else - { - await InvokeCommandExecutedEventAsync(context.Extension, new CommandExecutedEventArgs() - { - Context = context, - CommandObject = commandObject - }); - } - - // Dispose of the service scope if it was created. - context.ServiceScope.Dispose(); - } - - /// - /// Ensures the command is executable before attempting to execute it. - /// - /// - /// This does NOT execute any context checks. This only checks if the command is executable based on the number of arguments provided. - /// - /// The context of the command being executed. - /// Any error message that occurred during the check. - /// Whether the command can be executed. - protected virtual bool IsCommandExecutable(CommandContext context, [NotNullWhen(false)] out string? errorMessage) - { - if (context.Command.Method is null) - { - errorMessage = "Unable to execute a command that has no method. Is this command a group command?"; - return false; - } - else if (context.Command.Target is null && context.Command.Method.DeclaringType is null) - { - errorMessage = "Unable to execute a delegate that has no target or declaring type. Is this command a group command?"; - return false; - } - else if (context.Arguments.Count != context.Command.Parameters.Count) - { - errorMessage = "The number of arguments provided does not match the number of parameters the command expects."; - return false; - } - - errorMessage = null; - return true; - } - - /// - /// Executes any context checks tied - /// - /// - /// - protected virtual async ValueTask> ExecuteContextChecksAsync(CommandContext context) - { - // Execute all checks and return any that failed. - List failedChecks = []; - - // Reuse the same instance of UnconditionalCheckAttribute for all unconditional checks. - UnconditionalCheckAttribute unconditionalCheck = new(); - - // First, execute all unconditional checks - foreach (ContextCheckMapEntry entry in context.Extension.Checks) - { - // Users must implement the check that requests the UnconditionalCheckAttribute from IContextCheck - if (entry.AttributeType != typeof(UnconditionalCheckAttribute)) - { - continue; - } - - try - { - // Create the check instance - object check = ActivatorUtilities.CreateInstance(context.ServiceProvider, entry.CheckType); - - // Execute it - string? result = await entry.ExecuteCheckAsync(check, unconditionalCheck, context); - - // It failed, add it to the list and continue with the others - if (result is not null) - { - failedChecks.Add(new() - { - ContextCheckAttribute = unconditionalCheck, - ErrorMessage = result - }); - } - } - catch (Exception error) - { - failedChecks.Add(new() - { - ContextCheckAttribute = unconditionalCheck, - ErrorMessage = error.Message, - Exception = error - }); - } - } - - // Add all of the checks attached to the delegate first. - List checks = new(context.Command.Attributes.OfType()); - - // Add the parent's checks last so we can execute the checks in order. - Command? parent = context.Command.Parent; - while (parent is not null) - { - checks.AddRange(parent.Attributes.OfType()); - parent = parent.Parent; - } - - // If there are no checks, we can skip this step. - if (checks.Count == 0) - { - return []; - } - - // Reverse foreach so we execute the top-most command's checks first. - for (int i = checks.Count - 1; i >= 0; i--) - { - // Search for any checks that match the current check's type, as there can be multiple checks for the same attribute. - foreach (ContextCheckMapEntry entry in context.Extension.Checks) - { - ContextCheckAttribute checkAttribute = checks[i]; - - // Skip checks that don't match the current check's type. - if (entry.AttributeType != checkAttribute.GetType()) - { - continue; - } - - try - { - // Create the check instance - object check = ActivatorUtilities.CreateInstance(context.ServiceProvider, entry.CheckType); - - // Execute it - string? result = await entry.ExecuteCheckAsync(check, checkAttribute, context); - - // It failed, add it to the list and continue with the others - if (result is not null) - { - failedChecks.Add(new() - { - ContextCheckAttribute = checkAttribute, - ErrorMessage = result - }); - - continue; - } - } - // try/catch blocks are free until they catch - catch (Exception error) - { - failedChecks.Add(new() - { - ContextCheckAttribute = checkAttribute, - ErrorMessage = error.Message, - Exception = error - }); - } - } - } - - return failedChecks; - } - - public virtual async ValueTask> ExecuteParameterChecksAsync(CommandContext context) - { - List failedChecks = []; - - // iterate over all parameters and their attributes. - foreach (CommandParameter parameter in context.Command.Parameters) - { - foreach (ParameterCheckAttribute checkAttribute in parameter.Attributes.OfType()) - { - ParameterCheckInfo info = new(parameter, context.Arguments[parameter]); - - // execute each check, skipping over non-matching ones - foreach (ParameterCheckMapEntry entry in context.Extension.ParameterChecks) - { - if (entry.AttributeType != checkAttribute.GetType()) - { - continue; - } - - try - { - // create the check instance - object check = ActivatorUtilities.CreateInstance(context.ServiceProvider, entry.CheckType); - - // execute the check - string? result = await entry.ExecuteCheckAsync(check, checkAttribute, info, context); - - // it failed, add it to the list and continue with the others - if (result is not null) - { - failedChecks.Add(new() - { - ParameterCheckAttribute = checkAttribute, - ErrorMessage = result - }); - - continue; - } - } - // if an error occurred, add it to the list and continue, making sure to set the error message. - catch (Exception error) - { - failedChecks.Add(new() - { - ParameterCheckAttribute = checkAttribute, - ErrorMessage = error.Message, - Exception = error - }); - } - } - } - } - - return failedChecks; - } - - /// - /// This method will execute the command provided without any safety checks, context checks or event invocation. - /// - /// The context of the command being executed. - /// A tuple containing the command object and any error that occurred during execution. The command object may be null when the delegate is static and is from a static class. - public virtual async ValueTask<(object? CommandObject, Exception? Error)> ExecuteCoreAsync(CommandContext context) - { - // Keep the command object in scope so it can be accessed after the command has been executed. - object? commandObject = null; - - try - { - // If the class isn't static, we need to create an instance of it. - if (!context.Command.Method!.DeclaringType!.IsAbstract || !context.Command.Method.DeclaringType.IsSealed) - { - // The delegate's object was provided, so we can use that. - commandObject = context.Command.Target ?? ActivatorUtilities.CreateInstance(context.ServiceProvider, context.Command.Method.DeclaringType); - } - - // Grab the method that wraps Task/ValueTask execution. - if (!this.commandWrappers.TryGetValue(context.Command.Id, out Func? wrapper)) - { - wrapper = CommandEmitUtil.GetCommandInvocationFunc(context.Command.Method, context.Command.Target); - this.commandWrappers[context.Command.Id] = wrapper; - } - - // Execute the command and return the result. - await wrapper(commandObject, [context, .. context.Arguments.Values]); - return (commandObject, null); - } - catch (Exception error) - { - // The command threw. Unwrap the stack trace as much as we can to provide helpful information to the developer. - if (error is TargetInvocationException targetInvocationError && targetInvocationError.InnerException is not null) - { - error = ExceptionDispatchInfo.Capture(targetInvocationError.InnerException).SourceException; - } - - return (commandObject, error); - } - } - - /// - /// Invokes the event, which isn't normally exposed to the public API. - /// - /// The extension/shard that the event is being invoked on. - /// The event arguments to pass to the event. - protected virtual async ValueTask InvokeCommandErroredEventAsync(CommandsExtension extension, CommandErroredEventArgs eventArgs) - => await extension.commandErrored.InvokeAsync(extension, eventArgs); - - /// - /// Invokes the event, which isn't normally exposed to the public API. - /// - /// The extension/shard that the event is being invoked on. - /// The event arguments to pass to the event. - protected virtual async ValueTask InvokeCommandExecutedEventAsync(CommandsExtension extension, CommandExecutedEventArgs eventArgs) - => await extension.commandExecuted.InvokeAsync(extension, eventArgs); -} +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Reflection; +using System.Runtime.ExceptionServices; +using System.Threading; +using System.Threading.Tasks; +using DSharpPlus.Commands.ContextChecks; +using DSharpPlus.Commands.ContextChecks.ParameterChecks; +using DSharpPlus.Commands.EventArgs; +using DSharpPlus.Commands.Exceptions; +using DSharpPlus.Commands.Invocation; +using DSharpPlus.Commands.Trees; +using Microsoft.Extensions.DependencyInjection; + +namespace DSharpPlus.Commands; + +public class DefaultCommandExecutor : ICommandExecutor +{ + /// + /// This dictionary contains all of the command wrappers intended to be used for bypassing the overhead of reflection and Task/ValueTask handling. + /// + protected readonly ConcurrentDictionary> commandWrappers = new(); + + /// + /// This method will ensure that the command is executable, execute all context checks, and then execute the command, and invoke the appropriate events. + /// + /// + /// If any exceptions caused by the command were to occur, they will be delegated to the event. + /// + /// The context of the command being executed. + /// The cancellation token to cancel the command execution. + public virtual async ValueTask ExecuteAsync(CommandContext context, CancellationToken cancellationToken = default) + { + // Do some safety checks + if (!IsCommandExecutable(context, out string? errorMessage)) + { + await InvokeCommandErroredEventAsync(context.Extension, new CommandErroredEventArgs() + { + Context = context, + Exception = new CommandNotExecutableException(context.Command, errorMessage), + CommandObject = null + }); + + return; + } + + // Execute all context checks and return any that failed. + IReadOnlyList failedChecks = await ExecuteContextChecksAsync(context); + if (failedChecks.Count > 0) + { + await InvokeCommandErroredEventAsync(context.Extension, new CommandErroredEventArgs() + { + Context = context, + Exception = new ChecksFailedException(failedChecks, context.Command), + CommandObject = null + }); + + return; + } + + IReadOnlyList failedParameterChecks = await ExecuteParameterChecksAsync(context); + if (failedParameterChecks.Count > 0) + { + await InvokeCommandErroredEventAsync(context.Extension, new CommandErroredEventArgs() + { + Context = context, + Exception = new ParameterChecksFailedException(failedParameterChecks, context.Command), + CommandObject = null + }); + + return; + } + + // Execute the command + (object? commandObject, Exception? error) = await ExecuteCoreAsync(context); + + // If the command threw an exception, invoke the CommandErrored event. + if (error is not null) + { + await InvokeCommandErroredEventAsync(context.Extension, new CommandErroredEventArgs() + { + Context = context, + Exception = error, + CommandObject = commandObject + }); + } + // Otherwise, invoke the CommandExecuted event. + else + { + await InvokeCommandExecutedEventAsync(context.Extension, new CommandExecutedEventArgs() + { + Context = context, + CommandObject = commandObject + }); + } + + // Dispose of the service scope if it was created. + context.ServiceScope.Dispose(); + } + + /// + /// Ensures the command is executable before attempting to execute it. + /// + /// + /// This does NOT execute any context checks. This only checks if the command is executable based on the number of arguments provided. + /// + /// The context of the command being executed. + /// Any error message that occurred during the check. + /// Whether the command can be executed. + protected virtual bool IsCommandExecutable(CommandContext context, [NotNullWhen(false)] out string? errorMessage) + { + if (context.Command.Method is null) + { + errorMessage = "Unable to execute a command that has no method. Is this command a group command?"; + return false; + } + else if (context.Command.Target is null && context.Command.Method.DeclaringType is null) + { + errorMessage = "Unable to execute a delegate that has no target or declaring type. Is this command a group command?"; + return false; + } + else if (context.Arguments.Count != context.Command.Parameters.Count) + { + errorMessage = "The number of arguments provided does not match the number of parameters the command expects."; + return false; + } + + errorMessage = null; + return true; + } + + /// + /// Executes any context checks tied + /// + /// + /// + protected virtual async ValueTask> ExecuteContextChecksAsync(CommandContext context) + { + // Execute all checks and return any that failed. + List failedChecks = []; + + // Reuse the same instance of UnconditionalCheckAttribute for all unconditional checks. + UnconditionalCheckAttribute unconditionalCheck = new(); + + // First, execute all unconditional checks + foreach (ContextCheckMapEntry entry in context.Extension.Checks) + { + // Users must implement the check that requests the UnconditionalCheckAttribute from IContextCheck + if (entry.AttributeType != typeof(UnconditionalCheckAttribute)) + { + continue; + } + + try + { + // Create the check instance + object check = ActivatorUtilities.CreateInstance(context.ServiceProvider, entry.CheckType); + + // Execute it + string? result = await entry.ExecuteCheckAsync(check, unconditionalCheck, context); + + // It failed, add it to the list and continue with the others + if (result is not null) + { + failedChecks.Add(new() + { + ContextCheckAttribute = unconditionalCheck, + ErrorMessage = result + }); + } + } + catch (Exception error) + { + failedChecks.Add(new() + { + ContextCheckAttribute = unconditionalCheck, + ErrorMessage = error.Message, + Exception = error + }); + } + } + + // Add all of the checks attached to the delegate first. + List checks = new(context.Command.Attributes.OfType()); + + // Add the parent's checks last so we can execute the checks in order. + Command? parent = context.Command.Parent; + while (parent is not null) + { + checks.AddRange(parent.Attributes.OfType()); + parent = parent.Parent; + } + + // If there are no checks, we can skip this step. + if (checks.Count == 0) + { + return []; + } + + // Reverse foreach so we execute the top-most command's checks first. + for (int i = checks.Count - 1; i >= 0; i--) + { + // Search for any checks that match the current check's type, as there can be multiple checks for the same attribute. + foreach (ContextCheckMapEntry entry in context.Extension.Checks) + { + ContextCheckAttribute checkAttribute = checks[i]; + + // Skip checks that don't match the current check's type. + if (entry.AttributeType != checkAttribute.GetType()) + { + continue; + } + + try + { + // Create the check instance + object check = ActivatorUtilities.CreateInstance(context.ServiceProvider, entry.CheckType); + + // Execute it + string? result = await entry.ExecuteCheckAsync(check, checkAttribute, context); + + // It failed, add it to the list and continue with the others + if (result is not null) + { + failedChecks.Add(new() + { + ContextCheckAttribute = checkAttribute, + ErrorMessage = result + }); + + continue; + } + } + // try/catch blocks are free until they catch + catch (Exception error) + { + failedChecks.Add(new() + { + ContextCheckAttribute = checkAttribute, + ErrorMessage = error.Message, + Exception = error + }); + } + } + } + + return failedChecks; + } + + public virtual async ValueTask> ExecuteParameterChecksAsync(CommandContext context) + { + List failedChecks = []; + + // iterate over all parameters and their attributes. + foreach (CommandParameter parameter in context.Command.Parameters) + { + foreach (ParameterCheckAttribute checkAttribute in parameter.Attributes.OfType()) + { + ParameterCheckInfo info = new(parameter, context.Arguments[parameter]); + + // execute each check, skipping over non-matching ones + foreach (ParameterCheckMapEntry entry in context.Extension.ParameterChecks) + { + if (entry.AttributeType != checkAttribute.GetType()) + { + continue; + } + + try + { + // create the check instance + object check = ActivatorUtilities.CreateInstance(context.ServiceProvider, entry.CheckType); + + // execute the check + string? result = await entry.ExecuteCheckAsync(check, checkAttribute, info, context); + + // it failed, add it to the list and continue with the others + if (result is not null) + { + failedChecks.Add(new() + { + ParameterCheckAttribute = checkAttribute, + ErrorMessage = result + }); + + continue; + } + } + // if an error occurred, add it to the list and continue, making sure to set the error message. + catch (Exception error) + { + failedChecks.Add(new() + { + ParameterCheckAttribute = checkAttribute, + ErrorMessage = error.Message, + Exception = error + }); + } + } + } + } + + return failedChecks; + } + + /// + /// This method will execute the command provided without any safety checks, context checks or event invocation. + /// + /// The context of the command being executed. + /// A tuple containing the command object and any error that occurred during execution. The command object may be null when the delegate is static and is from a static class. + public virtual async ValueTask<(object? CommandObject, Exception? Error)> ExecuteCoreAsync(CommandContext context) + { + // Keep the command object in scope so it can be accessed after the command has been executed. + object? commandObject = null; + + try + { + // If the class isn't static, we need to create an instance of it. + if (!context.Command.Method!.DeclaringType!.IsAbstract || !context.Command.Method.DeclaringType.IsSealed) + { + // The delegate's object was provided, so we can use that. + commandObject = context.Command.Target ?? ActivatorUtilities.CreateInstance(context.ServiceProvider, context.Command.Method.DeclaringType); + } + + // Grab the method that wraps Task/ValueTask execution. + if (!this.commandWrappers.TryGetValue(context.Command.Id, out Func? wrapper)) + { + wrapper = CommandEmitUtil.GetCommandInvocationFunc(context.Command.Method, context.Command.Target); + this.commandWrappers[context.Command.Id] = wrapper; + } + + // Execute the command and return the result. + await wrapper(commandObject, [context, .. context.Arguments.Values]); + return (commandObject, null); + } + catch (Exception error) + { + // The command threw. Unwrap the stack trace as much as we can to provide helpful information to the developer. + if (error is TargetInvocationException targetInvocationError && targetInvocationError.InnerException is not null) + { + error = ExceptionDispatchInfo.Capture(targetInvocationError.InnerException).SourceException; + } + + return (commandObject, error); + } + } + + /// + /// Invokes the event, which isn't normally exposed to the public API. + /// + /// The extension/shard that the event is being invoked on. + /// The event arguments to pass to the event. + protected virtual async ValueTask InvokeCommandErroredEventAsync(CommandsExtension extension, CommandErroredEventArgs eventArgs) + => await extension.commandErrored.InvokeAsync(extension, eventArgs); + + /// + /// Invokes the event, which isn't normally exposed to the public API. + /// + /// The extension/shard that the event is being invoked on. + /// The event arguments to pass to the event. + protected virtual async ValueTask InvokeCommandExecutedEventAsync(CommandsExtension extension, CommandExecutedEventArgs eventArgs) + => await extension.commandExecuted.InvokeAsync(extension, eventArgs); +} diff --git a/DSharpPlus.Commands/EventArgs/CommandErroredEventArgs.cs b/DSharpPlus.Commands/EventArgs/CommandErroredEventArgs.cs index 71cc505fe7..71442b4770 100644 --- a/DSharpPlus.Commands/EventArgs/CommandErroredEventArgs.cs +++ b/DSharpPlus.Commands/EventArgs/CommandErroredEventArgs.cs @@ -1,11 +1,11 @@ -using System; -using DSharpPlus.EventArgs; - -namespace DSharpPlus.Commands.EventArgs; - -public sealed class CommandErroredEventArgs : DiscordEventArgs -{ - public required CommandContext Context { get; init; } - public required Exception Exception { get; init; } - public required object? CommandObject { get; init; } -} +using System; +using DSharpPlus.EventArgs; + +namespace DSharpPlus.Commands.EventArgs; + +public sealed class CommandErroredEventArgs : DiscordEventArgs +{ + public required CommandContext Context { get; init; } + public required Exception Exception { get; init; } + public required object? CommandObject { get; init; } +} diff --git a/DSharpPlus.Commands/EventArgs/CommandExecutedEventArgs.cs b/DSharpPlus.Commands/EventArgs/CommandExecutedEventArgs.cs index 2567f1def4..43b5af3ee0 100644 --- a/DSharpPlus.Commands/EventArgs/CommandExecutedEventArgs.cs +++ b/DSharpPlus.Commands/EventArgs/CommandExecutedEventArgs.cs @@ -1,9 +1,9 @@ -using DSharpPlus.EventArgs; - -namespace DSharpPlus.Commands.EventArgs; - -public sealed class CommandExecutedEventArgs : DiscordEventArgs -{ - public required CommandContext Context { get; init; } - public required object? CommandObject { get; init; } -} +using DSharpPlus.EventArgs; + +namespace DSharpPlus.Commands.EventArgs; + +public sealed class CommandExecutedEventArgs : DiscordEventArgs +{ + public required CommandContext Context { get; init; } + public required object? CommandObject { get; init; } +} diff --git a/DSharpPlus.Commands/EventArgs/ConfigureCommandsEventArgs.cs b/DSharpPlus.Commands/EventArgs/ConfigureCommandsEventArgs.cs index 499555e1b8..e289c277df 100644 --- a/DSharpPlus.Commands/EventArgs/ConfigureCommandsEventArgs.cs +++ b/DSharpPlus.Commands/EventArgs/ConfigureCommandsEventArgs.cs @@ -1,19 +1,19 @@ -using System.Collections.Generic; -using DSharpPlus.Commands.Trees; -using DSharpPlus.EventArgs; - -namespace DSharpPlus.Commands.EventArgs; - -/// -/// The event args passed to event. -/// -public sealed class ConfigureCommandsEventArgs : DiscordEventArgs -{ - /// - /// The collection of command trees to be built when the event is done. - /// - /// - /// This collection is mutable and can be modified to add or remove command trees. - /// - public required List CommandTrees { get; init; } -} +using System.Collections.Generic; +using DSharpPlus.Commands.Trees; +using DSharpPlus.EventArgs; + +namespace DSharpPlus.Commands.EventArgs; + +/// +/// The event args passed to event. +/// +public sealed class ConfigureCommandsEventArgs : DiscordEventArgs +{ + /// + /// The collection of command trees to be built when the event is done. + /// + /// + /// This collection is mutable and can be modified to add or remove command trees. + /// + public required List CommandTrees { get; init; } +} diff --git a/DSharpPlus.Commands/Exceptions/ChecksFailedException.cs b/DSharpPlus.Commands/Exceptions/ChecksFailedException.cs index 2f1a4b0d0d..ea4a9640be 100644 --- a/DSharpPlus.Commands/Exceptions/ChecksFailedException.cs +++ b/DSharpPlus.Commands/Exceptions/ChecksFailedException.cs @@ -1,17 +1,17 @@ -using System.Collections.Generic; -using DSharpPlus.Commands.ContextChecks; -using DSharpPlus.Commands.Trees; - -namespace DSharpPlus.Commands.Exceptions; - -public sealed class ChecksFailedException : CommandsException -{ - public Command Command { get; init; } - public IReadOnlyList Errors { get; init; } - - public ChecksFailedException(IReadOnlyList errors, Command command, string? message = null) : base(message ?? $"Checks for {command.FullName} failed.") - { - this.Command = command; - this.Errors = errors; - } -} +using System.Collections.Generic; +using DSharpPlus.Commands.ContextChecks; +using DSharpPlus.Commands.Trees; + +namespace DSharpPlus.Commands.Exceptions; + +public sealed class ChecksFailedException : CommandsException +{ + public Command Command { get; init; } + public IReadOnlyList Errors { get; init; } + + public ChecksFailedException(IReadOnlyList errors, Command command, string? message = null) : base(message ?? $"Checks for {command.FullName} failed.") + { + this.Command = command; + this.Errors = errors; + } +} diff --git a/DSharpPlus.Commands/Exceptions/CommandNotExecutableException.cs b/DSharpPlus.Commands/Exceptions/CommandNotExecutableException.cs index 2ee15decc0..538504fcf8 100644 --- a/DSharpPlus.Commands/Exceptions/CommandNotExecutableException.cs +++ b/DSharpPlus.Commands/Exceptions/CommandNotExecutableException.cs @@ -1,15 +1,15 @@ -using System; -using DSharpPlus.Commands.Trees; - -namespace DSharpPlus.Commands.Exceptions; - -public sealed class CommandNotExecutableException : CommandsException -{ - public Command Command { get; init; } - - public CommandNotExecutableException(Command command, string? message = null) : base(message ?? $"Command {command.Name} is not executable.") - { - ArgumentNullException.ThrowIfNull(command, nameof(command)); - this.Command = command; - } -} +using System; +using DSharpPlus.Commands.Trees; + +namespace DSharpPlus.Commands.Exceptions; + +public sealed class CommandNotExecutableException : CommandsException +{ + public Command Command { get; init; } + + public CommandNotExecutableException(Command command, string? message = null) : base(message ?? $"Command {command.Name} is not executable.") + { + ArgumentNullException.ThrowIfNull(command, nameof(command)); + this.Command = command; + } +} diff --git a/DSharpPlus.Commands/Exceptions/CommandNotFoundException.cs b/DSharpPlus.Commands/Exceptions/CommandNotFoundException.cs index 4474035131..17f83c4029 100644 --- a/DSharpPlus.Commands/Exceptions/CommandNotFoundException.cs +++ b/DSharpPlus.Commands/Exceptions/CommandNotFoundException.cs @@ -1,14 +1,14 @@ -using System; - -namespace DSharpPlus.Commands.Exceptions; - -public sealed class CommandNotFoundException : CommandsException -{ - public string CommandName { get; init; } - - public CommandNotFoundException(string commandName, string? message = null) : base(message ?? $"Command {commandName} not found.") - { - ArgumentNullException.ThrowIfNull(commandName, nameof(commandName)); - this.CommandName = commandName; - } -} +using System; + +namespace DSharpPlus.Commands.Exceptions; + +public sealed class CommandNotFoundException : CommandsException +{ + public string CommandName { get; init; } + + public CommandNotFoundException(string commandName, string? message = null) : base(message ?? $"Command {commandName} not found.") + { + ArgumentNullException.ThrowIfNull(commandName, nameof(commandName)); + this.CommandName = commandName; + } +} diff --git a/DSharpPlus.Commands/Exceptions/CommandsException.cs b/DSharpPlus.Commands/Exceptions/CommandsException.cs index 32175b960a..90a6fa8ca3 100644 --- a/DSharpPlus.Commands/Exceptions/CommandsException.cs +++ b/DSharpPlus.Commands/Exceptions/CommandsException.cs @@ -1,10 +1,10 @@ -using System; - -namespace DSharpPlus.Commands.Exceptions; - -public abstract class CommandsException : Exception -{ - protected CommandsException() { } - protected CommandsException(string? message) : base(message) { } - protected CommandsException(string? message, Exception? innerException) : base(message, innerException) { } -} +using System; + +namespace DSharpPlus.Commands.Exceptions; + +public abstract class CommandsException : Exception +{ + protected CommandsException() { } + protected CommandsException(string? message) : base(message) { } + protected CommandsException(string? message, Exception? innerException) : base(message, innerException) { } +} diff --git a/DSharpPlus.Commands/Exceptions/ParameterChecksFailedException.cs b/DSharpPlus.Commands/Exceptions/ParameterChecksFailedException.cs index 7049bc419f..0d6e010fcc 100644 --- a/DSharpPlus.Commands/Exceptions/ParameterChecksFailedException.cs +++ b/DSharpPlus.Commands/Exceptions/ParameterChecksFailedException.cs @@ -1,17 +1,17 @@ -using System.Collections.Generic; -using DSharpPlus.Commands.ContextChecks.ParameterChecks; -using DSharpPlus.Commands.Trees; - -namespace DSharpPlus.Commands.Exceptions; - -public class ParameterChecksFailedException : CommandsException -{ - public Command Command { get; init; } - public IReadOnlyList Errors { get; init; } - - public ParameterChecksFailedException(IReadOnlyList errors, Command command, string? message = null) : base(message ?? $"Checks for {command.FullName} failed.") - { - this.Command = command; - this.Errors = errors; - } -} +using System.Collections.Generic; +using DSharpPlus.Commands.ContextChecks.ParameterChecks; +using DSharpPlus.Commands.Trees; + +namespace DSharpPlus.Commands.Exceptions; + +public class ParameterChecksFailedException : CommandsException +{ + public Command Command { get; init; } + public IReadOnlyList Errors { get; init; } + + public ParameterChecksFailedException(IReadOnlyList errors, Command command, string? message = null) : base(message ?? $"Checks for {command.FullName} failed.") + { + this.Command = command; + this.Errors = errors; + } +} diff --git a/DSharpPlus.Commands/Exceptions/ParseArgumentException.cs b/DSharpPlus.Commands/Exceptions/ParseArgumentException.cs index c487c93c76..5e59b713da 100644 --- a/DSharpPlus.Commands/Exceptions/ParseArgumentException.cs +++ b/DSharpPlus.Commands/Exceptions/ParseArgumentException.cs @@ -1,35 +1,35 @@ -using System; -using DSharpPlus.Commands.Converters.Results; -using DSharpPlus.Commands.Trees; - -namespace DSharpPlus.Commands.Exceptions; - -/// -/// Indicates that an argument failed to parse. -/// -public sealed class ArgumentParseException : CommandsException -{ - /// - /// The argument failed to parse - /// - public CommandParameter Parameter { get; init; } - - /// - /// The result of the conversion, containing the exception and possibly the failed value. - /// - public ArgumentFailedConversionResult? ConversionResult { get; init; } - - /// - /// Creates a new argument parse exception. - /// - /// The parameter that failed to parse. - /// The result of the conversion, containing the exception and possibly the failed value. - /// The message to display. - public ArgumentParseException(CommandParameter parameter, ArgumentFailedConversionResult? conversionResult = null, string? message = null) - : base(message ?? $"Failed to parse {parameter.Name}.", conversionResult?.Error) - { - ArgumentNullException.ThrowIfNull(parameter, nameof(parameter)); - this.Parameter = parameter; - this.ConversionResult = conversionResult; - } -} +using System; +using DSharpPlus.Commands.Converters.Results; +using DSharpPlus.Commands.Trees; + +namespace DSharpPlus.Commands.Exceptions; + +/// +/// Indicates that an argument failed to parse. +/// +public sealed class ArgumentParseException : CommandsException +{ + /// + /// The argument failed to parse + /// + public CommandParameter Parameter { get; init; } + + /// + /// The result of the conversion, containing the exception and possibly the failed value. + /// + public ArgumentFailedConversionResult? ConversionResult { get; init; } + + /// + /// Creates a new argument parse exception. + /// + /// The parameter that failed to parse. + /// The result of the conversion, containing the exception and possibly the failed value. + /// The message to display. + public ArgumentParseException(CommandParameter parameter, ArgumentFailedConversionResult? conversionResult = null, string? message = null) + : base(message ?? $"Failed to parse {parameter.Name}.", conversionResult?.Error) + { + ArgumentNullException.ThrowIfNull(parameter, nameof(parameter)); + this.Parameter = parameter; + this.ConversionResult = conversionResult; + } +} diff --git a/DSharpPlus.Commands/ExtensionMethods.cs b/DSharpPlus.Commands/ExtensionMethods.cs index 832505b8ef..6211f9e9a7 100644 --- a/DSharpPlus.Commands/ExtensionMethods.cs +++ b/DSharpPlus.Commands/ExtensionMethods.cs @@ -1,83 +1,83 @@ -using System; -using System.Collections.Generic; -using DSharpPlus.Extensions; -using Microsoft.Extensions.DependencyInjection; - -namespace DSharpPlus.Commands; - -/// -/// Extension methods used by the for the . -/// -public static class ExtensionMethods -{ - /// - /// Registers the extension with the . - /// - /// The client builder to register the extension with. - /// Any setup code you want to run on the extension, such as registering commands and converters. - /// The configuration to use for the extension. - public static DiscordClientBuilder UseCommands( - this DiscordClientBuilder builder, - Action setup, - CommandsConfiguration? configuration = null - ) => builder.ConfigureServices(services => services.AddCommandsExtension(setup, configuration)); - - /// - /// Registers the commands extension with an . - /// - /// The service collection to register the extension with. - /// Any setup code you want to run on the extension, such as registering commands and converters. - /// The configuration to use for the extension. - public static IServiceCollection AddCommandsExtension( - this IServiceCollection services, - Action setup, - CommandsConfiguration? configuration = null - ) - { - AddCommandsExtension(services, setup, _ => configuration ?? new CommandsConfiguration()); - return services; - } - - /// - public static IServiceCollection AddCommandsExtension( - this IServiceCollection services, - Action setup, - Func configurationFactory - ) - { - services - .ConfigureEventHandlers(eventHandlingBuilder => - eventHandlingBuilder - .AddEventHandlers(ServiceLifetime.Singleton) - .AddEventHandlers(ServiceLifetime.Transient) - ) - .AddSingleton(provider => - { - DiscordClient client = provider.GetRequiredService(); - CommandsConfiguration configuration = configurationFactory(provider); - CommandsExtension extension = new(configuration); - extension.Setup(client); - setup(provider, extension); - return extension; - }); - - return services; - } - - /// - internal static int IndexOf(this IEnumerable array, T? value) where T : IEquatable - { - int index = 0; - foreach (T item in array) - { - if (item.Equals(value)) - { - return index; - } - - index++; - } - - return -1; - } -} +using System; +using System.Collections.Generic; +using DSharpPlus.Extensions; +using Microsoft.Extensions.DependencyInjection; + +namespace DSharpPlus.Commands; + +/// +/// Extension methods used by the for the . +/// +public static class ExtensionMethods +{ + /// + /// Registers the extension with the . + /// + /// The client builder to register the extension with. + /// Any setup code you want to run on the extension, such as registering commands and converters. + /// The configuration to use for the extension. + public static DiscordClientBuilder UseCommands( + this DiscordClientBuilder builder, + Action setup, + CommandsConfiguration? configuration = null + ) => builder.ConfigureServices(services => services.AddCommandsExtension(setup, configuration)); + + /// + /// Registers the commands extension with an . + /// + /// The service collection to register the extension with. + /// Any setup code you want to run on the extension, such as registering commands and converters. + /// The configuration to use for the extension. + public static IServiceCollection AddCommandsExtension( + this IServiceCollection services, + Action setup, + CommandsConfiguration? configuration = null + ) + { + AddCommandsExtension(services, setup, _ => configuration ?? new CommandsConfiguration()); + return services; + } + + /// + public static IServiceCollection AddCommandsExtension( + this IServiceCollection services, + Action setup, + Func configurationFactory + ) + { + services + .ConfigureEventHandlers(eventHandlingBuilder => + eventHandlingBuilder + .AddEventHandlers(ServiceLifetime.Singleton) + .AddEventHandlers(ServiceLifetime.Transient) + ) + .AddSingleton(provider => + { + DiscordClient client = provider.GetRequiredService(); + CommandsConfiguration configuration = configurationFactory(provider); + CommandsExtension extension = new(configuration); + extension.Setup(client); + setup(provider, extension); + return extension; + }); + + return services; + } + + /// + internal static int IndexOf(this IEnumerable array, T? value) where T : IEquatable + { + int index = 0; + foreach (T item in array) + { + if (item.Equals(value)) + { + return index; + } + + index++; + } + + return -1; + } +} diff --git a/DSharpPlus.Commands/ICommandExecutor.cs b/DSharpPlus.Commands/ICommandExecutor.cs index 400c14ab92..2000766465 100644 --- a/DSharpPlus.Commands/ICommandExecutor.cs +++ b/DSharpPlus.Commands/ICommandExecutor.cs @@ -1,15 +1,15 @@ -using System.Threading; -using System.Threading.Tasks; - -namespace DSharpPlus.Commands; - -public interface ICommandExecutor -{ - - /// - /// Executes a command asynchronously. - /// - /// The context of the command. - /// The cancellation token to use. - public ValueTask ExecuteAsync(CommandContext context, CancellationToken cancellationToken = default); -} +using System.Threading; +using System.Threading.Tasks; + +namespace DSharpPlus.Commands; + +public interface ICommandExecutor +{ + + /// + /// Executes a command asynchronously. + /// + /// The context of the command. + /// The cancellation token to use. + public ValueTask ExecuteAsync(CommandContext context, CancellationToken cancellationToken = default); +} diff --git a/DSharpPlus.Commands/Invocation/AnonymousDelegateUtil.cs b/DSharpPlus.Commands/Invocation/AnonymousDelegateUtil.cs index ac2d804631..ca1142a588 100644 --- a/DSharpPlus.Commands/Invocation/AnonymousDelegateUtil.cs +++ b/DSharpPlus.Commands/Invocation/AnonymousDelegateUtil.cs @@ -1,31 +1,31 @@ -using System; -using System.Reflection; -using System.Threading.Tasks; - -namespace DSharpPlus.Commands.Invocation; - -/// -/// Contains stubs to invoke anonymous delegates -/// -internal static class AnonymousDelegateUtil -{ - public static Func GetAnonymousInvocationFunc(MethodInfo method, object? target) - { - if (method.ReturnType.IsAssignableTo(typeof(ValueTask))) - { - return async (object? _, object?[] parameters) => await (ValueTask)method.Invoke(target, parameters)!; - } - else if (method.ReturnType.IsAssignableTo(typeof(Task))) - { - return async (object? _, object?[] parameters) => await (Task)method.Invoke(target, parameters)!; - } - - throw new InvalidOperationException - ( - $"This command executor only supports ValueTask and Task return types for commands, found " + - $"{method.ReturnType} on command method " + - method.DeclaringType is not null ? $"{method.DeclaringType?.FullName ?? ""}." : "" + - method.Name - ); - } -} +using System; +using System.Reflection; +using System.Threading.Tasks; + +namespace DSharpPlus.Commands.Invocation; + +/// +/// Contains stubs to invoke anonymous delegates +/// +internal static class AnonymousDelegateUtil +{ + public static Func GetAnonymousInvocationFunc(MethodInfo method, object? target) + { + if (method.ReturnType.IsAssignableTo(typeof(ValueTask))) + { + return async (object? _, object?[] parameters) => await (ValueTask)method.Invoke(target, parameters)!; + } + else if (method.ReturnType.IsAssignableTo(typeof(Task))) + { + return async (object? _, object?[] parameters) => await (Task)method.Invoke(target, parameters)!; + } + + throw new InvalidOperationException + ( + $"This command executor only supports ValueTask and Task return types for commands, found " + + $"{method.ReturnType} on command method " + + method.DeclaringType is not null ? $"{method.DeclaringType?.FullName ?? ""}." : "" + + method.Name + ); + } +} diff --git a/DSharpPlus.Commands/Invocation/CommandEmitUtil.cs b/DSharpPlus.Commands/Invocation/CommandEmitUtil.cs index 3a5b859b27..24c8327b6a 100644 --- a/DSharpPlus.Commands/Invocation/CommandEmitUtil.cs +++ b/DSharpPlus.Commands/Invocation/CommandEmitUtil.cs @@ -1,134 +1,134 @@ -using System; -using System.Reflection; -using System.Reflection.Emit; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; - -namespace DSharpPlus.Commands.Invocation; - -/// -/// Contains utilities to conveniently await all (supported) commands as ValueTasks. -/// -internal static class CommandEmitUtil -{ - /// - /// Creates a wrapper function to invoke a command. - /// - /// The corresponding MethodInfo for this command. - /// The object targeted by this delegate, if applicable. - /// Thrown if the command returns anything but ValueTask and Task. - public static Func GetCommandInvocationFunc(MethodInfo method, object? target) - { - // This method is very likely to be an anonymous delegate, which happens to be very slow to invoke. - // We're going to take the slow path here and simply do `MethodInfo.Invoke`, for lack of a better way. - // Not that there's any other path to take here, so I guess it isn't "slow" when there's nothing else to compare it too... - if (method.Name.Contains('<') && method.Name.Contains('>') && method.GetCustomAttribute() is not null) - { - return AnonymousDelegateUtil.GetAnonymousInvocationFunc(method, target); - } - else if (method.ReturnType == typeof(ValueTask)) - { - return GetValueTaskFunc(method); - } - else if (method.ReturnType == typeof(Task)) - { - return GetTaskFunc(method); - } - - // This could happen for `void` methods when the user explicitly builds a command tree with them. - throw new InvalidOperationException - ( - $"This command executor only supports ValueTask and Task return types for commands, found " + - $"{method.ReturnType} on command method " + - method.DeclaringType is not null ? $"{method.DeclaringType?.FullName ?? ""}." : "" + - method.Name - ); - } - - /// - /// Emits a wrapper function around a command method that returns a . - /// - /// The method to wrap. - /// An asynchronous function that wraps the command method, returning a . - private static Func GetValueTaskFunc(MethodInfo method) - { - // Create the wrapper function - DynamicMethod dynamicMethod = new($"{method.Name}-valuetask-wrapper", typeof(ValueTask), [typeof(object), typeof(object?[])]); - - // Create the wrapper logic - EmitMethodWrapper(dynamicMethod.GetILGenerator(), method); - - // Return the delegate for the wrapper which invokes the method - return dynamicMethod.CreateDelegate>(); - } - - /// - /// Emits a wrapper function around a command method that returns a . - /// - /// The method to wrap. - /// An asynchronous function that wraps the command method, returning a . - private static Func GetTaskFunc(MethodInfo method) - { - // Create the wrapper function - DynamicMethod dynamicMethod = new($"{method.Name}-task-wrapper", typeof(Task), [typeof(object), typeof(object?[])]); - - // Create the wrapper logic - EmitMethodWrapper(dynamicMethod.GetILGenerator(), method); - - // Return the delegate for the wrapper which invokes the method - Func taskWrapper = dynamicMethod.CreateDelegate>(); - - // Create an async wrapper around the task wrapper - return async (object? instance, object?[] parameters) => await taskWrapper(instance, parameters); - } - - /// - /// Writes the body of the wrapper function which invokes the command method. - /// - /// - /// - private static void EmitMethodWrapper(ILGenerator dynamicMethodIlGenerator, MethodInfo method) - { - // If there is an object to execute the method on, load it onto the stack. - if (!method.IsStatic) - { - // Load the instance (this) onto the stack. - dynamicMethodIlGenerator.Emit(OpCodes.Ldarg_0); - - if (method.DeclaringType!.IsValueType) - { - dynamicMethodIlGenerator.Emit(OpCodes.Unbox_Any, method.DeclaringType); - } - } - - // Load each element of the parameter array onto the stack. - ParameterInfo[] parameters = method.GetParameters(); - for (int i = 0; i < parameters.Length; i++) - { - // ldarg.1 loads the array of arguments. - dynamicMethodIlGenerator.Emit(OpCodes.Ldarg_1); - - // ldc.i4 loads the index of the current argument. - dynamicMethodIlGenerator.Emit(OpCodes.Ldc_I4, i); - - // ldelem.ref loads the element at the given index from the array. - dynamicMethodIlGenerator.Emit(OpCodes.Ldelem_Ref); - - // Unbox value types - reference types don't need any special handling because they're just pointers - if (parameters[i].ParameterType.IsValueType) - { - // If the parameter is a value type, unbox it. - // This is necessary because the argument is stored as an object (reference type) - // when it needs to be treated as a value type. - dynamicMethodIlGenerator.Emit(OpCodes.Unbox_Any, parameters[i].ParameterType); - } - } - - // The method call is performed after loading the instance (if applicable) - // and arguments onto the stack. - dynamicMethodIlGenerator.Emit(OpCodes.Call, method); - - // Return from the method. - dynamicMethodIlGenerator.Emit(OpCodes.Ret); - } -} +using System; +using System.Reflection; +using System.Reflection.Emit; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +namespace DSharpPlus.Commands.Invocation; + +/// +/// Contains utilities to conveniently await all (supported) commands as ValueTasks. +/// +internal static class CommandEmitUtil +{ + /// + /// Creates a wrapper function to invoke a command. + /// + /// The corresponding MethodInfo for this command. + /// The object targeted by this delegate, if applicable. + /// Thrown if the command returns anything but ValueTask and Task. + public static Func GetCommandInvocationFunc(MethodInfo method, object? target) + { + // This method is very likely to be an anonymous delegate, which happens to be very slow to invoke. + // We're going to take the slow path here and simply do `MethodInfo.Invoke`, for lack of a better way. + // Not that there's any other path to take here, so I guess it isn't "slow" when there's nothing else to compare it too... + if (method.Name.Contains('<') && method.Name.Contains('>') && method.GetCustomAttribute() is not null) + { + return AnonymousDelegateUtil.GetAnonymousInvocationFunc(method, target); + } + else if (method.ReturnType == typeof(ValueTask)) + { + return GetValueTaskFunc(method); + } + else if (method.ReturnType == typeof(Task)) + { + return GetTaskFunc(method); + } + + // This could happen for `void` methods when the user explicitly builds a command tree with them. + throw new InvalidOperationException + ( + $"This command executor only supports ValueTask and Task return types for commands, found " + + $"{method.ReturnType} on command method " + + method.DeclaringType is not null ? $"{method.DeclaringType?.FullName ?? ""}." : "" + + method.Name + ); + } + + /// + /// Emits a wrapper function around a command method that returns a . + /// + /// The method to wrap. + /// An asynchronous function that wraps the command method, returning a . + private static Func GetValueTaskFunc(MethodInfo method) + { + // Create the wrapper function + DynamicMethod dynamicMethod = new($"{method.Name}-valuetask-wrapper", typeof(ValueTask), [typeof(object), typeof(object?[])]); + + // Create the wrapper logic + EmitMethodWrapper(dynamicMethod.GetILGenerator(), method); + + // Return the delegate for the wrapper which invokes the method + return dynamicMethod.CreateDelegate>(); + } + + /// + /// Emits a wrapper function around a command method that returns a . + /// + /// The method to wrap. + /// An asynchronous function that wraps the command method, returning a . + private static Func GetTaskFunc(MethodInfo method) + { + // Create the wrapper function + DynamicMethod dynamicMethod = new($"{method.Name}-task-wrapper", typeof(Task), [typeof(object), typeof(object?[])]); + + // Create the wrapper logic + EmitMethodWrapper(dynamicMethod.GetILGenerator(), method); + + // Return the delegate for the wrapper which invokes the method + Func taskWrapper = dynamicMethod.CreateDelegate>(); + + // Create an async wrapper around the task wrapper + return async (object? instance, object?[] parameters) => await taskWrapper(instance, parameters); + } + + /// + /// Writes the body of the wrapper function which invokes the command method. + /// + /// + /// + private static void EmitMethodWrapper(ILGenerator dynamicMethodIlGenerator, MethodInfo method) + { + // If there is an object to execute the method on, load it onto the stack. + if (!method.IsStatic) + { + // Load the instance (this) onto the stack. + dynamicMethodIlGenerator.Emit(OpCodes.Ldarg_0); + + if (method.DeclaringType!.IsValueType) + { + dynamicMethodIlGenerator.Emit(OpCodes.Unbox_Any, method.DeclaringType); + } + } + + // Load each element of the parameter array onto the stack. + ParameterInfo[] parameters = method.GetParameters(); + for (int i = 0; i < parameters.Length; i++) + { + // ldarg.1 loads the array of arguments. + dynamicMethodIlGenerator.Emit(OpCodes.Ldarg_1); + + // ldc.i4 loads the index of the current argument. + dynamicMethodIlGenerator.Emit(OpCodes.Ldc_I4, i); + + // ldelem.ref loads the element at the given index from the array. + dynamicMethodIlGenerator.Emit(OpCodes.Ldelem_Ref); + + // Unbox value types - reference types don't need any special handling because they're just pointers + if (parameters[i].ParameterType.IsValueType) + { + // If the parameter is a value type, unbox it. + // This is necessary because the argument is stored as an object (reference type) + // when it needs to be treated as a value type. + dynamicMethodIlGenerator.Emit(OpCodes.Unbox_Any, parameters[i].ParameterType); + } + } + + // The method call is performed after loading the instance (if applicable) + // and arguments onto the stack. + dynamicMethodIlGenerator.Emit(OpCodes.Call, method); + + // Return from the method. + dynamicMethodIlGenerator.Emit(OpCodes.Ret); + } +} diff --git a/DSharpPlus.Commands/ParameterAttribute.cs b/DSharpPlus.Commands/ParameterAttribute.cs index 774d905e7b..356c889d65 100644 --- a/DSharpPlus.Commands/ParameterAttribute.cs +++ b/DSharpPlus.Commands/ParameterAttribute.cs @@ -1,30 +1,30 @@ -using System; - -namespace DSharpPlus.Commands; - -[AttributeUsage(AttributeTargets.Parameter)] -public sealed class ParameterAttribute : Attribute -{ - /// - /// The name of the parameter. - /// - public string Name { get; init; } - - /// - /// Creates a new instance of the class. - /// - /// The name of the parameter. - public ParameterAttribute(string name) - { - if (string.IsNullOrWhiteSpace(name)) - { - throw new ArgumentNullException(nameof(name), "The name of the parameter cannot be null or whitespace."); - } - else if (name.Length is < 1 or > 32) - { - throw new ArgumentOutOfRangeException(nameof(name), "The name of the parameter must be between 1 and 32 characters."); - } - - this.Name = name; - } -} +using System; + +namespace DSharpPlus.Commands; + +[AttributeUsage(AttributeTargets.Parameter)] +public sealed class ParameterAttribute : Attribute +{ + /// + /// The name of the parameter. + /// + public string Name { get; init; } + + /// + /// Creates a new instance of the class. + /// + /// The name of the parameter. + public ParameterAttribute(string name) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentNullException(nameof(name), "The name of the parameter cannot be null or whitespace."); + } + else if (name.Length is < 1 or > 32) + { + throw new ArgumentOutOfRangeException(nameof(name), "The name of the parameter must be between 1 and 32 characters."); + } + + this.Name = name; + } +} diff --git a/DSharpPlus.Commands/Processors/BaseCommandProcessor/BaseCommandLogging.cs b/DSharpPlus.Commands/Processors/BaseCommandProcessor/BaseCommandLogging.cs index 73b1b9c2bc..92e9812b9c 100644 --- a/DSharpPlus.Commands/Processors/BaseCommandProcessor/BaseCommandLogging.cs +++ b/DSharpPlus.Commands/Processors/BaseCommandProcessor/BaseCommandLogging.cs @@ -1,13 +1,13 @@ -using System; -using Microsoft.Extensions.Logging; - -namespace DSharpPlus.Commands.Processors; - -internal static class BaseCommandLogging -{ - // Startup logs - internal static readonly Action invalidArgumentConverterImplementation = LoggerMessage.Define(LogLevel.Error, new EventId(1, "Command Processor Startup"), "Argument Converter {FullName} does not implement {InterfaceFullName}"); - internal static readonly Action invalidEnumConverterImplementation = LoggerMessage.Define(LogLevel.Error, new EventId(1, "Command Processor Startup"), "'{GenericEnumConverterFullName}' does not implement '{TConverterFullName}' and cannot be used. Please ensure the command processor '{CommandProcessor}' overrides '{NameOfAddEnumConverters}' and provides it's own generic enum converter. Currently, any commands with enum parameters will NOT be registered."); - internal static readonly Action duplicateArgumentConvertersRegistered = LoggerMessage.Define(LogLevel.Warning, new EventId(1, "Command Processor Startup"), "Failed to add converter {ConverterFullName} because a converter for type {ParameterType} already exists: {ExistingConverter}"); - internal static readonly Action failedConverterCreation = LoggerMessage.Define(LogLevel.Error, new EventId(1), "Failed to create instance of converter '{FullName}' due to a lack of empty public constructors, lack of a service provider, or lack of services within the service provider."); -} +using System; +using Microsoft.Extensions.Logging; + +namespace DSharpPlus.Commands.Processors; + +internal static class BaseCommandLogging +{ + // Startup logs + internal static readonly Action invalidArgumentConverterImplementation = LoggerMessage.Define(LogLevel.Error, new EventId(1, "Command Processor Startup"), "Argument Converter {FullName} does not implement {InterfaceFullName}"); + internal static readonly Action invalidEnumConverterImplementation = LoggerMessage.Define(LogLevel.Error, new EventId(1, "Command Processor Startup"), "'{GenericEnumConverterFullName}' does not implement '{TConverterFullName}' and cannot be used. Please ensure the command processor '{CommandProcessor}' overrides '{NameOfAddEnumConverters}' and provides it's own generic enum converter. Currently, any commands with enum parameters will NOT be registered."); + internal static readonly Action duplicateArgumentConvertersRegistered = LoggerMessage.Define(LogLevel.Warning, new EventId(1, "Command Processor Startup"), "Failed to add converter {ConverterFullName} because a converter for type {ParameterType} already exists: {ExistingConverter}"); + internal static readonly Action failedConverterCreation = LoggerMessage.Define(LogLevel.Error, new EventId(1), "Failed to create instance of converter '{FullName}' due to a lack of empty public constructors, lack of a service provider, or lack of services within the service provider."); +} diff --git a/DSharpPlus.Commands/Processors/BaseCommandProcessor/BaseCommandProcessor.ConverterDelegateFactory.cs b/DSharpPlus.Commands/Processors/BaseCommandProcessor/BaseCommandProcessor.ConverterDelegateFactory.cs index a21ae4cccd..37f53bab62 100644 --- a/DSharpPlus.Commands/Processors/BaseCommandProcessor/BaseCommandProcessor.ConverterDelegateFactory.cs +++ b/DSharpPlus.Commands/Processors/BaseCommandProcessor/BaseCommandProcessor.ConverterDelegateFactory.cs @@ -1,186 +1,186 @@ -using System; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Reflection; -using System.Threading.Tasks; -using DSharpPlus.Commands.Converters; -using DSharpPlus.Entities; -using Microsoft.Extensions.DependencyInjection; - -namespace DSharpPlus.Commands.Processors; - -public abstract partial class BaseCommandProcessor : ICommandProcessor - where TConverter : class, IArgumentConverter - where TConverterContext : ConverterContext - where TCommandContext : CommandContext -{ - /// - /// A factory used for creating and caching converter objects and delegates. - /// - protected class ConverterDelegateFactory - { - private static readonly MethodInfo createConverterDelegateMethod = typeof(ConverterDelegateFactory) - .GetMethod(nameof(CreateConverterDelegate), BindingFlags.Instance | BindingFlags.NonPublic) - ?? throw new UnreachableException($"Method {nameof(CreateConverterDelegate)} was unable to be found."); - - /// - /// The converter instance, if it's already been created. - /// - /// - /// Prefer using to get the converter instance. - /// - public TConverter? ConverterInstance { get; private set; } - - /// - /// The converter's type (not to be confused with parameter type). Only available when passing the type to the constructor. - /// - public Type? ConverterType { get; private set; } - - /// - /// The parameter type that this converter converts to. - /// - public Type ParameterType { get; init; } - - /// - /// The command processor that this converter is associated with. - /// - public BaseCommandProcessor CommandProcessor { get; init; } - - /// - /// The delegate that executes the converter, casting the returned strongly typed value () - /// to a less strongly typed value () for easier argument converter invocation. - /// - private ConverterDelegate? converterDelegate; - - /// - /// Creates a new converter delegate factory, which will use - /// the provided converter instance to create the delegate. - /// - /// The command processor that this converter is associated with. - /// The parameter type that this converter converts to. - /// The converter instance to use. - public ConverterDelegateFactory(BaseCommandProcessor processor, Type parameterType, TConverter converter) - { - this.ConverterInstance = converter; - this.ConverterType = null; - this.ParameterType = parameterType; - this.CommandProcessor = processor; - } - - /// - /// Creates a new converter delegate factory, which will obtain or construct - /// through the service provider as needed. - /// The converter delegate will be created using the newly created converter instance. - /// - public ConverterDelegateFactory(BaseCommandProcessor processor, Type parameterType, Type converterType) - { - this.ConverterType = converterType; - this.ParameterType = parameterType; - this.CommandProcessor = processor; - } - - /// - /// Creates and caches the converter instance if it hasn't been created yet. - /// - /// The service provider to use for creating the converter instance, if needed. - /// The converter instance. - [MemberNotNull(nameof(ConverterInstance))] - public TConverter GetConverter(IServiceProvider serviceProvider) - { - if (this.ConverterInstance is not null) - { - return this.ConverterInstance; - } - else if (this.ConverterType is null) - { - throw new UnreachableException($"Both {nameof(this.ConverterInstance)} and {nameof(this.ConverterType)} are null. Please open an issue about this."); - } - - this.ConverterInstance = (TConverter)ActivatorUtilities.GetServiceOrCreateInstance(serviceProvider, this.ConverterType); - return this.ConverterInstance; - } - - /// - /// Creates and caches the converter delegate if it hasn't been created yet. - /// - /// The service provider to use for creating the converter instance, if needed. - /// The converter delegate. - [MemberNotNull(nameof(converterDelegate))] - public ConverterDelegate GetConverterDelegate(IServiceProvider serviceProvider) - { - if (this.converterDelegate is not null) - { - return this.converterDelegate; - } - - // Sets the converter instance if it's null - GetConverter(serviceProvider); - - // Create the generic version of CreateConverterDelegate to the parameter type - MethodInfo createConverterDelegateGenericMethod = createConverterDelegateMethod.MakeGenericMethod(this.ParameterType); - - // Invoke the generic method to obtain ExecuteConverterAsync as a delegate - this.converterDelegate = (ConverterDelegate)createConverterDelegateGenericMethod.Invoke(this, [])!; - return this.converterDelegate; - } - - /// - /// A generic method used for obtaining the method as a delegate. - /// - /// The type of the parameter that the converter converts to. - /// A delegate that executes the converter, casting the returned strongly typed value () - /// to a less strongly typed value () for easier argument converter invocation. - /// - private ConverterDelegate CreateConverterDelegate() => ((Delegate)ExecuteConverterAsync).Method.CreateDelegate(this); - - /// - /// Invokes the converter on the provided context, casting the returned strongly typed value () - /// to a less strongly typed value () for easier argument converter invocation. - /// - /// The converter context passed to the converter. - /// The type of the parameter that the converter converts to. - /// The result of the converter. - private async ValueTask ExecuteConverterAsync(ConverterContext context) => await this.CommandProcessor.ExecuteConverterAsync( - this.ConverterInstance!, - context.As() - ); - - /// - public override string? ToString() - { - if (this.ConverterType is not null) - { - return this.ConverterType.FullName ?? this.ConverterType.Name; - } - else if (this.ConverterInstance is not null) - { - Type type = this.ConverterInstance.GetType(); - return type.FullName ?? type.Name; - } - else if (this.converterDelegate is not null) - { - return this.converterDelegate.Method.DeclaringType is null - ? this.converterDelegate.ToString() - : this.converterDelegate.Method.DeclaringType.FullName - ?? this.converterDelegate.Method.DeclaringType.Name; - } - - return base.ToString(); - } - - /// - public override bool Equals(object? obj) => obj is ConverterDelegateFactory other - && (this.ConverterType == other.ConverterType - || this.ConverterInstance == other.ConverterInstance - || this.converterDelegate == other.converterDelegate); - - /// - public override int GetHashCode() => HashCode.Combine(this.ConverterType, this.ConverterInstance, this.converterDelegate); - - /// - public static bool operator ==(ConverterDelegateFactory left, ConverterDelegateFactory right) => left.Equals(right); - - /// - public static bool operator !=(ConverterDelegateFactory left, ConverterDelegateFactory right) => !left.Equals(right); - } -} +using System; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Threading.Tasks; +using DSharpPlus.Commands.Converters; +using DSharpPlus.Entities; +using Microsoft.Extensions.DependencyInjection; + +namespace DSharpPlus.Commands.Processors; + +public abstract partial class BaseCommandProcessor : ICommandProcessor + where TConverter : class, IArgumentConverter + where TConverterContext : ConverterContext + where TCommandContext : CommandContext +{ + /// + /// A factory used for creating and caching converter objects and delegates. + /// + protected class ConverterDelegateFactory + { + private static readonly MethodInfo createConverterDelegateMethod = typeof(ConverterDelegateFactory) + .GetMethod(nameof(CreateConverterDelegate), BindingFlags.Instance | BindingFlags.NonPublic) + ?? throw new UnreachableException($"Method {nameof(CreateConverterDelegate)} was unable to be found."); + + /// + /// The converter instance, if it's already been created. + /// + /// + /// Prefer using to get the converter instance. + /// + public TConverter? ConverterInstance { get; private set; } + + /// + /// The converter's type (not to be confused with parameter type). Only available when passing the type to the constructor. + /// + public Type? ConverterType { get; private set; } + + /// + /// The parameter type that this converter converts to. + /// + public Type ParameterType { get; init; } + + /// + /// The command processor that this converter is associated with. + /// + public BaseCommandProcessor CommandProcessor { get; init; } + + /// + /// The delegate that executes the converter, casting the returned strongly typed value () + /// to a less strongly typed value () for easier argument converter invocation. + /// + private ConverterDelegate? converterDelegate; + + /// + /// Creates a new converter delegate factory, which will use + /// the provided converter instance to create the delegate. + /// + /// The command processor that this converter is associated with. + /// The parameter type that this converter converts to. + /// The converter instance to use. + public ConverterDelegateFactory(BaseCommandProcessor processor, Type parameterType, TConverter converter) + { + this.ConverterInstance = converter; + this.ConverterType = null; + this.ParameterType = parameterType; + this.CommandProcessor = processor; + } + + /// + /// Creates a new converter delegate factory, which will obtain or construct + /// through the service provider as needed. + /// The converter delegate will be created using the newly created converter instance. + /// + public ConverterDelegateFactory(BaseCommandProcessor processor, Type parameterType, Type converterType) + { + this.ConverterType = converterType; + this.ParameterType = parameterType; + this.CommandProcessor = processor; + } + + /// + /// Creates and caches the converter instance if it hasn't been created yet. + /// + /// The service provider to use for creating the converter instance, if needed. + /// The converter instance. + [MemberNotNull(nameof(ConverterInstance))] + public TConverter GetConverter(IServiceProvider serviceProvider) + { + if (this.ConverterInstance is not null) + { + return this.ConverterInstance; + } + else if (this.ConverterType is null) + { + throw new UnreachableException($"Both {nameof(this.ConverterInstance)} and {nameof(this.ConverterType)} are null. Please open an issue about this."); + } + + this.ConverterInstance = (TConverter)ActivatorUtilities.GetServiceOrCreateInstance(serviceProvider, this.ConverterType); + return this.ConverterInstance; + } + + /// + /// Creates and caches the converter delegate if it hasn't been created yet. + /// + /// The service provider to use for creating the converter instance, if needed. + /// The converter delegate. + [MemberNotNull(nameof(converterDelegate))] + public ConverterDelegate GetConverterDelegate(IServiceProvider serviceProvider) + { + if (this.converterDelegate is not null) + { + return this.converterDelegate; + } + + // Sets the converter instance if it's null + GetConverter(serviceProvider); + + // Create the generic version of CreateConverterDelegate to the parameter type + MethodInfo createConverterDelegateGenericMethod = createConverterDelegateMethod.MakeGenericMethod(this.ParameterType); + + // Invoke the generic method to obtain ExecuteConverterAsync as a delegate + this.converterDelegate = (ConverterDelegate)createConverterDelegateGenericMethod.Invoke(this, [])!; + return this.converterDelegate; + } + + /// + /// A generic method used for obtaining the method as a delegate. + /// + /// The type of the parameter that the converter converts to. + /// A delegate that executes the converter, casting the returned strongly typed value () + /// to a less strongly typed value () for easier argument converter invocation. + /// + private ConverterDelegate CreateConverterDelegate() => ((Delegate)ExecuteConverterAsync).Method.CreateDelegate(this); + + /// + /// Invokes the converter on the provided context, casting the returned strongly typed value () + /// to a less strongly typed value () for easier argument converter invocation. + /// + /// The converter context passed to the converter. + /// The type of the parameter that the converter converts to. + /// The result of the converter. + private async ValueTask ExecuteConverterAsync(ConverterContext context) => await this.CommandProcessor.ExecuteConverterAsync( + this.ConverterInstance!, + context.As() + ); + + /// + public override string? ToString() + { + if (this.ConverterType is not null) + { + return this.ConverterType.FullName ?? this.ConverterType.Name; + } + else if (this.ConverterInstance is not null) + { + Type type = this.ConverterInstance.GetType(); + return type.FullName ?? type.Name; + } + else if (this.converterDelegate is not null) + { + return this.converterDelegate.Method.DeclaringType is null + ? this.converterDelegate.ToString() + : this.converterDelegate.Method.DeclaringType.FullName + ?? this.converterDelegate.Method.DeclaringType.Name; + } + + return base.ToString(); + } + + /// + public override bool Equals(object? obj) => obj is ConverterDelegateFactory other + && (this.ConverterType == other.ConverterType + || this.ConverterInstance == other.ConverterInstance + || this.converterDelegate == other.converterDelegate); + + /// + public override int GetHashCode() => HashCode.Combine(this.ConverterType, this.ConverterInstance, this.converterDelegate); + + /// + public static bool operator ==(ConverterDelegateFactory left, ConverterDelegateFactory right) => left.Equals(right); + + /// + public static bool operator !=(ConverterDelegateFactory left, ConverterDelegateFactory right) => !left.Equals(right); + } +} diff --git a/DSharpPlus.Commands/Processors/BaseCommandProcessor/BaseCommandProcessor.cs b/DSharpPlus.Commands/Processors/BaseCommandProcessor/BaseCommandProcessor.cs index dc4e21c53e..a1f1d01af8 100644 --- a/DSharpPlus.Commands/Processors/BaseCommandProcessor/BaseCommandProcessor.cs +++ b/DSharpPlus.Commands/Processors/BaseCommandProcessor/BaseCommandProcessor.cs @@ -1,402 +1,402 @@ -using System; -using System.Collections.Frozen; -using System.Collections.Generic; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; -using DSharpPlus.Commands.ArgumentModifiers; -using DSharpPlus.Commands.Converters; -using DSharpPlus.Commands.Converters.Results; -using DSharpPlus.Commands.Trees; -using DSharpPlus.Entities; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; - -namespace DSharpPlus.Commands.Processors; - -/// -/// A command processor containing command logic that's shared between all command processors. -/// -/// -/// When implementing a new command processor, it's recommended to inherit from this class. -/// You can however implement directly instead, if desired. -/// -/// -/// The converter type that's associated with this command processor. -/// May have extra metadata related to this processor specifically. -/// -/// The context type that's used for argument converters. -/// The context type that's used for command execution. -public abstract partial class BaseCommandProcessor : ICommandProcessor - where TConverter : class, IArgumentConverter - where TConverterContext : ConverterContext - where TCommandContext : CommandContext -{ - /// - public Type ContextType => typeof(TCommandContext); - - /// - public abstract IReadOnlyList Commands { get; } - - /// - public IReadOnlyDictionary Converters { get; protected set; } = FrozenDictionary.Empty; - - /// - IReadOnlyDictionary ICommandProcessor.Converters => Unsafe.As>(this.Converters); - - /// - /// A dictionary of argument converter delegates indexed by the output type they convert to. - /// - public IReadOnlyDictionary ConverterDelegates { get; protected set; } = FrozenDictionary.Empty; - - /// - /// A dictionary of argument converter factories indexed by the output type they convert to. - /// These factories populate the and dictionaries. - /// - protected Dictionary converterFactories = []; - - /// - /// The extension this processor belongs to. - /// - protected CommandsExtension? extension; - - /// - /// The logger for this processor. - /// - protected ILogger> logger = - NullLogger>.Instance; - - /// - // TODO: Register to the service provider and create the converters through the service provider. - public virtual void AddConverter() where T : TConverter, new() => AddConverter(typeof(T), new T()); - - /// - /// Registers a new argument converter with the processor. - /// - /// The converter to register. - /// The type that the converter converts to. - public virtual void AddConverter(TConverter converter) => AddConverter(typeof(T), converter); - - /// - /// Registers a new argument converter with the processor. - /// - /// The type that the converter converts to. - /// The converter to register. - public virtual void AddConverter(Type type, TConverter converter) => AddConverter(new(this, type, converter)); - - /// - /// Scans the specified assembly for argument converters and registers them with the processor. - /// The argument converters will be created through the provided to the . - /// - /// The assembly to scan for argument converters. - public virtual void AddConverters(Assembly assembly) => AddConverters(assembly.GetTypes()); - - /// - /// Adds multiple argument converters to the processor. - /// - /// - /// This method WILL NOT THROW if a converter is invalid. Instead, it will log an error and continue. - /// - /// The types to add as argument converters. - public virtual void AddConverters(IEnumerable types) - { - foreach (Type type in types) - { - // Ignore types that don't have a concrete implementation (abstract classes or interfaces) - // Additionally ignore types that have open generics (IArgumentConverter) - // instead of closed generics (IArgumentConverter) - if (type.IsAbstract || type.IsInterface || type.IsGenericTypeDefinition || !type.IsAssignableTo(typeof(TConverter))) - { - continue; - } - - // Check if the type implements IArgumentConverter - Type? genericArgumentConverter = type.GetInterfaces().FirstOrDefault(type => type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IArgumentConverter<>)); - if (genericArgumentConverter is null) - { - BaseCommandLogging.invalidArgumentConverterImplementation( - this.logger, - type.FullName ?? type.Name, - typeof(IArgumentConverter<>).FullName ?? typeof(IArgumentConverter<>).Name, - null - ); - - continue; - } - - // GenericTypeArguments[0] here is the T in IArgumentConverter - AddConverter(new(this, genericArgumentConverter.GenericTypeArguments[0], type)); - } - } - - /// - /// Registers a new argument converter factory with the processor. - /// - /// The factory that will create the argument converter and it's delegate. - protected virtual void AddConverter(ConverterDelegateFactory factory) - { - if (this.converterFactories.TryGetValue(factory.ParameterType, out ConverterDelegateFactory? existingFactory)) - { - // If it's a different factory trying to be added, log it. - // If it's the same factory that's being readded (likely - // from a gateway disconnect), ignore it. - if (existingFactory != factory) - { - BaseCommandLogging.duplicateArgumentConvertersRegistered( - this.logger, - factory.ToString()!, - factory.ParameterType.FullName ?? factory.ParameterType.Name, - existingFactory.ToString()!, - null - ); - } - - return; - } - - this.converterFactories.Add(factory.ParameterType, factory); - } - - /// - /// Finds all parameters that are enums and creates a generic enum converter for them. - /// - protected virtual void AddEnumConverters(Type? genericEnumConverterType = null) - { - if (this.extension is null) - { - throw new InvalidOperationException("The processor has not been configured yet."); - } - - // If the generic enum converter type is not provided, use the default EnumConverter<>. - genericEnumConverterType ??= typeof(EnumConverter<>); - - // For every enum type found, add the enum converter to it directly. - Dictionary enumConverterCache = []; - foreach (Command command in this.extension.Commands.Values.SelectMany(command => command.Flatten())) - { - foreach (CommandParameter parameter in command.Parameters) - { - Type baseType = IArgumentConverter.GetConverterFriendlyBaseType(parameter.Type); - if (!baseType.IsEnum) - { - continue; - } - - // Try to reuse any existing enum converters for this enum type. - if (!enumConverterCache.TryGetValue(baseType, out TConverter? enumConverter)) - { - Type genericConverter = genericEnumConverterType.MakeGenericType(baseType); - ConstructorInfo? constructor = genericConverter.GetConstructor([]) - ?? throw new UnreachableException($"The generic enum converter {genericConverter.FullName!} does not have a parameterless constructor."); - - enumConverter = (TConverter)constructor.Invoke([]); - enumConverterCache.Add(baseType, enumConverter); - } - - AddConverter(baseType, enumConverter); - } - } - } - - /// - [MemberNotNull(nameof(extension))] - public virtual ValueTask ConfigureAsync(CommandsExtension extension) - { - this.extension = extension; - this.logger = extension.ServiceProvider.GetService>>() - ?? NullLogger>.Instance; - - // Register all converters from the processor's assembly - AddConverters(GetType().Assembly); - - // This goes through all command parameters and creates the generic version of the enum converters. - AddEnumConverters(); - - // Populate the default converters - Dictionary converters = []; - Dictionary converterDelegates = []; - foreach (KeyValuePair factory in this.converterFactories) - { - converters.Add(factory.Key, factory.Value.GetConverter(extension.ServiceProvider)); - converterDelegates.Add(factory.Key, factory.Value.GetConverterDelegate(extension.ServiceProvider)); - } - - this.Converters = converters.ToFrozenDictionary(); - this.ConverterDelegates = converterDelegates.ToFrozenDictionary(); - return default; - } - - /// - /// Parses the arguments provided to the command and returns a prepared command context. - /// - /// The context used for the argument converters. - /// The prepared CommandContext. - public virtual async ValueTask> ParseParametersAsync(TConverterContext converterContext) - { - // If there's no parameters, begone. - if (converterContext.Command.Parameters.Count == 0) - { - return FrozenDictionary.Empty; - } - - // Populate the parsed arguments with - // to indicate that the arguments haven't been parsed yet. - // If this method ever exits early without finishing parsing, the - // callee will know where the argument parsing stopped. - Dictionary parsedArguments = new(converterContext.Command.Parameters.Count); - foreach (CommandParameter parameter in converterContext.Command.Parameters) - { - parsedArguments.Add(parameter, new ArgumentNotParsedResult()); - } - - while (converterContext.NextParameter()) - { - object? parsedArgument = await ParseParameterAsync(converterContext); - parsedArguments[converterContext.Parameter] = parsedArgument; - if (parsedArgument is ArgumentFailedConversionResult) - { - // Stop parsing if the argument failed to convert. - // The other parameters will be set to . - // ...XML docs don't work in comments. Pretend they do <3 - break; - } - } - - return parsedArguments; - } - - /// - /// Parses a single parameter from the command context. This method will handle annotated parameters. - /// - /// The converter context containing all the relevant data for the argument parsing. - public virtual async ValueTask ParseParameterAsync(TConverterContext converterContext) - { - if (this.extension is null) - { - throw new InvalidOperationException("The processor has not been configured yet."); - } - - try - { - ConverterDelegate converterDelegate = this.ConverterDelegates[IArgumentConverter.GetConverterFriendlyBaseType(converterContext.Parameter.Type)]; - IOptional optional = await converterDelegate(converterContext); - if (optional.HasValue) - { - // Thanks Roslyn for not yelling at me to make a ternary operator. - return optional.RawValue; - } - - // If there's invalid input, the argument converter should throw. - // Returning an Optional with no value means that the argument converter - // expected this case and intentionally failed. - // We return a special value here to indicate that the argument failed conversion, - // which allows the callee to choose how to handle the failure - // (e.g. return an error message or selecting the default value). - return new ArgumentFailedConversionResult(); - } - catch (Exception error) - { - // If an exception occurs during argument parsing, parsing is immediately stopped. - // We'll set the current parameter to an error state and return the parsed arguments. - // The callee will choose how to handle the error. - return new ArgumentFailedConversionResult - { - Error = error, - Value = converterContext.Argument, - }; - } - } - - /// - /// Executes an argument converter on the specified context. - /// - protected virtual async ValueTask ExecuteConverterAsync(TConverter converter, TConverterContext context) - { - if (!context.NextArgument()) - { - // Try to return the default value if it exists. - return context.Parameter.DefaultValue.HasValue - ? Optional.FromValue(context.Parameter.DefaultValue.Value) - : Optional.FromValue(new ArgumentNotParsedResult()); - } - - if (converter is not IArgumentConverter typedConverter) - { - throw new InvalidOperationException($"The converter {converter.GetType().FullName} does not implement IArgumentConverter<{typeof(T).FullName}>."); - } - // If the parameter is a vararg parameter or params, we'll - // parse all the arguments until we reach the maximum argument count. - else if (context.VariadicArgumentAttribute is null) - { - return await typedConverter.ConvertAsync(context); - } - // Call next variadic-argument to ensure that the context is ready to parse the next argument. - else if (!context.NextVariadicArgument()) - { - return Optional.FromValue(new() - { - Value = context.Argument - }); - } - - // int.MaxValue is used to indicate that there's no maximum argument count. - // If there's a maximum argument count, we'll ensure that the list has enough - // capacity to store all the arguments. - // `params` parameters are treated as vararg parameters with no maximum argument count. - // Due to `params` being semi-used, let's not allocate 2+ gigabytes of memory for a single parameter. - List varArgValues = []; - if (context.VariadicArgumentAttribute.MaximumArgumentCount != int.MaxValue) - { - varArgValues.EnsureCapacity(context.VariadicArgumentAttribute.MaximumArgumentCount); - } - - // This is a do-while loop because we called NextArgument() at the top of the method. - do - { - Optional parsedArgument = await typedConverter.ConvertAsync(context); - if (!parsedArgument.HasValue) - { - // If the argument converter failed, we might - // have reached the end of the arguments. - // Return what we have now, the next time this - // method is invoked, we'll be able to determine if - // the argument failed to convert or if there are - // no more arguments for this parameter. - return Optional.FromValue(new() - { - Value = context.Argument - }); - } - - varArgValues.Add(parsedArgument.Value); - } while (context.NextArgument() && context.NextVariadicArgument()); - - if (varArgValues.Count < context.VariadicArgumentAttribute.MinimumArgumentCount) - { - // If the minimum argument count isn't met, we'll return an error. - // The callee will choose how to handle the error. - return Optional.FromValue(new() - { - Error = new ArgumentException($"The parameter {context.Parameter.Name} requires at least {context.VariadicArgumentAttribute.MinimumArgumentCount:N0} arguments, but only {varArgValues.Count:N0} were provided."), - Value = varArgValues.ToArray(), - }); - } - - // Oh my heart (varArgValues.ToArray()) - : Optional.FromValue>(varArgValues); - } - - /// - /// Constructs a command context from the parsed arguments and the current state of the . - /// - /// The context used for the argument converters. - /// The arguments successfully parsed by the argument converters. - /// The constructed command context. - public abstract TCommandContext CreateCommandContext(TConverterContext converterContext, IReadOnlyDictionary parsedArguments); -} +using System; +using System.Collections.Frozen; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using DSharpPlus.Commands.ArgumentModifiers; +using DSharpPlus.Commands.Converters; +using DSharpPlus.Commands.Converters.Results; +using DSharpPlus.Commands.Trees; +using DSharpPlus.Entities; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace DSharpPlus.Commands.Processors; + +/// +/// A command processor containing command logic that's shared between all command processors. +/// +/// +/// When implementing a new command processor, it's recommended to inherit from this class. +/// You can however implement directly instead, if desired. +/// +/// +/// The converter type that's associated with this command processor. +/// May have extra metadata related to this processor specifically. +/// +/// The context type that's used for argument converters. +/// The context type that's used for command execution. +public abstract partial class BaseCommandProcessor : ICommandProcessor + where TConverter : class, IArgumentConverter + where TConverterContext : ConverterContext + where TCommandContext : CommandContext +{ + /// + public Type ContextType => typeof(TCommandContext); + + /// + public abstract IReadOnlyList Commands { get; } + + /// + public IReadOnlyDictionary Converters { get; protected set; } = FrozenDictionary.Empty; + + /// + IReadOnlyDictionary ICommandProcessor.Converters => Unsafe.As>(this.Converters); + + /// + /// A dictionary of argument converter delegates indexed by the output type they convert to. + /// + public IReadOnlyDictionary ConverterDelegates { get; protected set; } = FrozenDictionary.Empty; + + /// + /// A dictionary of argument converter factories indexed by the output type they convert to. + /// These factories populate the and dictionaries. + /// + protected Dictionary converterFactories = []; + + /// + /// The extension this processor belongs to. + /// + protected CommandsExtension? extension; + + /// + /// The logger for this processor. + /// + protected ILogger> logger = + NullLogger>.Instance; + + /// + // TODO: Register to the service provider and create the converters through the service provider. + public virtual void AddConverter() where T : TConverter, new() => AddConverter(typeof(T), new T()); + + /// + /// Registers a new argument converter with the processor. + /// + /// The converter to register. + /// The type that the converter converts to. + public virtual void AddConverter(TConverter converter) => AddConverter(typeof(T), converter); + + /// + /// Registers a new argument converter with the processor. + /// + /// The type that the converter converts to. + /// The converter to register. + public virtual void AddConverter(Type type, TConverter converter) => AddConverter(new(this, type, converter)); + + /// + /// Scans the specified assembly for argument converters and registers them with the processor. + /// The argument converters will be created through the provided to the . + /// + /// The assembly to scan for argument converters. + public virtual void AddConverters(Assembly assembly) => AddConverters(assembly.GetTypes()); + + /// + /// Adds multiple argument converters to the processor. + /// + /// + /// This method WILL NOT THROW if a converter is invalid. Instead, it will log an error and continue. + /// + /// The types to add as argument converters. + public virtual void AddConverters(IEnumerable types) + { + foreach (Type type in types) + { + // Ignore types that don't have a concrete implementation (abstract classes or interfaces) + // Additionally ignore types that have open generics (IArgumentConverter) + // instead of closed generics (IArgumentConverter) + if (type.IsAbstract || type.IsInterface || type.IsGenericTypeDefinition || !type.IsAssignableTo(typeof(TConverter))) + { + continue; + } + + // Check if the type implements IArgumentConverter + Type? genericArgumentConverter = type.GetInterfaces().FirstOrDefault(type => type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IArgumentConverter<>)); + if (genericArgumentConverter is null) + { + BaseCommandLogging.invalidArgumentConverterImplementation( + this.logger, + type.FullName ?? type.Name, + typeof(IArgumentConverter<>).FullName ?? typeof(IArgumentConverter<>).Name, + null + ); + + continue; + } + + // GenericTypeArguments[0] here is the T in IArgumentConverter + AddConverter(new(this, genericArgumentConverter.GenericTypeArguments[0], type)); + } + } + + /// + /// Registers a new argument converter factory with the processor. + /// + /// The factory that will create the argument converter and it's delegate. + protected virtual void AddConverter(ConverterDelegateFactory factory) + { + if (this.converterFactories.TryGetValue(factory.ParameterType, out ConverterDelegateFactory? existingFactory)) + { + // If it's a different factory trying to be added, log it. + // If it's the same factory that's being readded (likely + // from a gateway disconnect), ignore it. + if (existingFactory != factory) + { + BaseCommandLogging.duplicateArgumentConvertersRegistered( + this.logger, + factory.ToString()!, + factory.ParameterType.FullName ?? factory.ParameterType.Name, + existingFactory.ToString()!, + null + ); + } + + return; + } + + this.converterFactories.Add(factory.ParameterType, factory); + } + + /// + /// Finds all parameters that are enums and creates a generic enum converter for them. + /// + protected virtual void AddEnumConverters(Type? genericEnumConverterType = null) + { + if (this.extension is null) + { + throw new InvalidOperationException("The processor has not been configured yet."); + } + + // If the generic enum converter type is not provided, use the default EnumConverter<>. + genericEnumConverterType ??= typeof(EnumConverter<>); + + // For every enum type found, add the enum converter to it directly. + Dictionary enumConverterCache = []; + foreach (Command command in this.extension.Commands.Values.SelectMany(command => command.Flatten())) + { + foreach (CommandParameter parameter in command.Parameters) + { + Type baseType = IArgumentConverter.GetConverterFriendlyBaseType(parameter.Type); + if (!baseType.IsEnum) + { + continue; + } + + // Try to reuse any existing enum converters for this enum type. + if (!enumConverterCache.TryGetValue(baseType, out TConverter? enumConverter)) + { + Type genericConverter = genericEnumConverterType.MakeGenericType(baseType); + ConstructorInfo? constructor = genericConverter.GetConstructor([]) + ?? throw new UnreachableException($"The generic enum converter {genericConverter.FullName!} does not have a parameterless constructor."); + + enumConverter = (TConverter)constructor.Invoke([]); + enumConverterCache.Add(baseType, enumConverter); + } + + AddConverter(baseType, enumConverter); + } + } + } + + /// + [MemberNotNull(nameof(extension))] + public virtual ValueTask ConfigureAsync(CommandsExtension extension) + { + this.extension = extension; + this.logger = extension.ServiceProvider.GetService>>() + ?? NullLogger>.Instance; + + // Register all converters from the processor's assembly + AddConverters(GetType().Assembly); + + // This goes through all command parameters and creates the generic version of the enum converters. + AddEnumConverters(); + + // Populate the default converters + Dictionary converters = []; + Dictionary converterDelegates = []; + foreach (KeyValuePair factory in this.converterFactories) + { + converters.Add(factory.Key, factory.Value.GetConverter(extension.ServiceProvider)); + converterDelegates.Add(factory.Key, factory.Value.GetConverterDelegate(extension.ServiceProvider)); + } + + this.Converters = converters.ToFrozenDictionary(); + this.ConverterDelegates = converterDelegates.ToFrozenDictionary(); + return default; + } + + /// + /// Parses the arguments provided to the command and returns a prepared command context. + /// + /// The context used for the argument converters. + /// The prepared CommandContext. + public virtual async ValueTask> ParseParametersAsync(TConverterContext converterContext) + { + // If there's no parameters, begone. + if (converterContext.Command.Parameters.Count == 0) + { + return FrozenDictionary.Empty; + } + + // Populate the parsed arguments with + // to indicate that the arguments haven't been parsed yet. + // If this method ever exits early without finishing parsing, the + // callee will know where the argument parsing stopped. + Dictionary parsedArguments = new(converterContext.Command.Parameters.Count); + foreach (CommandParameter parameter in converterContext.Command.Parameters) + { + parsedArguments.Add(parameter, new ArgumentNotParsedResult()); + } + + while (converterContext.NextParameter()) + { + object? parsedArgument = await ParseParameterAsync(converterContext); + parsedArguments[converterContext.Parameter] = parsedArgument; + if (parsedArgument is ArgumentFailedConversionResult) + { + // Stop parsing if the argument failed to convert. + // The other parameters will be set to . + // ...XML docs don't work in comments. Pretend they do <3 + break; + } + } + + return parsedArguments; + } + + /// + /// Parses a single parameter from the command context. This method will handle annotated parameters. + /// + /// The converter context containing all the relevant data for the argument parsing. + public virtual async ValueTask ParseParameterAsync(TConverterContext converterContext) + { + if (this.extension is null) + { + throw new InvalidOperationException("The processor has not been configured yet."); + } + + try + { + ConverterDelegate converterDelegate = this.ConverterDelegates[IArgumentConverter.GetConverterFriendlyBaseType(converterContext.Parameter.Type)]; + IOptional optional = await converterDelegate(converterContext); + if (optional.HasValue) + { + // Thanks Roslyn for not yelling at me to make a ternary operator. + return optional.RawValue; + } + + // If there's invalid input, the argument converter should throw. + // Returning an Optional with no value means that the argument converter + // expected this case and intentionally failed. + // We return a special value here to indicate that the argument failed conversion, + // which allows the callee to choose how to handle the failure + // (e.g. return an error message or selecting the default value). + return new ArgumentFailedConversionResult(); + } + catch (Exception error) + { + // If an exception occurs during argument parsing, parsing is immediately stopped. + // We'll set the current parameter to an error state and return the parsed arguments. + // The callee will choose how to handle the error. + return new ArgumentFailedConversionResult + { + Error = error, + Value = converterContext.Argument, + }; + } + } + + /// + /// Executes an argument converter on the specified context. + /// + protected virtual async ValueTask ExecuteConverterAsync(TConverter converter, TConverterContext context) + { + if (!context.NextArgument()) + { + // Try to return the default value if it exists. + return context.Parameter.DefaultValue.HasValue + ? Optional.FromValue(context.Parameter.DefaultValue.Value) + : Optional.FromValue(new ArgumentNotParsedResult()); + } + + if (converter is not IArgumentConverter typedConverter) + { + throw new InvalidOperationException($"The converter {converter.GetType().FullName} does not implement IArgumentConverter<{typeof(T).FullName}>."); + } + // If the parameter is a vararg parameter or params, we'll + // parse all the arguments until we reach the maximum argument count. + else if (context.VariadicArgumentAttribute is null) + { + return await typedConverter.ConvertAsync(context); + } + // Call next variadic-argument to ensure that the context is ready to parse the next argument. + else if (!context.NextVariadicArgument()) + { + return Optional.FromValue(new() + { + Value = context.Argument + }); + } + + // int.MaxValue is used to indicate that there's no maximum argument count. + // If there's a maximum argument count, we'll ensure that the list has enough + // capacity to store all the arguments. + // `params` parameters are treated as vararg parameters with no maximum argument count. + // Due to `params` being semi-used, let's not allocate 2+ gigabytes of memory for a single parameter. + List varArgValues = []; + if (context.VariadicArgumentAttribute.MaximumArgumentCount != int.MaxValue) + { + varArgValues.EnsureCapacity(context.VariadicArgumentAttribute.MaximumArgumentCount); + } + + // This is a do-while loop because we called NextArgument() at the top of the method. + do + { + Optional parsedArgument = await typedConverter.ConvertAsync(context); + if (!parsedArgument.HasValue) + { + // If the argument converter failed, we might + // have reached the end of the arguments. + // Return what we have now, the next time this + // method is invoked, we'll be able to determine if + // the argument failed to convert or if there are + // no more arguments for this parameter. + return Optional.FromValue(new() + { + Value = context.Argument + }); + } + + varArgValues.Add(parsedArgument.Value); + } while (context.NextArgument() && context.NextVariadicArgument()); + + if (varArgValues.Count < context.VariadicArgumentAttribute.MinimumArgumentCount) + { + // If the minimum argument count isn't met, we'll return an error. + // The callee will choose how to handle the error. + return Optional.FromValue(new() + { + Error = new ArgumentException($"The parameter {context.Parameter.Name} requires at least {context.VariadicArgumentAttribute.MinimumArgumentCount:N0} arguments, but only {varArgValues.Count:N0} were provided."), + Value = varArgValues.ToArray(), + }); + } + + // Oh my heart (varArgValues.ToArray()) + : Optional.FromValue>(varArgValues); + } + + /// + /// Constructs a command context from the parsed arguments and the current state of the . + /// + /// The context used for the argument converters. + /// The arguments successfully parsed by the argument converters. + /// The constructed command context. + public abstract TCommandContext CreateCommandContext(TConverterContext converterContext, IReadOnlyDictionary parsedArguments); +} diff --git a/DSharpPlus.Commands/Processors/BaseCommandProcessor/ICommandProcessor.cs b/DSharpPlus.Commands/Processors/BaseCommandProcessor/ICommandProcessor.cs index 7394660526..4906bf26bd 100644 --- a/DSharpPlus.Commands/Processors/BaseCommandProcessor/ICommandProcessor.cs +++ b/DSharpPlus.Commands/Processors/BaseCommandProcessor/ICommandProcessor.cs @@ -1,34 +1,34 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using DSharpPlus.Commands.Converters; -using DSharpPlus.Commands.Trees; - -namespace DSharpPlus.Commands.Processors; - -public interface ICommandProcessor -{ - /// - /// Processor specific context type. Context type which is provided on command invokation - /// - public Type ContextType { get; } - - /// - /// A dictionary of argument converters indexed by the type they convert to. - /// - public IReadOnlyDictionary Converters { get; } - - /// - /// List of commands which are registered to this processor - /// - public IReadOnlyList Commands { get; } - - /// - /// This method is called on initial setup and when the extension is refreshed. - /// Register your needed event handlers here but use a mechanism to track - /// if the inital setup was already done and if this call is only a refresh - /// - /// Extension this processor belongs to - /// - public ValueTask ConfigureAsync(CommandsExtension extension); -} +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using DSharpPlus.Commands.Converters; +using DSharpPlus.Commands.Trees; + +namespace DSharpPlus.Commands.Processors; + +public interface ICommandProcessor +{ + /// + /// Processor specific context type. Context type which is provided on command invokation + /// + public Type ContextType { get; } + + /// + /// A dictionary of argument converters indexed by the type they convert to. + /// + public IReadOnlyDictionary Converters { get; } + + /// + /// List of commands which are registered to this processor + /// + public IReadOnlyList Commands { get; } + + /// + /// This method is called on initial setup and when the extension is refreshed. + /// Register your needed event handlers here but use a mechanism to track + /// if the inital setup was already done and if this call is only a refresh + /// + /// Extension this processor belongs to + /// + public ValueTask ConfigureAsync(CommandsExtension extension); +} diff --git a/DSharpPlus.Commands/Processors/MessageCommands/MessageCommandContext.cs b/DSharpPlus.Commands/Processors/MessageCommands/MessageCommandContext.cs index f655dfe8ce..88b74b60b5 100644 --- a/DSharpPlus.Commands/Processors/MessageCommands/MessageCommandContext.cs +++ b/DSharpPlus.Commands/Processors/MessageCommands/MessageCommandContext.cs @@ -1,8 +1,8 @@ -using DSharpPlus.Commands.Processors.SlashCommands; - -namespace DSharpPlus.Commands.Processors.MessageCommands; - -/// -/// Indicates that the command was invoked via a message interaction. -/// -public record MessageCommandContext : SlashCommandContext; +using DSharpPlus.Commands.Processors.SlashCommands; + +namespace DSharpPlus.Commands.Processors.MessageCommands; + +/// +/// Indicates that the command was invoked via a message interaction. +/// +public record MessageCommandContext : SlashCommandContext; diff --git a/DSharpPlus.Commands/Processors/MessageCommands/MessageCommandLogging.cs b/DSharpPlus.Commands/Processors/MessageCommands/MessageCommandLogging.cs index 0460cc8f22..8aa3153a9b 100644 --- a/DSharpPlus.Commands/Processors/MessageCommands/MessageCommandLogging.cs +++ b/DSharpPlus.Commands/Processors/MessageCommands/MessageCommandLogging.cs @@ -1,14 +1,14 @@ -using System; -using DSharpPlus.Commands.Processors.SlashCommands; -using Microsoft.Extensions.Logging; - -namespace DSharpPlus.Commands.Processors.MessageCommands; - -internal static class MessageCommandLogging -{ - internal static readonly Action interactionReceivedBeforeConfigured = LoggerMessage.Define(LogLevel.Warning, new EventId(1, "Message Commands Startup"), "Received an interaction before the message commands processor was configured. This interaction will be ignored."); - internal static readonly Action messageCommandCannotHaveSubcommands = LoggerMessage.Define(LogLevel.Warning, new EventId(4, "Message Commands Startup"), "The message context menu command '{CommandName}' cannot have subcommands."); - internal static readonly Action messageCommandContextParameterType = LoggerMessage.Define(LogLevel.Warning, new EventId(5, "Message Commands Startup"), $"The first parameter of '{{CommandName}}' does not implement {nameof(SlashCommandContext)}. Since this command is being registered as a message context menu command, it's first parameter must inherit the {nameof(SlashCommandContext)} class."); - internal static readonly Action invalidParameterType = LoggerMessage.Define(LogLevel.Warning, new EventId(2, "Message Commands Startup"), "The second parameter of '{CommandName}' is not a DiscordMessage. Since this command is being registered as a message context menu command, it's second parameter must be a DiscordMessage."); - internal static readonly Action invalidParameterMissingDefaultValue = LoggerMessage.Define(LogLevel.Warning, new EventId(3, "Message Commands Startup"), "Parameter {ParameterIndex} of '{CommandName}' does not have a default value. Since this command is being registered as a message context menu command, any additional parameters must have a default value."); -} +using System; +using DSharpPlus.Commands.Processors.SlashCommands; +using Microsoft.Extensions.Logging; + +namespace DSharpPlus.Commands.Processors.MessageCommands; + +internal static class MessageCommandLogging +{ + internal static readonly Action interactionReceivedBeforeConfigured = LoggerMessage.Define(LogLevel.Warning, new EventId(1, "Message Commands Startup"), "Received an interaction before the message commands processor was configured. This interaction will be ignored."); + internal static readonly Action messageCommandCannotHaveSubcommands = LoggerMessage.Define(LogLevel.Warning, new EventId(4, "Message Commands Startup"), "The message context menu command '{CommandName}' cannot have subcommands."); + internal static readonly Action messageCommandContextParameterType = LoggerMessage.Define(LogLevel.Warning, new EventId(5, "Message Commands Startup"), $"The first parameter of '{{CommandName}}' does not implement {nameof(SlashCommandContext)}. Since this command is being registered as a message context menu command, it's first parameter must inherit the {nameof(SlashCommandContext)} class."); + internal static readonly Action invalidParameterType = LoggerMessage.Define(LogLevel.Warning, new EventId(2, "Message Commands Startup"), "The second parameter of '{CommandName}' is not a DiscordMessage. Since this command is being registered as a message context menu command, it's second parameter must be a DiscordMessage."); + internal static readonly Action invalidParameterMissingDefaultValue = LoggerMessage.Define(LogLevel.Warning, new EventId(3, "Message Commands Startup"), "Parameter {ParameterIndex} of '{CommandName}' does not have a default value. Since this command is being registered as a message context menu command, any additional parameters must have a default value."); +} diff --git a/DSharpPlus.Commands/Processors/MessageCommands/MessageCommandProcessor.cs b/DSharpPlus.Commands/Processors/MessageCommands/MessageCommandProcessor.cs index d156003f44..01921440dc 100644 --- a/DSharpPlus.Commands/Processors/MessageCommands/MessageCommandProcessor.cs +++ b/DSharpPlus.Commands/Processors/MessageCommands/MessageCommandProcessor.cs @@ -1,197 +1,197 @@ -using System; -using System.Collections.Frozen; -using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; -using DSharpPlus.Commands.ContextChecks; -using DSharpPlus.Commands.Converters; -using DSharpPlus.Commands.EventArgs; -using DSharpPlus.Commands.Exceptions; -using DSharpPlus.Commands.Processors.SlashCommands; -using DSharpPlus.Commands.Processors.SlashCommands.Localization; -using DSharpPlus.Commands.Processors.SlashCommands.Metadata; -using DSharpPlus.Commands.Trees; -using DSharpPlus.Commands.Trees.Metadata; -using DSharpPlus.Entities; -using DSharpPlus.EventArgs; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; - -namespace DSharpPlus.Commands.Processors.MessageCommands; - -public sealed class MessageCommandProcessor : ICommandProcessor -{ - /// - public Type ContextType => typeof(SlashCommandContext); - - /// - public IReadOnlyDictionary Converters => this.slashCommandProcessor is not null - ? Unsafe.As>(this.slashCommandProcessor.Converters) - : FrozenDictionary.Empty; - - /// - public IReadOnlyList Commands => this.commands; - private readonly List commands = []; - - private CommandsExtension? extension; - private SlashCommandProcessor? slashCommandProcessor; - - /// - public async ValueTask ConfigureAsync(CommandsExtension extension) - { - this.extension = extension; - this.slashCommandProcessor = this.extension.GetProcessor() ?? new SlashCommandProcessor(); - - ILogger logger = this.extension.ServiceProvider.GetService>() ?? NullLogger.Instance; - List applicationCommands = []; - - IReadOnlyList commands = this.extension.GetCommandsForProcessor(this); - IEnumerable flattenCommands = commands.SelectMany(x => x.Flatten()); - - foreach (Command command in flattenCommands) - { - // Message commands must be explicitly defined as such, otherwise they are ignored. - if (!command.Attributes.Any(x => x is SlashCommandTypesAttribute slashCommandTypesAttribute - && slashCommandTypesAttribute.ApplicationCommandTypes.Contains(DiscordApplicationCommandType.MessageContextMenu))) - { - continue; - } - // Ensure there are no subcommands. - else if (command.Subcommands.Count != 0) - { - MessageCommandLogging.messageCommandCannotHaveSubcommands(logger, command.FullName, null); - continue; - } - else if (!command.Method!.GetParameters()[0].ParameterType.IsAssignableFrom(typeof(MessageCommandContext))) - { - MessageCommandLogging.messageCommandContextParameterType(logger, command.FullName, null); - continue; - } - // Check to see if the method signature is valid. - else if (command.Parameters.Count < 1 || IArgumentConverter.GetConverterFriendlyBaseType(command.Parameters[0].Type) != typeof(DiscordMessage)) - { - MessageCommandLogging.invalidParameterType(logger, command.FullName, null); - continue; - } - - // Iterate over all parameters and ensure they have default values. - for (int i = 1; i < command.Parameters.Count; i++) - { - if (!command.Parameters[i].DefaultValue.HasValue) - { - MessageCommandLogging.invalidParameterMissingDefaultValue(logger, i, command.FullName, null); - continue; - } - } - - this.commands.Add(command); - applicationCommands.Add(await ToApplicationCommandAsync(command)); - } - - this.slashCommandProcessor.AddApplicationCommands(applicationCommands); - } - - public async Task ExecuteInteractionAsync(DiscordClient client, ContextMenuInteractionCreatedEventArgs eventArgs) - { - if (this.extension is null || this.slashCommandProcessor is null) - { - throw new InvalidOperationException("SlashCommandProcessor has not been configured."); - } - else if (eventArgs.Interaction.Type is not DiscordInteractionType.ApplicationCommand - || eventArgs.Interaction.Data.Type is not DiscordApplicationCommandType.MessageContextMenu) - { - return; - } - - AsyncServiceScope scope = this.extension.ServiceProvider.CreateAsyncScope(); - if (this.slashCommandProcessor.ApplicationCommandMapping.Count == 0) - { - ILogger logger = this.extension.ServiceProvider.GetService>() ?? NullLogger.Instance; - logger.LogWarning("Received an interaction for a message command, but commands have not been registered yet. Ignoring n"); - } - - if (!this.slashCommandProcessor.TryFindCommand(eventArgs.Interaction, out Command? command, out _)) - { - await this.extension.commandErrored.InvokeAsync(this.extension, new CommandErroredEventArgs() - { - Context = new MessageCommandContext() - { - Arguments = new Dictionary(), - Channel = eventArgs.Interaction.Channel, - Command = null!, - Extension = this.extension, - Interaction = eventArgs.Interaction, - Options = eventArgs.Interaction.Data.Options ?? [], - ServiceScope = scope, - User = eventArgs.Interaction.User, - }, - CommandObject = null, - Exception = new CommandNotFoundException(eventArgs.Interaction.Data.Name), - }); - - await scope.DisposeAsync(); - return; - } - - // The first parameter for MessageContextMenu commands is always the DiscordMessage. - Dictionary arguments = new() { { command.Parameters[0], eventArgs.TargetMessage } }; - - // Because methods can have multiple interaction invocation types, - // there has been a demand to be able to register methods with multiple - // parameters, even for MessageContextMenu commands. - // The condition is that all the parameters on the method must have default values. - for (int i = 1; i < command.Parameters.Count; i++) - { - // We verify at startup that all parameters have default values. - arguments.Add(command.Parameters[i], command.Parameters[i].DefaultValue.Value); - } - - MessageCommandContext commandContext = new() - { - Arguments = arguments, - Channel = eventArgs.Interaction.Channel, - Command = command, - Extension = this.extension, - Interaction = eventArgs.Interaction, - Options = [], - ServiceScope = scope, - User = eventArgs.Interaction.User, - }; - - await this.extension.CommandExecutor.ExecuteAsync(commandContext); - } - - public async Task ToApplicationCommandAsync(Command command) - { - if (this.slashCommandProcessor is null) - { - throw new InvalidOperationException("SlashCommandProcessor has not been configured."); - } - - IReadOnlyDictionary nameLocalizations = FrozenDictionary.Empty; - if (command.Attributes.OfType().FirstOrDefault() is InteractionLocalizerAttribute localizerAttribute) - { - nameLocalizations = await this.slashCommandProcessor.ExecuteLocalizerAsync(localizerAttribute.LocalizerType, $"{command.FullName}.name"); - } - - DiscordPermissions? userPermissions = command.Attributes.OfType().FirstOrDefault()?.UserPermissions; - - return new - ( - name: command.Attributes.OfType().FirstOrDefault()?.DisplayName ?? command.FullName, - description: string.Empty, - type: DiscordApplicationCommandType.MessageContextMenu, - name_localizations: nameLocalizations, - allowDMUsage: command.Attributes.Any(x => x is AllowDMUsageAttribute), - defaultMemberPermissions: userPermissions is not null - ? userPermissions - : new DiscordPermissions(DiscordPermission.UseApplicationCommands), - nsfw: command.Attributes.Any(x => x is RequireNsfwAttribute), - contexts: command.Attributes.OfType().FirstOrDefault()?.AllowedContexts, - integrationTypes: command.Attributes.OfType().FirstOrDefault()?.InstallTypes - ); - } -} +using System; +using System.Collections.Frozen; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using DSharpPlus.Commands.ContextChecks; +using DSharpPlus.Commands.Converters; +using DSharpPlus.Commands.EventArgs; +using DSharpPlus.Commands.Exceptions; +using DSharpPlus.Commands.Processors.SlashCommands; +using DSharpPlus.Commands.Processors.SlashCommands.Localization; +using DSharpPlus.Commands.Processors.SlashCommands.Metadata; +using DSharpPlus.Commands.Trees; +using DSharpPlus.Commands.Trees.Metadata; +using DSharpPlus.Entities; +using DSharpPlus.EventArgs; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace DSharpPlus.Commands.Processors.MessageCommands; + +public sealed class MessageCommandProcessor : ICommandProcessor +{ + /// + public Type ContextType => typeof(SlashCommandContext); + + /// + public IReadOnlyDictionary Converters => this.slashCommandProcessor is not null + ? Unsafe.As>(this.slashCommandProcessor.Converters) + : FrozenDictionary.Empty; + + /// + public IReadOnlyList Commands => this.commands; + private readonly List commands = []; + + private CommandsExtension? extension; + private SlashCommandProcessor? slashCommandProcessor; + + /// + public async ValueTask ConfigureAsync(CommandsExtension extension) + { + this.extension = extension; + this.slashCommandProcessor = this.extension.GetProcessor() ?? new SlashCommandProcessor(); + + ILogger logger = this.extension.ServiceProvider.GetService>() ?? NullLogger.Instance; + List applicationCommands = []; + + IReadOnlyList commands = this.extension.GetCommandsForProcessor(this); + IEnumerable flattenCommands = commands.SelectMany(x => x.Flatten()); + + foreach (Command command in flattenCommands) + { + // Message commands must be explicitly defined as such, otherwise they are ignored. + if (!command.Attributes.Any(x => x is SlashCommandTypesAttribute slashCommandTypesAttribute + && slashCommandTypesAttribute.ApplicationCommandTypes.Contains(DiscordApplicationCommandType.MessageContextMenu))) + { + continue; + } + // Ensure there are no subcommands. + else if (command.Subcommands.Count != 0) + { + MessageCommandLogging.messageCommandCannotHaveSubcommands(logger, command.FullName, null); + continue; + } + else if (!command.Method!.GetParameters()[0].ParameterType.IsAssignableFrom(typeof(MessageCommandContext))) + { + MessageCommandLogging.messageCommandContextParameterType(logger, command.FullName, null); + continue; + } + // Check to see if the method signature is valid. + else if (command.Parameters.Count < 1 || IArgumentConverter.GetConverterFriendlyBaseType(command.Parameters[0].Type) != typeof(DiscordMessage)) + { + MessageCommandLogging.invalidParameterType(logger, command.FullName, null); + continue; + } + + // Iterate over all parameters and ensure they have default values. + for (int i = 1; i < command.Parameters.Count; i++) + { + if (!command.Parameters[i].DefaultValue.HasValue) + { + MessageCommandLogging.invalidParameterMissingDefaultValue(logger, i, command.FullName, null); + continue; + } + } + + this.commands.Add(command); + applicationCommands.Add(await ToApplicationCommandAsync(command)); + } + + this.slashCommandProcessor.AddApplicationCommands(applicationCommands); + } + + public async Task ExecuteInteractionAsync(DiscordClient client, ContextMenuInteractionCreatedEventArgs eventArgs) + { + if (this.extension is null || this.slashCommandProcessor is null) + { + throw new InvalidOperationException("SlashCommandProcessor has not been configured."); + } + else if (eventArgs.Interaction.Type is not DiscordInteractionType.ApplicationCommand + || eventArgs.Interaction.Data.Type is not DiscordApplicationCommandType.MessageContextMenu) + { + return; + } + + AsyncServiceScope scope = this.extension.ServiceProvider.CreateAsyncScope(); + if (this.slashCommandProcessor.ApplicationCommandMapping.Count == 0) + { + ILogger logger = this.extension.ServiceProvider.GetService>() ?? NullLogger.Instance; + logger.LogWarning("Received an interaction for a message command, but commands have not been registered yet. Ignoring n"); + } + + if (!this.slashCommandProcessor.TryFindCommand(eventArgs.Interaction, out Command? command, out _)) + { + await this.extension.commandErrored.InvokeAsync(this.extension, new CommandErroredEventArgs() + { + Context = new MessageCommandContext() + { + Arguments = new Dictionary(), + Channel = eventArgs.Interaction.Channel, + Command = null!, + Extension = this.extension, + Interaction = eventArgs.Interaction, + Options = eventArgs.Interaction.Data.Options ?? [], + ServiceScope = scope, + User = eventArgs.Interaction.User, + }, + CommandObject = null, + Exception = new CommandNotFoundException(eventArgs.Interaction.Data.Name), + }); + + await scope.DisposeAsync(); + return; + } + + // The first parameter for MessageContextMenu commands is always the DiscordMessage. + Dictionary arguments = new() { { command.Parameters[0], eventArgs.TargetMessage } }; + + // Because methods can have multiple interaction invocation types, + // there has been a demand to be able to register methods with multiple + // parameters, even for MessageContextMenu commands. + // The condition is that all the parameters on the method must have default values. + for (int i = 1; i < command.Parameters.Count; i++) + { + // We verify at startup that all parameters have default values. + arguments.Add(command.Parameters[i], command.Parameters[i].DefaultValue.Value); + } + + MessageCommandContext commandContext = new() + { + Arguments = arguments, + Channel = eventArgs.Interaction.Channel, + Command = command, + Extension = this.extension, + Interaction = eventArgs.Interaction, + Options = [], + ServiceScope = scope, + User = eventArgs.Interaction.User, + }; + + await this.extension.CommandExecutor.ExecuteAsync(commandContext); + } + + public async Task ToApplicationCommandAsync(Command command) + { + if (this.slashCommandProcessor is null) + { + throw new InvalidOperationException("SlashCommandProcessor has not been configured."); + } + + IReadOnlyDictionary nameLocalizations = FrozenDictionary.Empty; + if (command.Attributes.OfType().FirstOrDefault() is InteractionLocalizerAttribute localizerAttribute) + { + nameLocalizations = await this.slashCommandProcessor.ExecuteLocalizerAsync(localizerAttribute.LocalizerType, $"{command.FullName}.name"); + } + + DiscordPermissions? userPermissions = command.Attributes.OfType().FirstOrDefault()?.UserPermissions; + + return new + ( + name: command.Attributes.OfType().FirstOrDefault()?.DisplayName ?? command.FullName, + description: string.Empty, + type: DiscordApplicationCommandType.MessageContextMenu, + name_localizations: nameLocalizations, + allowDMUsage: command.Attributes.Any(x => x is AllowDMUsageAttribute), + defaultMemberPermissions: userPermissions is not null + ? userPermissions + : new DiscordPermissions(DiscordPermission.UseApplicationCommands), + nsfw: command.Attributes.Any(x => x is RequireNsfwAttribute), + contexts: command.Attributes.OfType().FirstOrDefault()?.AllowedContexts, + integrationTypes: command.Attributes.OfType().FirstOrDefault()?.InstallTypes + ); + } +} diff --git a/DSharpPlus.Commands/Processors/SlashCommands/ArgumentModifiers/ChoiceDisplayNameAttribute.cs b/DSharpPlus.Commands/Processors/SlashCommands/ArgumentModifiers/ChoiceDisplayNameAttribute.cs index 3109527ce5..3cddf3245f 100644 --- a/DSharpPlus.Commands/Processors/SlashCommands/ArgumentModifiers/ChoiceDisplayNameAttribute.cs +++ b/DSharpPlus.Commands/Processors/SlashCommands/ArgumentModifiers/ChoiceDisplayNameAttribute.cs @@ -1,12 +1,12 @@ -using System; - -namespace DSharpPlus.Commands.Processors.SlashCommands.ArgumentModifiers; - -/// -/// Used to annotate enum members with a display name for the built-in choice provider. -/// -[AttributeUsage(AttributeTargets.Field, AllowMultiple = false)] -public sealed class ChoiceDisplayNameAttribute(string name) : Attribute -{ - public string DisplayName { get; set; } = name; -} +using System; + +namespace DSharpPlus.Commands.Processors.SlashCommands.ArgumentModifiers; + +/// +/// Used to annotate enum members with a display name for the built-in choice provider. +/// +[AttributeUsage(AttributeTargets.Field, AllowMultiple = false)] +public sealed class ChoiceDisplayNameAttribute(string name) : Attribute +{ + public string DisplayName { get; set; } = name; +} diff --git a/DSharpPlus.Commands/Processors/SlashCommands/ArgumentModifiers/SlashAutoCompleteProviderAttribute.cs b/DSharpPlus.Commands/Processors/SlashCommands/ArgumentModifiers/SlashAutoCompleteProviderAttribute.cs index 736abfd4db..c5beb3f17e 100644 --- a/DSharpPlus.Commands/Processors/SlashCommands/ArgumentModifiers/SlashAutoCompleteProviderAttribute.cs +++ b/DSharpPlus.Commands/Processors/SlashCommands/ArgumentModifiers/SlashAutoCompleteProviderAttribute.cs @@ -1,71 +1,71 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using DSharpPlus.Entities; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; - -namespace DSharpPlus.Commands.Processors.SlashCommands.ArgumentModifiers; - -[AttributeUsage(AttributeTargets.Parameter, Inherited = false, AllowMultiple = false)] -public class SlashAutoCompleteProviderAttribute : Attribute -{ - public Type AutoCompleteType { get; init; } - - public SlashAutoCompleteProviderAttribute(Type autoCompleteType) - { - ArgumentNullException.ThrowIfNull(autoCompleteType, nameof(autoCompleteType)); - if (autoCompleteType.GetInterface(nameof(IAutoCompleteProvider)) is null) - { - throw new ArgumentException("The provided type must implement IAutoCompleteProvider.", nameof(autoCompleteType)); - } - - this.AutoCompleteType = autoCompleteType; - } - - public async ValueTask> AutoCompleteAsync(AutoCompleteContext context) - { - IAutoCompleteProvider autoCompleteProvider; - try - { - autoCompleteProvider = (IAutoCompleteProvider)ActivatorUtilities.CreateInstance(context.ServiceProvider, this.AutoCompleteType); - } - catch (Exception error) - { - ILogger logger = context.ServiceProvider.GetRequiredService>(); - logger.LogError(error, "AutoCompleteProvider '{Type}' for parameter '{ParameterName}' was not able to be constructed.", this.AutoCompleteType, context.Parameter.ToString()); - return []; - } - - List choices = new(25); - foreach (DiscordAutoCompleteChoice choice in await autoCompleteProvider.AutoCompleteAsync(context)) - { - if (choices.Count == 25) - { - ILogger logger = context.ServiceProvider.GetRequiredService>(); - logger.LogWarning( - "AutoCompleteProvider '{Type}' for parameter '{ParameterName}' returned more than 25 choices, only the first 25 will be used.", - this.AutoCompleteType, - context.Parameter.ToString() - ); - - break; - } - - choices.Add(choice); - } - - return choices; - } -} - -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Parameter, Inherited = false, AllowMultiple = false)] -public sealed class SlashAutoCompleteProviderAttribute : SlashAutoCompleteProviderAttribute where T : IAutoCompleteProvider -{ - public SlashAutoCompleteProviderAttribute() : base(typeof(T)) { } -} - -public interface IAutoCompleteProvider -{ - public ValueTask> AutoCompleteAsync(AutoCompleteContext context); -} +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using DSharpPlus.Entities; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace DSharpPlus.Commands.Processors.SlashCommands.ArgumentModifiers; + +[AttributeUsage(AttributeTargets.Parameter, Inherited = false, AllowMultiple = false)] +public class SlashAutoCompleteProviderAttribute : Attribute +{ + public Type AutoCompleteType { get; init; } + + public SlashAutoCompleteProviderAttribute(Type autoCompleteType) + { + ArgumentNullException.ThrowIfNull(autoCompleteType, nameof(autoCompleteType)); + if (autoCompleteType.GetInterface(nameof(IAutoCompleteProvider)) is null) + { + throw new ArgumentException("The provided type must implement IAutoCompleteProvider.", nameof(autoCompleteType)); + } + + this.AutoCompleteType = autoCompleteType; + } + + public async ValueTask> AutoCompleteAsync(AutoCompleteContext context) + { + IAutoCompleteProvider autoCompleteProvider; + try + { + autoCompleteProvider = (IAutoCompleteProvider)ActivatorUtilities.CreateInstance(context.ServiceProvider, this.AutoCompleteType); + } + catch (Exception error) + { + ILogger logger = context.ServiceProvider.GetRequiredService>(); + logger.LogError(error, "AutoCompleteProvider '{Type}' for parameter '{ParameterName}' was not able to be constructed.", this.AutoCompleteType, context.Parameter.ToString()); + return []; + } + + List choices = new(25); + foreach (DiscordAutoCompleteChoice choice in await autoCompleteProvider.AutoCompleteAsync(context)) + { + if (choices.Count == 25) + { + ILogger logger = context.ServiceProvider.GetRequiredService>(); + logger.LogWarning( + "AutoCompleteProvider '{Type}' for parameter '{ParameterName}' returned more than 25 choices, only the first 25 will be used.", + this.AutoCompleteType, + context.Parameter.ToString() + ); + + break; + } + + choices.Add(choice); + } + + return choices; + } +} + +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Parameter, Inherited = false, AllowMultiple = false)] +public sealed class SlashAutoCompleteProviderAttribute : SlashAutoCompleteProviderAttribute where T : IAutoCompleteProvider +{ + public SlashAutoCompleteProviderAttribute() : base(typeof(T)) { } +} + +public interface IAutoCompleteProvider +{ + public ValueTask> AutoCompleteAsync(AutoCompleteContext context); +} diff --git a/DSharpPlus.Commands/Processors/SlashCommands/ArgumentModifiers/SlashChoiceProviderAttribute.cs b/DSharpPlus.Commands/Processors/SlashCommands/ArgumentModifiers/SlashChoiceProviderAttribute.cs index e7e2c0832d..0705f20670 100644 --- a/DSharpPlus.Commands/Processors/SlashCommands/ArgumentModifiers/SlashChoiceProviderAttribute.cs +++ b/DSharpPlus.Commands/Processors/SlashCommands/ArgumentModifiers/SlashChoiceProviderAttribute.cs @@ -1,91 +1,91 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using DSharpPlus.Commands.Exceptions; -using DSharpPlus.Commands.Trees; -using DSharpPlus.Entities; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; - -namespace DSharpPlus.Commands.Processors.SlashCommands.ArgumentModifiers; - -[AttributeUsage(AttributeTargets.Parameter, Inherited = false, AllowMultiple = false)] -public class SlashChoiceProviderAttribute : Attribute -{ - public Type ProviderType { get; init; } - - public SlashChoiceProviderAttribute(Type providerType) - { - ArgumentNullException.ThrowIfNull(providerType, nameof(providerType)); - if (providerType.GetInterface(nameof(IChoiceProvider)) is null) - { - throw new ArgumentException("The provided type must implement IChoiceProvider.", nameof(providerType)); - } - - this.ProviderType = providerType; - } - - public async ValueTask> GrabChoicesAsync(IServiceProvider serviceProvider, CommandParameter parameter) - { - IChoiceProvider choiceProvider; - try - { - choiceProvider = (IChoiceProvider) - ActivatorUtilities.CreateInstance(serviceProvider, this.ProviderType); - } - catch (Exception error) - { - ILogger logger = serviceProvider.GetRequiredService>(); - logger.LogError( - error, - "ChoiceProvider '{Type}' for parameter '{ParameterName}' was not able to be constructed.", - this.ProviderType, - parameter.ToString() - ); - - return []; - } - - List choices = new(25); - IEnumerable userProvidedChoices; - - try - { - userProvidedChoices = await choiceProvider.ProvideAsync(parameter); - } - catch(Exception e) - { - throw new ChoiceProviderFailedException(this.ProviderType, e); - } - - foreach (DiscordApplicationCommandOptionChoice choice in userProvidedChoices) - { - if (choices.Count == 25) - { - ILogger logger = serviceProvider.GetRequiredService>(); - logger.LogWarning( - "ChoiceProvider '{Type}' for parameter '{ParameterName}' returned more than 25 choices, only the first 25 will be used.", - this.ProviderType, - parameter.ToString() - ); - - break; - } - - choices.Add(choice); - } - - return choices; - } -} - -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Parameter, Inherited = false, AllowMultiple = false)] -public sealed class SlashChoiceProviderAttribute : SlashChoiceProviderAttribute where T : IChoiceProvider -{ - public SlashChoiceProviderAttribute() : base(typeof(T)) { } -} - -public interface IChoiceProvider -{ - public ValueTask> ProvideAsync(CommandParameter parameter); -} +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using DSharpPlus.Commands.Exceptions; +using DSharpPlus.Commands.Trees; +using DSharpPlus.Entities; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace DSharpPlus.Commands.Processors.SlashCommands.ArgumentModifiers; + +[AttributeUsage(AttributeTargets.Parameter, Inherited = false, AllowMultiple = false)] +public class SlashChoiceProviderAttribute : Attribute +{ + public Type ProviderType { get; init; } + + public SlashChoiceProviderAttribute(Type providerType) + { + ArgumentNullException.ThrowIfNull(providerType, nameof(providerType)); + if (providerType.GetInterface(nameof(IChoiceProvider)) is null) + { + throw new ArgumentException("The provided type must implement IChoiceProvider.", nameof(providerType)); + } + + this.ProviderType = providerType; + } + + public async ValueTask> GrabChoicesAsync(IServiceProvider serviceProvider, CommandParameter parameter) + { + IChoiceProvider choiceProvider; + try + { + choiceProvider = (IChoiceProvider) + ActivatorUtilities.CreateInstance(serviceProvider, this.ProviderType); + } + catch (Exception error) + { + ILogger logger = serviceProvider.GetRequiredService>(); + logger.LogError( + error, + "ChoiceProvider '{Type}' for parameter '{ParameterName}' was not able to be constructed.", + this.ProviderType, + parameter.ToString() + ); + + return []; + } + + List choices = new(25); + IEnumerable userProvidedChoices; + + try + { + userProvidedChoices = await choiceProvider.ProvideAsync(parameter); + } + catch(Exception e) + { + throw new ChoiceProviderFailedException(this.ProviderType, e); + } + + foreach (DiscordApplicationCommandOptionChoice choice in userProvidedChoices) + { + if (choices.Count == 25) + { + ILogger logger = serviceProvider.GetRequiredService>(); + logger.LogWarning( + "ChoiceProvider '{Type}' for parameter '{ParameterName}' returned more than 25 choices, only the first 25 will be used.", + this.ProviderType, + parameter.ToString() + ); + + break; + } + + choices.Add(choice); + } + + return choices; + } +} + +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Parameter, Inherited = false, AllowMultiple = false)] +public sealed class SlashChoiceProviderAttribute : SlashChoiceProviderAttribute where T : IChoiceProvider +{ + public SlashChoiceProviderAttribute() : base(typeof(T)) { } +} + +public interface IChoiceProvider +{ + public ValueTask> ProvideAsync(CommandParameter parameter); +} diff --git a/DSharpPlus.Commands/Processors/SlashCommands/AutoCompleteContext.cs b/DSharpPlus.Commands/Processors/SlashCommands/AutoCompleteContext.cs index 8730397c66..7a97d02e34 100644 --- a/DSharpPlus.Commands/Processors/SlashCommands/AutoCompleteContext.cs +++ b/DSharpPlus.Commands/Processors/SlashCommands/AutoCompleteContext.cs @@ -1,14 +1,14 @@ -using System.Collections.Generic; -using DSharpPlus.Commands.Trees; -using DSharpPlus.Entities; - -namespace DSharpPlus.Commands.Processors.SlashCommands; - -public sealed record AutoCompleteContext : AbstractContext -{ - public required DiscordInteraction Interaction { get; init; } - public required IEnumerable Options { get; init; } - public required IReadOnlyDictionary Arguments { get; init; } - public required CommandParameter Parameter { get; init; } - public required string? UserInput { get; init; } -} +using System.Collections.Generic; +using DSharpPlus.Commands.Trees; +using DSharpPlus.Entities; + +namespace DSharpPlus.Commands.Processors.SlashCommands; + +public sealed record AutoCompleteContext : AbstractContext +{ + public required DiscordInteraction Interaction { get; init; } + public required IEnumerable Options { get; init; } + public required IReadOnlyDictionary Arguments { get; init; } + public required CommandParameter Parameter { get; init; } + public required string? UserInput { get; init; } +} diff --git a/DSharpPlus.Commands/Processors/SlashCommands/ISlashArgumentConverter.cs b/DSharpPlus.Commands/Processors/SlashCommands/ISlashArgumentConverter.cs index 2a1c3da569..7e8788d9f1 100644 --- a/DSharpPlus.Commands/Processors/SlashCommands/ISlashArgumentConverter.cs +++ b/DSharpPlus.Commands/Processors/SlashCommands/ISlashArgumentConverter.cs @@ -1,11 +1,11 @@ -using DSharpPlus.Commands.Converters; -using DSharpPlus.Entities; - -namespace DSharpPlus.Commands.Processors.SlashCommands; - -public interface ISlashArgumentConverter : IArgumentConverter -{ - public DiscordApplicationCommandOptionType ParameterType { get; } -} - -public interface ISlashArgumentConverter : ISlashArgumentConverter, IArgumentConverter; +using DSharpPlus.Commands.Converters; +using DSharpPlus.Entities; + +namespace DSharpPlus.Commands.Processors.SlashCommands; + +public interface ISlashArgumentConverter : IArgumentConverter +{ + public DiscordApplicationCommandOptionType ParameterType { get; } +} + +public interface ISlashArgumentConverter : ISlashArgumentConverter, IArgumentConverter; diff --git a/DSharpPlus.Commands/Processors/SlashCommands/InteractionConverterContext.cs b/DSharpPlus.Commands/Processors/SlashCommands/InteractionConverterContext.cs index c90421dbdd..aeb36d7630 100644 --- a/DSharpPlus.Commands/Processors/SlashCommands/InteractionConverterContext.cs +++ b/DSharpPlus.Commands/Processors/SlashCommands/InteractionConverterContext.cs @@ -1,55 +1,55 @@ -using System.Collections.Generic; -using System.Linq; -using DSharpPlus.Commands.Converters; -using DSharpPlus.Commands.Processors.SlashCommands.InteractionNamingPolicies; -using DSharpPlus.Entities; - -namespace DSharpPlus.Commands.Processors.SlashCommands; - -/// -/// Represents a context for interaction-based argument converters. -/// -public record InteractionConverterContext : ConverterContext -{ - /// - /// The parameter naming policy to use when mapping parameters to interaction data. - /// - public required IInteractionNamingPolicy ParameterNamePolicy { get; init; } - - /// - /// The underlying interaction. - /// - public required DiscordInteraction Interaction { get; init; } - - /// - /// The options passed to this command. - /// - public required IReadOnlyList Options { get; init; } - - /// - /// The current argument to convert. - /// - public new DiscordInteractionDataOption? Argument { get; protected set; } - - /// - public override bool NextArgument() - { - // Support for variadic-argument parameters - if (this.VariadicArgumentAttribute is not null && !NextVariadicArgument()) - { - return false; - } - - // Convert the parameter into it's interaction-friendly name - string parameterPolicyName = this.ParameterNamePolicy.GetParameterName(this.Parameter, SlashCommandProcessor.ResolveCulture(this.Interaction), this.VariadicArgumentParameterIndex); - DiscordInteractionDataOption? argument = this.Options.SingleOrDefault(argument => argument.Name == parameterPolicyName); - if (argument is null) - { - return false; - } - - this.Argument = argument; - base.Argument = argument.Value; - return true; - } -} +using System.Collections.Generic; +using System.Linq; +using DSharpPlus.Commands.Converters; +using DSharpPlus.Commands.Processors.SlashCommands.InteractionNamingPolicies; +using DSharpPlus.Entities; + +namespace DSharpPlus.Commands.Processors.SlashCommands; + +/// +/// Represents a context for interaction-based argument converters. +/// +public record InteractionConverterContext : ConverterContext +{ + /// + /// The parameter naming policy to use when mapping parameters to interaction data. + /// + public required IInteractionNamingPolicy ParameterNamePolicy { get; init; } + + /// + /// The underlying interaction. + /// + public required DiscordInteraction Interaction { get; init; } + + /// + /// The options passed to this command. + /// + public required IReadOnlyList Options { get; init; } + + /// + /// The current argument to convert. + /// + public new DiscordInteractionDataOption? Argument { get; protected set; } + + /// + public override bool NextArgument() + { + // Support for variadic-argument parameters + if (this.VariadicArgumentAttribute is not null && !NextVariadicArgument()) + { + return false; + } + + // Convert the parameter into it's interaction-friendly name + string parameterPolicyName = this.ParameterNamePolicy.GetParameterName(this.Parameter, SlashCommandProcessor.ResolveCulture(this.Interaction), this.VariadicArgumentParameterIndex); + DiscordInteractionDataOption? argument = this.Options.SingleOrDefault(argument => argument.Name == parameterPolicyName); + if (argument is null) + { + return false; + } + + this.Argument = argument; + base.Argument = argument.Value; + return true; + } +} diff --git a/DSharpPlus.Commands/Processors/SlashCommands/InteractionNamingPolicies/IInteractionNamingPolicy.cs b/DSharpPlus.Commands/Processors/SlashCommands/InteractionNamingPolicies/IInteractionNamingPolicy.cs index 4888c4da68..eec98c306d 100644 --- a/DSharpPlus.Commands/Processors/SlashCommands/InteractionNamingPolicies/IInteractionNamingPolicy.cs +++ b/DSharpPlus.Commands/Processors/SlashCommands/InteractionNamingPolicies/IInteractionNamingPolicy.cs @@ -1,33 +1,33 @@ -using System; -using System.Collections; -using System.Globalization; -using DSharpPlus.Commands.Trees; - -namespace DSharpPlus.Commands.Processors.SlashCommands.InteractionNamingPolicies; - -/// -/// Represents a policy for naming parameters. This is used to determine the -/// name of the parameter when registering or receiving interaction data. -/// -public interface IInteractionNamingPolicy -{ - /// - /// Transforms the parameter name into the name that should be used for the interaction data. - /// - /// The parameter being transformed. - /// The culture to use for the transformation. - /// - /// If this parameter is part of an , the index of the parameter. - /// The value will be -1 if this parameter is not part of an . - /// - /// The name that should be used for the interaction data. - public string GetParameterName(CommandParameter parameter, CultureInfo culture, int arrayIndex); - - /// - /// Transforms the text into it's new case. - /// - /// The text to transform. - /// The culture to use for the transformation. - /// The transformed text. - public string TransformText(ReadOnlySpan text, CultureInfo culture); -} +using System; +using System.Collections; +using System.Globalization; +using DSharpPlus.Commands.Trees; + +namespace DSharpPlus.Commands.Processors.SlashCommands.InteractionNamingPolicies; + +/// +/// Represents a policy for naming parameters. This is used to determine the +/// name of the parameter when registering or receiving interaction data. +/// +public interface IInteractionNamingPolicy +{ + /// + /// Transforms the parameter name into the name that should be used for the interaction data. + /// + /// The parameter being transformed. + /// The culture to use for the transformation. + /// + /// If this parameter is part of an , the index of the parameter. + /// The value will be -1 if this parameter is not part of an . + /// + /// The name that should be used for the interaction data. + public string GetParameterName(CommandParameter parameter, CultureInfo culture, int arrayIndex); + + /// + /// Transforms the text into it's new case. + /// + /// The text to transform. + /// The culture to use for the transformation. + /// The transformed text. + public string TransformText(ReadOnlySpan text, CultureInfo culture); +} diff --git a/DSharpPlus.Commands/Processors/SlashCommands/InteractionNamingPolicies/KebabCaseNamingPolicy.cs b/DSharpPlus.Commands/Processors/SlashCommands/InteractionNamingPolicies/KebabCaseNamingPolicy.cs index 4382ef8602..9edc879eb7 100644 --- a/DSharpPlus.Commands/Processors/SlashCommands/InteractionNamingPolicies/KebabCaseNamingPolicy.cs +++ b/DSharpPlus.Commands/Processors/SlashCommands/InteractionNamingPolicies/KebabCaseNamingPolicy.cs @@ -1,48 +1,48 @@ -using System; -using System.Buffers; -using System.Globalization; -using System.Text; - -using CommunityToolkit.HighPerformance.Buffers; - -using DSharpPlus.Commands.Trees; - -namespace DSharpPlus.Commands.Processors.SlashCommands.InteractionNamingPolicies; - -/// -/// Transforms parameter names into kebab-case. -/// -public sealed class KebabCaseNamingPolicy : IInteractionNamingPolicy -{ - /// - /// Transforms the parameter name into it's kebab-case equivalent. - /// - /// - public string GetParameterName(CommandParameter parameter, CultureInfo culture, int arrayIndex) - { - if (string.IsNullOrWhiteSpace(parameter.Name)) - { - throw new InvalidOperationException("Parameter name cannot be null or empty."); - } - - StringBuilder stringBuilder = new(TransformText(parameter.Name, culture)); - if (arrayIndex > -1) - { - stringBuilder.Append('-'); - stringBuilder.Append(arrayIndex.ToString(culture)); - } - - return stringBuilder.ToString(); - } - - /// - public string TransformText(ReadOnlySpan text, CultureInfo culture) - { - ArrayPoolBufferWriter writer = new(32); - - CaseImplHelpers.KebabCaseCore(text, writer, culture); - ((IMemoryOwner)writer).Memory.Span.Replace('_', '-'); - - return new string(writer.WrittenSpan); - } -} +using System; +using System.Buffers; +using System.Globalization; +using System.Text; + +using CommunityToolkit.HighPerformance.Buffers; + +using DSharpPlus.Commands.Trees; + +namespace DSharpPlus.Commands.Processors.SlashCommands.InteractionNamingPolicies; + +/// +/// Transforms parameter names into kebab-case. +/// +public sealed class KebabCaseNamingPolicy : IInteractionNamingPolicy +{ + /// + /// Transforms the parameter name into it's kebab-case equivalent. + /// + /// + public string GetParameterName(CommandParameter parameter, CultureInfo culture, int arrayIndex) + { + if (string.IsNullOrWhiteSpace(parameter.Name)) + { + throw new InvalidOperationException("Parameter name cannot be null or empty."); + } + + StringBuilder stringBuilder = new(TransformText(parameter.Name, culture)); + if (arrayIndex > -1) + { + stringBuilder.Append('-'); + stringBuilder.Append(arrayIndex.ToString(culture)); + } + + return stringBuilder.ToString(); + } + + /// + public string TransformText(ReadOnlySpan text, CultureInfo culture) + { + ArrayPoolBufferWriter writer = new(32); + + CaseImplHelpers.KebabCaseCore(text, writer, culture); + ((IMemoryOwner)writer).Memory.Span.Replace('_', '-'); + + return new string(writer.WrittenSpan); + } +} diff --git a/DSharpPlus.Commands/Processors/SlashCommands/InteractionNamingPolicies/LowercaseNamingPolicy.cs b/DSharpPlus.Commands/Processors/SlashCommands/InteractionNamingPolicies/LowercaseNamingPolicy.cs index 75842f514e..5b0f72e051 100644 --- a/DSharpPlus.Commands/Processors/SlashCommands/InteractionNamingPolicies/LowercaseNamingPolicy.cs +++ b/DSharpPlus.Commands/Processors/SlashCommands/InteractionNamingPolicies/LowercaseNamingPolicy.cs @@ -1,47 +1,47 @@ -using System; -using System.Buffers; -using System.Globalization; -using System.Text; - -using CommunityToolkit.HighPerformance.Buffers; - -using DSharpPlus.Commands.Trees; - -namespace DSharpPlus.Commands.Processors.SlashCommands.InteractionNamingPolicies; - -/// -/// Transforms parameter names into lowercase. -/// -public sealed class LowercaseNamingPolicy : IInteractionNamingPolicy -{ - /// - /// Transforms the parameter name into it's lowercase equivalent. - /// - /// - public string GetParameterName(CommandParameter parameter, CultureInfo culture, int arrayIndex) - { - if (string.IsNullOrWhiteSpace(parameter.Name)) - { - throw new InvalidOperationException("Parameter name cannot be null or empty."); - } - - StringBuilder stringBuilder = new(TransformText(parameter.Name, culture)); - if (arrayIndex > -1) - { - stringBuilder.Append(arrayIndex.ToString(culture)); - } - - return stringBuilder.ToString(); - } - - /// - public string TransformText(ReadOnlySpan text, CultureInfo culture) - { - ArrayPoolBufferWriter writer = new(32); - - CaseImplHelpers.LowercaseCore(text, writer, culture); - ((IMemoryOwner)writer).Memory.Span.Replace('-', '_'); - - return new string(writer.WrittenSpan); - } -} +using System; +using System.Buffers; +using System.Globalization; +using System.Text; + +using CommunityToolkit.HighPerformance.Buffers; + +using DSharpPlus.Commands.Trees; + +namespace DSharpPlus.Commands.Processors.SlashCommands.InteractionNamingPolicies; + +/// +/// Transforms parameter names into lowercase. +/// +public sealed class LowercaseNamingPolicy : IInteractionNamingPolicy +{ + /// + /// Transforms the parameter name into it's lowercase equivalent. + /// + /// + public string GetParameterName(CommandParameter parameter, CultureInfo culture, int arrayIndex) + { + if (string.IsNullOrWhiteSpace(parameter.Name)) + { + throw new InvalidOperationException("Parameter name cannot be null or empty."); + } + + StringBuilder stringBuilder = new(TransformText(parameter.Name, culture)); + if (arrayIndex > -1) + { + stringBuilder.Append(arrayIndex.ToString(culture)); + } + + return stringBuilder.ToString(); + } + + /// + public string TransformText(ReadOnlySpan text, CultureInfo culture) + { + ArrayPoolBufferWriter writer = new(32); + + CaseImplHelpers.LowercaseCore(text, writer, culture); + ((IMemoryOwner)writer).Memory.Span.Replace('-', '_'); + + return new string(writer.WrittenSpan); + } +} diff --git a/DSharpPlus.Commands/Processors/SlashCommands/InteractionNamingPolicies/SnakeCaseNamingPolicy.cs b/DSharpPlus.Commands/Processors/SlashCommands/InteractionNamingPolicies/SnakeCaseNamingPolicy.cs index 5adf4dbb1f..757ac19d7c 100644 --- a/DSharpPlus.Commands/Processors/SlashCommands/InteractionNamingPolicies/SnakeCaseNamingPolicy.cs +++ b/DSharpPlus.Commands/Processors/SlashCommands/InteractionNamingPolicies/SnakeCaseNamingPolicy.cs @@ -1,48 +1,48 @@ -using System; -using System.Buffers; -using System.Globalization; -using System.Text; - -using CommunityToolkit.HighPerformance.Buffers; - -using DSharpPlus.Commands.Trees; - -namespace DSharpPlus.Commands.Processors.SlashCommands.InteractionNamingPolicies; - -/// -/// Transforms parameter names into snake_case. -/// -public class SnakeCaseNamingPolicy : IInteractionNamingPolicy -{ - /// - /// Transforms the parameter name into it's snake_case equivalent. - /// - /// - public string GetParameterName(CommandParameter parameter, CultureInfo culture, int arrayIndex) - { - if (string.IsNullOrWhiteSpace(parameter.Name)) - { - throw new InvalidOperationException("Parameter name cannot be null or empty."); - } - - StringBuilder stringBuilder = new(TransformText(parameter.Name, culture)); - if (arrayIndex > -1) - { - stringBuilder.Append('_'); - stringBuilder.Append(arrayIndex); - } - - return stringBuilder.ToString(); - } - - /// - public string TransformText(ReadOnlySpan text, CultureInfo culture) - { - ArrayPoolBufferWriter writer = new(32); - - CaseImplHelpers.SnakeCaseCore(text, writer, culture); - ((IMemoryOwner)writer).Memory.Span.Replace('-', '_'); - - return new string(writer.WrittenSpan); - } -} +using System; +using System.Buffers; +using System.Globalization; +using System.Text; + +using CommunityToolkit.HighPerformance.Buffers; + +using DSharpPlus.Commands.Trees; + +namespace DSharpPlus.Commands.Processors.SlashCommands.InteractionNamingPolicies; + +/// +/// Transforms parameter names into snake_case. +/// +public class SnakeCaseNamingPolicy : IInteractionNamingPolicy +{ + /// + /// Transforms the parameter name into it's snake_case equivalent. + /// + /// + public string GetParameterName(CommandParameter parameter, CultureInfo culture, int arrayIndex) + { + if (string.IsNullOrWhiteSpace(parameter.Name)) + { + throw new InvalidOperationException("Parameter name cannot be null or empty."); + } + + StringBuilder stringBuilder = new(TransformText(parameter.Name, culture)); + if (arrayIndex > -1) + { + stringBuilder.Append('_'); + stringBuilder.Append(arrayIndex); + } + + return stringBuilder.ToString(); + } + + /// + public string TransformText(ReadOnlySpan text, CultureInfo culture) + { + ArrayPoolBufferWriter writer = new(32); + + CaseImplHelpers.SnakeCaseCore(text, writer, culture); + ((IMemoryOwner)writer).Memory.Span.Replace('-', '_'); + + return new string(writer.WrittenSpan); + } +} diff --git a/DSharpPlus.Commands/Processors/SlashCommands/InteractionTypes.cs b/DSharpPlus.Commands/Processors/SlashCommands/InteractionTypes.cs index 72afe72f34..936010c3bb 100644 --- a/DSharpPlus.Commands/Processors/SlashCommands/InteractionTypes.cs +++ b/DSharpPlus.Commands/Processors/SlashCommands/InteractionTypes.cs @@ -1,10 +1,10 @@ -using System; -using DSharpPlus.Entities; - -namespace DSharpPlus.Commands.Processors.SlashCommands; - -[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] -public sealed class SlashCommandTypesAttribute(params DiscordApplicationCommandType[] applicationCommandTypes) : Attribute -{ - public DiscordApplicationCommandType[] ApplicationCommandTypes { get; init; } = applicationCommandTypes; -} +using System; +using DSharpPlus.Entities; + +namespace DSharpPlus.Commands.Processors.SlashCommands; + +[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] +public sealed class SlashCommandTypesAttribute(params DiscordApplicationCommandType[] applicationCommandTypes) : Attribute +{ + public DiscordApplicationCommandType[] ApplicationCommandTypes { get; init; } = applicationCommandTypes; +} diff --git a/DSharpPlus.Commands/Processors/SlashCommands/Localization/DiscordLocale.cs b/DSharpPlus.Commands/Processors/SlashCommands/Localization/DiscordLocale.cs index 91a4026c35..6e0d2e2799 100644 --- a/DSharpPlus.Commands/Processors/SlashCommands/Localization/DiscordLocale.cs +++ b/DSharpPlus.Commands/Processors/SlashCommands/Localization/DiscordLocale.cs @@ -1,39 +1,39 @@ -using System.Diagnostics.CodeAnalysis; - -namespace DSharpPlus.Commands.Processors.SlashCommands.Localization; - -[SuppressMessage("Roslyn", "CA1707", Justification = "Underscores are required to be compliant with Discord's API.")] -public enum DiscordLocale -{ - id, - da, - de, - en_GB, - en_US, - es_ES, - fr, - hr, - it, - lt, - hu, - nl, - no, - pl, - pt_BR, - ro, - fi, - sv_SE, - vi, - tr, - cs, - el, - bg, - ru, - uk, - hi, - th, - zh_CN, - ja, - zh_TW, - ko -} +using System.Diagnostics.CodeAnalysis; + +namespace DSharpPlus.Commands.Processors.SlashCommands.Localization; + +[SuppressMessage("Roslyn", "CA1707", Justification = "Underscores are required to be compliant with Discord's API.")] +public enum DiscordLocale +{ + id, + da, + de, + en_GB, + en_US, + es_ES, + fr, + hr, + it, + lt, + hu, + nl, + no, + pl, + pt_BR, + ro, + fi, + sv_SE, + vi, + tr, + cs, + el, + bg, + ru, + uk, + hi, + th, + zh_CN, + ja, + zh_TW, + ko +} diff --git a/DSharpPlus.Commands/Processors/SlashCommands/Localization/IInteractionLocalizer.cs b/DSharpPlus.Commands/Processors/SlashCommands/Localization/IInteractionLocalizer.cs index 7b80f2c38f..2ff64fbebd 100644 --- a/DSharpPlus.Commands/Processors/SlashCommands/Localization/IInteractionLocalizer.cs +++ b/DSharpPlus.Commands/Processors/SlashCommands/Localization/IInteractionLocalizer.cs @@ -1,9 +1,9 @@ -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace DSharpPlus.Commands.Processors.SlashCommands.Localization; - -public interface IInteractionLocalizer -{ - public ValueTask> TranslateAsync(string fullSymbolName); -} +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace DSharpPlus.Commands.Processors.SlashCommands.Localization; + +public interface IInteractionLocalizer +{ + public ValueTask> TranslateAsync(string fullSymbolName); +} diff --git a/DSharpPlus.Commands/Processors/SlashCommands/Localization/InteractionLocalizerAttribute.cs b/DSharpPlus.Commands/Processors/SlashCommands/Localization/InteractionLocalizerAttribute.cs index 248dfba6fe..09784cd5f2 100644 --- a/DSharpPlus.Commands/Processors/SlashCommands/Localization/InteractionLocalizerAttribute.cs +++ b/DSharpPlus.Commands/Processors/SlashCommands/Localization/InteractionLocalizerAttribute.cs @@ -1,15 +1,15 @@ -using System; - -namespace DSharpPlus.Commands.Processors.SlashCommands.Localization; - -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Parameter, Inherited = false, AllowMultiple = false)] -public class InteractionLocalizerAttribute(Type localizerType) : Attribute -{ - public Type LocalizerType { get; init; } = localizerType ?? throw new ArgumentNullException(nameof(localizerType)); -} - -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Parameter, Inherited = false, AllowMultiple = false)] -public sealed class InteractionLocalizerAttribute : InteractionLocalizerAttribute where T : IInteractionLocalizer -{ - public InteractionLocalizerAttribute() : base(typeof(T)) { } -} +using System; + +namespace DSharpPlus.Commands.Processors.SlashCommands.Localization; + +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Parameter, Inherited = false, AllowMultiple = false)] +public class InteractionLocalizerAttribute(Type localizerType) : Attribute +{ + public Type LocalizerType { get; init; } = localizerType ?? throw new ArgumentNullException(nameof(localizerType)); +} + +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Parameter, Inherited = false, AllowMultiple = false)] +public sealed class InteractionLocalizerAttribute : InteractionLocalizerAttribute where T : IInteractionLocalizer +{ + public InteractionLocalizerAttribute() : base(typeof(T)) { } +} diff --git a/DSharpPlus.Commands/Processors/SlashCommands/Localization/LocalesHelper.cs b/DSharpPlus.Commands/Processors/SlashCommands/Localization/LocalesHelper.cs index f7c1a8ce6d..e33dbdc887 100644 --- a/DSharpPlus.Commands/Processors/SlashCommands/Localization/LocalesHelper.cs +++ b/DSharpPlus.Commands/Processors/SlashCommands/Localization/LocalesHelper.cs @@ -1,94 +1,94 @@ -using System.Collections.Frozen; -using System.Collections.Generic; -using System.Linq; - -namespace DSharpPlus.Commands.Processors.SlashCommands.Localization; - -public static class LocalesHelper -{ - public static IReadOnlyDictionary EnglishToLocale { get; } - public static IReadOnlyDictionary NativeToLocale { get; } - public static IReadOnlyDictionary LocaleToEnglish { get; } - public static IReadOnlyDictionary LocaleToNative { get; } - - static LocalesHelper() - { - Dictionary englishToLocale = new() - { - ["Indonesian"] = DiscordLocale.id, - ["Danish"] = DiscordLocale.da, - ["German"] = DiscordLocale.de, - ["English, UK"] = DiscordLocale.en_GB, - ["English, US"] = DiscordLocale.en_US, - ["Spanish"] = DiscordLocale.es_ES, - ["French"] = DiscordLocale.fr, - ["Croatian"] = DiscordLocale.hr, - ["Italian"] = DiscordLocale.it, - ["Lithuanian"] = DiscordLocale.lt, - ["Hungarian"] = DiscordLocale.hu, - ["Dutch"] = DiscordLocale.nl, - ["Norwegian"] = DiscordLocale.no, - ["Polish"] = DiscordLocale.pl, - ["Portuguese"] = DiscordLocale.pt_BR, - ["Romanian"] = DiscordLocale.ro, - ["Finnish"] = DiscordLocale.fi, - ["Swedish"] = DiscordLocale.sv_SE, - ["Vietnamese"] = DiscordLocale.vi, - ["Turkish"] = DiscordLocale.tr, - ["Czech"] = DiscordLocale.cs, - ["Greek"] = DiscordLocale.el, - ["Bulgarian"] = DiscordLocale.bg, - ["Russian"] = DiscordLocale.ru, - ["Ukrainian"] = DiscordLocale.uk, - ["Hindi"] = DiscordLocale.hi, - ["Thai"] = DiscordLocale.th, - ["Chinese, China"] = DiscordLocale.zh_CN, - ["Japanese"] = DiscordLocale.ja, - ["Chinese"] = DiscordLocale.zh_TW, - ["Korean"] = DiscordLocale.ko, - }; - - Dictionary nativeToLocale = new() - { - ["Bahasa Indonesia"] = DiscordLocale.id, - ["Dansk"] = DiscordLocale.da, - ["Deutsch"] = DiscordLocale.de, - ["English, UK"] = DiscordLocale.en_GB, - ["English, US"] = DiscordLocale.en_US, - ["Español"] = DiscordLocale.es_ES, - ["Français"] = DiscordLocale.fr, - ["Hrvatski"] = DiscordLocale.hr, - ["Italiano"] = DiscordLocale.it, - ["Lietuviškai"] = DiscordLocale.lt, - ["Magyar"] = DiscordLocale.hu, - ["Nederlands"] = DiscordLocale.nl, - ["Norsk"] = DiscordLocale.no, - ["Polski"] = DiscordLocale.pl, - ["Português do Brasil"] = DiscordLocale.pt_BR, - ["Română"] = DiscordLocale.ro, - ["Suomi"] = DiscordLocale.fi, - ["Svenska"] = DiscordLocale.sv_SE, - ["Tiếng Việt"] = DiscordLocale.vi, - ["Türkçe"] = DiscordLocale.tr, - ["Čeština"] = DiscordLocale.cs, - ["Ελληνικά"] = DiscordLocale.el, - ["български"] = DiscordLocale.bg, - ["Pусский"] = DiscordLocale.ru, - ["Українська"] = DiscordLocale.uk, - ["हिन्दी"] = DiscordLocale.hi, - ["ไทย"] = DiscordLocale.th, - ["中文"] = DiscordLocale.zh_CN, - ["日本語"] = DiscordLocale.ja, - ["繁體中文"] = DiscordLocale.zh_TW, - ["한국어"] = DiscordLocale.ko, - }; - - Dictionary localeToEnglish = englishToLocale.ToDictionary(x => x.Value, x => x.Key); - Dictionary localeToNative = nativeToLocale.ToDictionary(x => x.Value, x => x.Key); - - EnglishToLocale = englishToLocale.ToFrozenDictionary(); - NativeToLocale = nativeToLocale.ToFrozenDictionary(); - LocaleToEnglish = localeToEnglish.ToFrozenDictionary(); - LocaleToNative = localeToNative.ToFrozenDictionary(); - } -} +using System.Collections.Frozen; +using System.Collections.Generic; +using System.Linq; + +namespace DSharpPlus.Commands.Processors.SlashCommands.Localization; + +public static class LocalesHelper +{ + public static IReadOnlyDictionary EnglishToLocale { get; } + public static IReadOnlyDictionary NativeToLocale { get; } + public static IReadOnlyDictionary LocaleToEnglish { get; } + public static IReadOnlyDictionary LocaleToNative { get; } + + static LocalesHelper() + { + Dictionary englishToLocale = new() + { + ["Indonesian"] = DiscordLocale.id, + ["Danish"] = DiscordLocale.da, + ["German"] = DiscordLocale.de, + ["English, UK"] = DiscordLocale.en_GB, + ["English, US"] = DiscordLocale.en_US, + ["Spanish"] = DiscordLocale.es_ES, + ["French"] = DiscordLocale.fr, + ["Croatian"] = DiscordLocale.hr, + ["Italian"] = DiscordLocale.it, + ["Lithuanian"] = DiscordLocale.lt, + ["Hungarian"] = DiscordLocale.hu, + ["Dutch"] = DiscordLocale.nl, + ["Norwegian"] = DiscordLocale.no, + ["Polish"] = DiscordLocale.pl, + ["Portuguese"] = DiscordLocale.pt_BR, + ["Romanian"] = DiscordLocale.ro, + ["Finnish"] = DiscordLocale.fi, + ["Swedish"] = DiscordLocale.sv_SE, + ["Vietnamese"] = DiscordLocale.vi, + ["Turkish"] = DiscordLocale.tr, + ["Czech"] = DiscordLocale.cs, + ["Greek"] = DiscordLocale.el, + ["Bulgarian"] = DiscordLocale.bg, + ["Russian"] = DiscordLocale.ru, + ["Ukrainian"] = DiscordLocale.uk, + ["Hindi"] = DiscordLocale.hi, + ["Thai"] = DiscordLocale.th, + ["Chinese, China"] = DiscordLocale.zh_CN, + ["Japanese"] = DiscordLocale.ja, + ["Chinese"] = DiscordLocale.zh_TW, + ["Korean"] = DiscordLocale.ko, + }; + + Dictionary nativeToLocale = new() + { + ["Bahasa Indonesia"] = DiscordLocale.id, + ["Dansk"] = DiscordLocale.da, + ["Deutsch"] = DiscordLocale.de, + ["English, UK"] = DiscordLocale.en_GB, + ["English, US"] = DiscordLocale.en_US, + ["Español"] = DiscordLocale.es_ES, + ["Français"] = DiscordLocale.fr, + ["Hrvatski"] = DiscordLocale.hr, + ["Italiano"] = DiscordLocale.it, + ["Lietuviškai"] = DiscordLocale.lt, + ["Magyar"] = DiscordLocale.hu, + ["Nederlands"] = DiscordLocale.nl, + ["Norsk"] = DiscordLocale.no, + ["Polski"] = DiscordLocale.pl, + ["Português do Brasil"] = DiscordLocale.pt_BR, + ["Română"] = DiscordLocale.ro, + ["Suomi"] = DiscordLocale.fi, + ["Svenska"] = DiscordLocale.sv_SE, + ["Tiếng Việt"] = DiscordLocale.vi, + ["Türkçe"] = DiscordLocale.tr, + ["Čeština"] = DiscordLocale.cs, + ["Ελληνικά"] = DiscordLocale.el, + ["български"] = DiscordLocale.bg, + ["Pусский"] = DiscordLocale.ru, + ["Українська"] = DiscordLocale.uk, + ["हिन्दी"] = DiscordLocale.hi, + ["ไทย"] = DiscordLocale.th, + ["中文"] = DiscordLocale.zh_CN, + ["日本語"] = DiscordLocale.ja, + ["繁體中文"] = DiscordLocale.zh_TW, + ["한국어"] = DiscordLocale.ko, + }; + + Dictionary localeToEnglish = englishToLocale.ToDictionary(x => x.Value, x => x.Key); + Dictionary localeToNative = nativeToLocale.ToDictionary(x => x.Value, x => x.Key); + + EnglishToLocale = englishToLocale.ToFrozenDictionary(); + NativeToLocale = nativeToLocale.ToFrozenDictionary(); + LocaleToEnglish = localeToEnglish.ToFrozenDictionary(); + LocaleToNative = localeToNative.ToFrozenDictionary(); + } +} diff --git a/DSharpPlus.Commands/Processors/SlashCommands/Metadata/InteractionAllowedContextsAttribute.cs b/DSharpPlus.Commands/Processors/SlashCommands/Metadata/InteractionAllowedContextsAttribute.cs index 7fd1b13cb8..9b5e27da37 100644 --- a/DSharpPlus.Commands/Processors/SlashCommands/Metadata/InteractionAllowedContextsAttribute.cs +++ b/DSharpPlus.Commands/Processors/SlashCommands/Metadata/InteractionAllowedContextsAttribute.cs @@ -1,17 +1,17 @@ -using System; -using System.Collections.Generic; -using DSharpPlus.Entities; - -namespace DSharpPlus.Commands.Processors.SlashCommands.Metadata; - -/// -/// Specifies the allowed interaction contexts for a command. -/// -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)] -public sealed class InteractionAllowedContextsAttribute(params DiscordInteractionContextType[] allowedContexts) : Attribute -{ - /// - /// The contexts the command is allowed to be used in. - /// - public IReadOnlyList AllowedContexts { get; } = allowedContexts; -} +using System; +using System.Collections.Generic; +using DSharpPlus.Entities; + +namespace DSharpPlus.Commands.Processors.SlashCommands.Metadata; + +/// +/// Specifies the allowed interaction contexts for a command. +/// +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)] +public sealed class InteractionAllowedContextsAttribute(params DiscordInteractionContextType[] allowedContexts) : Attribute +{ + /// + /// The contexts the command is allowed to be used in. + /// + public IReadOnlyList AllowedContexts { get; } = allowedContexts; +} diff --git a/DSharpPlus.Commands/Processors/SlashCommands/Metadata/InteractionInstallTypeAttribute.cs b/DSharpPlus.Commands/Processors/SlashCommands/Metadata/InteractionInstallTypeAttribute.cs index 49e2313aa0..c596ab02f8 100644 --- a/DSharpPlus.Commands/Processors/SlashCommands/Metadata/InteractionInstallTypeAttribute.cs +++ b/DSharpPlus.Commands/Processors/SlashCommands/Metadata/InteractionInstallTypeAttribute.cs @@ -1,17 +1,17 @@ -using System; -using System.Collections.Generic; -using DSharpPlus.Entities; - -namespace DSharpPlus.Commands.Processors.SlashCommands.Metadata; - -/// -/// Specifies the installation context for a command or module. -/// -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)] -public class InteractionInstallTypeAttribute(params DiscordApplicationIntegrationType[] installTypes) : Attribute -{ - /// - /// The contexts the command is allowed to be installed to. - /// - public IReadOnlyList InstallTypes { get; } = installTypes; -} +using System; +using System.Collections.Generic; +using DSharpPlus.Entities; + +namespace DSharpPlus.Commands.Processors.SlashCommands.Metadata; + +/// +/// Specifies the installation context for a command or module. +/// +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)] +public class InteractionInstallTypeAttribute(params DiscordApplicationIntegrationType[] installTypes) : Attribute +{ + /// + /// The contexts the command is allowed to be installed to. + /// + public IReadOnlyList InstallTypes { get; } = installTypes; +} diff --git a/DSharpPlus.Commands/Processors/SlashCommands/SlashCommandConfiguration.cs b/DSharpPlus.Commands/Processors/SlashCommands/SlashCommandConfiguration.cs index 64d5eecf98..12ee6012d9 100644 --- a/DSharpPlus.Commands/Processors/SlashCommands/SlashCommandConfiguration.cs +++ b/DSharpPlus.Commands/Processors/SlashCommands/SlashCommandConfiguration.cs @@ -1,46 +1,46 @@ -using DSharpPlus.Commands.Processors.SlashCommands.InteractionNamingPolicies; -using DSharpPlus.Commands.Processors.SlashCommands.RemoteRecordRetentionPolicies; - -namespace DSharpPlus.Commands.Processors.SlashCommands; - -/// -/// The configuration for the . -/// -public sealed class SlashCommandConfiguration -{ - /// - /// Whether to register in their - /// application command form and map them back to their original commands. - /// - /// - /// Set this to if you want to manually register - /// commands or obtain your application commands from a different source. - /// - public bool RegisterCommands { get; init; } = true; - - /// - /// How to name parameters when registering or receiving interaction data. - /// - public IInteractionNamingPolicy NamingPolicy { get; init; } = new SnakeCaseNamingPolicy(); - - /// - /// Instructs DSharpPlus to always overwrite the command records Discord has of our bot on startup. - /// - /// - /// This skips the startup procedure of fetching commands and overwriting only if additions are detected. While - /// this may save time on startup, it also makes the library less resistant to unrecognized command types or - /// structures it cannot correctly handle.
- /// Currently, removals are not considered a reason to overwrite by default so as to work around an issue - /// where certain commands will cause bulk overwrites to fail. - ///
- public bool UnconditionallyOverwriteCommands { get; init; } = false; - - /// - /// Controls when DSharpPlus deletes an application command that does not have a local equivalent. - /// - /// - /// By default, this will delete all application commands except for activity entrypoints. - /// - public IRemoteRecordRetentionPolicy RemoteRecordRetentionPolicy { get; init; } - = new DefaultRemoteRecordRetentionPolicy(); -} +using DSharpPlus.Commands.Processors.SlashCommands.InteractionNamingPolicies; +using DSharpPlus.Commands.Processors.SlashCommands.RemoteRecordRetentionPolicies; + +namespace DSharpPlus.Commands.Processors.SlashCommands; + +/// +/// The configuration for the . +/// +public sealed class SlashCommandConfiguration +{ + /// + /// Whether to register in their + /// application command form and map them back to their original commands. + /// + /// + /// Set this to if you want to manually register + /// commands or obtain your application commands from a different source. + /// + public bool RegisterCommands { get; init; } = true; + + /// + /// How to name parameters when registering or receiving interaction data. + /// + public IInteractionNamingPolicy NamingPolicy { get; init; } = new SnakeCaseNamingPolicy(); + + /// + /// Instructs DSharpPlus to always overwrite the command records Discord has of our bot on startup. + /// + /// + /// This skips the startup procedure of fetching commands and overwriting only if additions are detected. While + /// this may save time on startup, it also makes the library less resistant to unrecognized command types or + /// structures it cannot correctly handle.
+ /// Currently, removals are not considered a reason to overwrite by default so as to work around an issue + /// where certain commands will cause bulk overwrites to fail. + ///
+ public bool UnconditionallyOverwriteCommands { get; init; } = false; + + /// + /// Controls when DSharpPlus deletes an application command that does not have a local equivalent. + /// + /// + /// By default, this will delete all application commands except for activity entrypoints. + /// + public IRemoteRecordRetentionPolicy RemoteRecordRetentionPolicy { get; init; } + = new DefaultRemoteRecordRetentionPolicy(); +} diff --git a/DSharpPlus.Commands/Processors/SlashCommands/SlashCommandContext.cs b/DSharpPlus.Commands/Processors/SlashCommands/SlashCommandContext.cs index 1fc3c1f6c0..1f1d999a59 100644 --- a/DSharpPlus.Commands/Processors/SlashCommands/SlashCommandContext.cs +++ b/DSharpPlus.Commands/Processors/SlashCommands/SlashCommandContext.cs @@ -1,161 +1,161 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using DSharpPlus.Entities; - -namespace DSharpPlus.Commands.Processors.SlashCommands; - -/// -/// Represents a base context for slash command contexts. -/// -public record SlashCommandContext : CommandContext -{ - public required DiscordInteraction Interaction { get; init; } - public required IReadOnlyList Options { get; init; } - - /// - /// Content to send in the response. - /// Specifies whether this response should be ephemeral. - public virtual ValueTask RespondAsync(string content, bool ephemeral) => RespondAsync(new DiscordInteractionResponseBuilder().WithContent(content).AsEphemeral(ephemeral)); - - /// - /// Embed to send in the response. - /// Specifies whether this response should be ephemeral. - public virtual ValueTask RespondAsync(DiscordEmbed embed, bool ephemeral) => RespondAsync(new DiscordInteractionResponseBuilder().AddEmbed(embed).AsEphemeral(ephemeral)); - - /// - /// Content to send in the response. - /// Embed to send in the response. - /// Specifies whether this response should be ephemeral. - public virtual ValueTask RespondAsync(string content, DiscordEmbed embed, bool ephemeral) => RespondAsync(new DiscordInteractionResponseBuilder() - .WithContent(content) - .AddEmbed(embed) - .AsEphemeral(ephemeral)); - - /// - public override async ValueTask RespondAsync(IDiscordMessageBuilder builder) - { - if (this.Interaction.ResponseState is DiscordInteractionResponseState.Replied) - { - throw new InvalidOperationException("Cannot respond to an interaction twice. Please use FollowupAsync instead."); - } - - DiscordInteractionResponseBuilder interactionBuilder = builder as DiscordInteractionResponseBuilder ?? new(builder); - - // Don't ping anyone if no mentions are explicitly set - if (interactionBuilder.Mentions.Count is 0) - { - interactionBuilder.AddMentions(Mentions.None); - } - - if (this.Interaction.ResponseState is DiscordInteractionResponseState.Unacknowledged) - { - await this.Interaction.CreateResponseAsync(DiscordInteractionResponseType.ChannelMessageWithSource, interactionBuilder); - } - else if (this.Interaction.ResponseState is DiscordInteractionResponseState.Deferred) - { - await this.Interaction.EditOriginalResponseAsync(new DiscordWebhookBuilder(interactionBuilder)); - } - } - - /// - /// Respond to the command with a Modal. - /// - /// Builder which is used to build the modal. - /// Thrown when the interaction response state is not - /// Thrown when the response builder is not valid - public async ValueTask RespondWithModalAsync(DiscordInteractionResponseBuilder builder) - { - if (this.Interaction.ResponseState is not DiscordInteractionResponseState.Unacknowledged) - { - throw new InvalidOperationException("Modal must be the first response to the interaction."); - } - else if (string.IsNullOrWhiteSpace(builder.CustomId)) - { - throw new ArgumentException("Modal response has to have a custom id"); - } - else if (builder.ComponentActionRows?.Any(x => x.Components.Any(y => y is not DiscordTextInputComponent)) ?? false) - { - throw new ArgumentException("Modals currently only support TextInputComponents"); - } - - await this.Interaction.CreateResponseAsync(DiscordInteractionResponseType.Modal, builder); - } - - /// - public override ValueTask DeferResponseAsync() => DeferResponseAsync(false); - - /// - /// Specifies whether this response should be ephemeral. - public async ValueTask DeferResponseAsync(bool ephemeral) => await this.Interaction.DeferAsync(ephemeral); - - /// - public override async ValueTask EditResponseAsync(IDiscordMessageBuilder builder) => - await this.Interaction.EditOriginalResponseAsync(builder as DiscordWebhookBuilder ?? new(builder)); - - /// - public override async ValueTask DeleteResponseAsync() => - await this.Interaction.DeleteOriginalResponseAsync(); - - /// - public override async ValueTask GetResponseAsync() => - await this.Interaction.GetOriginalResponseAsync(); - - /// - /// Content to send in the response. - /// Specifies whether this response should be ephemeral. - public virtual ValueTask FollowupAsync(string content, bool ephemeral) => - FollowupAsync(new DiscordFollowupMessageBuilder().WithContent(content).AsEphemeral(ephemeral)); - - /// - /// Embed to send in the response. - /// Specifies whether this response should be ephemeral. - public virtual ValueTask FollowupAsync(DiscordEmbed embed, bool ephemeral) => - FollowupAsync(new DiscordFollowupMessageBuilder().AddEmbed(embed).AsEphemeral(ephemeral)); - - /// - /// Content to send in the response. - /// Embed to send in the response. - /// Specifies whether this response should be ephemeral. - public virtual ValueTask FollowupAsync(string content, DiscordEmbed embed, bool ephemeral) => FollowupAsync(new DiscordFollowupMessageBuilder() - .WithContent(content) - .AddEmbed(embed) - .AsEphemeral(ephemeral)); - - /// - public override async ValueTask FollowupAsync(IDiscordMessageBuilder builder) - { - DiscordFollowupMessageBuilder followupBuilder = builder is DiscordFollowupMessageBuilder messageBuilder - ? messageBuilder - : new DiscordFollowupMessageBuilder(builder); - - DiscordMessage message = await this.Interaction.CreateFollowupMessageAsync(followupBuilder); - this.followupMessages.Add(message.Id, message); - return message; - } - - /// - public override async ValueTask EditFollowupAsync(ulong messageId, IDiscordMessageBuilder builder) - { - DiscordWebhookBuilder editedBuilder = builder as DiscordWebhookBuilder ?? new DiscordWebhookBuilder(builder); - this.followupMessages[messageId] = await this.Interaction.EditFollowupMessageAsync(messageId, editedBuilder); - return this.followupMessages[messageId]; - } - - /// - public override async ValueTask GetFollowupAsync(ulong messageId, bool ignoreCache = false) - { - // Fetch the follow up message if we don't have it cached. - if (ignoreCache || !this.followupMessages.TryGetValue(messageId, out DiscordMessage? message)) - { - message = await this.Interaction.GetFollowupMessageAsync(messageId); - this.followupMessages[messageId] = message; - } - - return message; - } - - /// - public override async ValueTask DeleteFollowupAsync(ulong messageId) => await this.Interaction.DeleteFollowupMessageAsync(messageId); -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using DSharpPlus.Entities; + +namespace DSharpPlus.Commands.Processors.SlashCommands; + +/// +/// Represents a base context for slash command contexts. +/// +public record SlashCommandContext : CommandContext +{ + public required DiscordInteraction Interaction { get; init; } + public required IReadOnlyList Options { get; init; } + + /// + /// Content to send in the response. + /// Specifies whether this response should be ephemeral. + public virtual ValueTask RespondAsync(string content, bool ephemeral) => RespondAsync(new DiscordInteractionResponseBuilder().WithContent(content).AsEphemeral(ephemeral)); + + /// + /// Embed to send in the response. + /// Specifies whether this response should be ephemeral. + public virtual ValueTask RespondAsync(DiscordEmbed embed, bool ephemeral) => RespondAsync(new DiscordInteractionResponseBuilder().AddEmbed(embed).AsEphemeral(ephemeral)); + + /// + /// Content to send in the response. + /// Embed to send in the response. + /// Specifies whether this response should be ephemeral. + public virtual ValueTask RespondAsync(string content, DiscordEmbed embed, bool ephemeral) => RespondAsync(new DiscordInteractionResponseBuilder() + .WithContent(content) + .AddEmbed(embed) + .AsEphemeral(ephemeral)); + + /// + public override async ValueTask RespondAsync(IDiscordMessageBuilder builder) + { + if (this.Interaction.ResponseState is DiscordInteractionResponseState.Replied) + { + throw new InvalidOperationException("Cannot respond to an interaction twice. Please use FollowupAsync instead."); + } + + DiscordInteractionResponseBuilder interactionBuilder = builder as DiscordInteractionResponseBuilder ?? new(builder); + + // Don't ping anyone if no mentions are explicitly set + if (interactionBuilder.Mentions.Count is 0) + { + interactionBuilder.AddMentions(Mentions.None); + } + + if (this.Interaction.ResponseState is DiscordInteractionResponseState.Unacknowledged) + { + await this.Interaction.CreateResponseAsync(DiscordInteractionResponseType.ChannelMessageWithSource, interactionBuilder); + } + else if (this.Interaction.ResponseState is DiscordInteractionResponseState.Deferred) + { + await this.Interaction.EditOriginalResponseAsync(new DiscordWebhookBuilder(interactionBuilder)); + } + } + + /// + /// Respond to the command with a Modal. + /// + /// Builder which is used to build the modal. + /// Thrown when the interaction response state is not + /// Thrown when the response builder is not valid + public async ValueTask RespondWithModalAsync(DiscordInteractionResponseBuilder builder) + { + if (this.Interaction.ResponseState is not DiscordInteractionResponseState.Unacknowledged) + { + throw new InvalidOperationException("Modal must be the first response to the interaction."); + } + else if (string.IsNullOrWhiteSpace(builder.CustomId)) + { + throw new ArgumentException("Modal response has to have a custom id"); + } + else if (builder.ComponentActionRows?.Any(x => x.Components.Any(y => y is not DiscordTextInputComponent)) ?? false) + { + throw new ArgumentException("Modals currently only support TextInputComponents"); + } + + await this.Interaction.CreateResponseAsync(DiscordInteractionResponseType.Modal, builder); + } + + /// + public override ValueTask DeferResponseAsync() => DeferResponseAsync(false); + + /// + /// Specifies whether this response should be ephemeral. + public async ValueTask DeferResponseAsync(bool ephemeral) => await this.Interaction.DeferAsync(ephemeral); + + /// + public override async ValueTask EditResponseAsync(IDiscordMessageBuilder builder) => + await this.Interaction.EditOriginalResponseAsync(builder as DiscordWebhookBuilder ?? new(builder)); + + /// + public override async ValueTask DeleteResponseAsync() => + await this.Interaction.DeleteOriginalResponseAsync(); + + /// + public override async ValueTask GetResponseAsync() => + await this.Interaction.GetOriginalResponseAsync(); + + /// + /// Content to send in the response. + /// Specifies whether this response should be ephemeral. + public virtual ValueTask FollowupAsync(string content, bool ephemeral) => + FollowupAsync(new DiscordFollowupMessageBuilder().WithContent(content).AsEphemeral(ephemeral)); + + /// + /// Embed to send in the response. + /// Specifies whether this response should be ephemeral. + public virtual ValueTask FollowupAsync(DiscordEmbed embed, bool ephemeral) => + FollowupAsync(new DiscordFollowupMessageBuilder().AddEmbed(embed).AsEphemeral(ephemeral)); + + /// + /// Content to send in the response. + /// Embed to send in the response. + /// Specifies whether this response should be ephemeral. + public virtual ValueTask FollowupAsync(string content, DiscordEmbed embed, bool ephemeral) => FollowupAsync(new DiscordFollowupMessageBuilder() + .WithContent(content) + .AddEmbed(embed) + .AsEphemeral(ephemeral)); + + /// + public override async ValueTask FollowupAsync(IDiscordMessageBuilder builder) + { + DiscordFollowupMessageBuilder followupBuilder = builder is DiscordFollowupMessageBuilder messageBuilder + ? messageBuilder + : new DiscordFollowupMessageBuilder(builder); + + DiscordMessage message = await this.Interaction.CreateFollowupMessageAsync(followupBuilder); + this.followupMessages.Add(message.Id, message); + return message; + } + + /// + public override async ValueTask EditFollowupAsync(ulong messageId, IDiscordMessageBuilder builder) + { + DiscordWebhookBuilder editedBuilder = builder as DiscordWebhookBuilder ?? new DiscordWebhookBuilder(builder); + this.followupMessages[messageId] = await this.Interaction.EditFollowupMessageAsync(messageId, editedBuilder); + return this.followupMessages[messageId]; + } + + /// + public override async ValueTask GetFollowupAsync(ulong messageId, bool ignoreCache = false) + { + // Fetch the follow up message if we don't have it cached. + if (ignoreCache || !this.followupMessages.TryGetValue(messageId, out DiscordMessage? message)) + { + message = await this.Interaction.GetFollowupMessageAsync(messageId); + this.followupMessages[messageId] = message; + } + + return message; + } + + /// + public override async ValueTask DeleteFollowupAsync(ulong messageId) => await this.Interaction.DeleteFollowupMessageAsync(messageId); +} diff --git a/DSharpPlus.Commands/Processors/SlashCommands/SlashCommandProcessor.Registration.cs b/DSharpPlus.Commands/Processors/SlashCommands/SlashCommandProcessor.Registration.cs index 0a87b79e87..2494333b99 100644 --- a/DSharpPlus.Commands/Processors/SlashCommands/SlashCommandProcessor.Registration.cs +++ b/DSharpPlus.Commands/Processors/SlashCommands/SlashCommandProcessor.Registration.cs @@ -1,711 +1,711 @@ -using System; -using System.Collections.Frozen; -using System.Collections.Generic; -using System.ComponentModel; -using System.Globalization; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading.Tasks; - -using DSharpPlus.Commands.ArgumentModifiers; -using DSharpPlus.Commands.ContextChecks; -using DSharpPlus.Commands.Converters; -using DSharpPlus.Commands.EventArgs; -using DSharpPlus.Commands.Processors.SlashCommands.ArgumentModifiers; -using DSharpPlus.Commands.Processors.SlashCommands.Localization; -using DSharpPlus.Commands.Processors.SlashCommands.Metadata; -using DSharpPlus.Commands.Trees; -using DSharpPlus.Commands.Trees.Metadata; -using DSharpPlus.Entities; - -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; - -namespace DSharpPlus.Commands.Processors.SlashCommands; - -public sealed partial class SlashCommandProcessor : BaseCommandProcessor -{ - [GeneratedRegex(@"^[-_\p{L}\p{N}\p{IsDevanagari}\p{IsThai}]{1,32}$")] - private static partial Regex NameLocalizationRegex(); - - private static FrozenDictionary applicationCommandMapping = FrozenDictionary.Empty; - private static readonly List applicationCommands = []; - - // if registration failed, this is set to true and will trigger better error messages - private bool registrationFailed = false; - - /// - /// The mapping of application command ids to objects. - /// - public IReadOnlyDictionary ApplicationCommandMapping => applicationCommandMapping; - - public void AddApplicationCommands(params DiscordApplicationCommand[] commands) => applicationCommands.AddRange(commands); - public void AddApplicationCommands(IEnumerable commands) => applicationCommands.AddRange(commands); - - /// - /// Registers as application commands. - /// This will registers regardless of 's value. - /// - /// The extension to read the commands from. - public async ValueTask RegisterSlashCommandsAsync(CommandsExtension extension) - { - if (this.isApplicationCommandsRegistered) - { - return; - } - - this.isApplicationCommandsRegistered = true; - - IReadOnlyList processorSpecificCommands = extension.GetCommandsForProcessor(this); - List globalApplicationCommands = []; - Dictionary> guildsApplicationCommands = []; - globalApplicationCommands.AddRange(applicationCommands); - - try - { - - foreach (Command command in processorSpecificCommands) - { - // If there is a SlashCommandTypesAttribute, check if it contains SlashCommandTypes.ApplicationCommand - // If there isn't, default to SlashCommands - if (command.Attributes.OfType().FirstOrDefault() is SlashCommandTypesAttribute slashCommandTypesAttribute - && !slashCommandTypesAttribute.ApplicationCommandTypes.Contains(DiscordApplicationCommandType.SlashCommand) - ) - { - continue; - } - - DiscordApplicationCommand applicationCommand = await ToApplicationCommandAsync(command); - if (command.GuildIds.Count == 0) - { - globalApplicationCommands.Add(applicationCommand); - continue; - } - - foreach (ulong guildId in command.GuildIds) - { - if (!guildsApplicationCommands.TryGetValue(guildId, out List? guildCommands)) - { - guildCommands = []; - guildsApplicationCommands.Add(guildId, guildCommands); - } - - guildCommands.Add(applicationCommand); - } - } - } - catch (Exception e) - { - this.logger.LogError(e, "Could not build valid application commands, cancelling application command registration."); - this.registrationFailed = true; - return; - } - - // we figured our structure out, fetch discord's records of the commands and match basic criteria - // skip if we are instructed to disable this behaviour - - List discordCommands = []; - - if (this.Configuration.UnconditionallyOverwriteCommands) - { - discordCommands.AddRange - ( - this.extension.DebugGuildId == 0 - ? await this.extension.Client.BulkOverwriteGlobalApplicationCommandsAsync(globalApplicationCommands) - : await this.extension.Client.BulkOverwriteGuildApplicationCommandsAsync - ( - this.extension.DebugGuildId, - globalApplicationCommands - ) - ); - } - else - { - IReadOnlyList preexisting = this.extension.DebugGuildId == 0 - ? await this.extension.Client.GetGlobalApplicationCommandsAsync(true) - : await this.extension.Client.GetGuildApplicationCommandsAsync(this.extension.DebugGuildId, true); - - discordCommands.AddRange(await VerifyAndUpdateRemoteCommandsAsync(globalApplicationCommands, preexisting)); - } - - // for the time being, we still overwrite guilds by force - foreach (KeyValuePair> kv in guildsApplicationCommands) - { - discordCommands.AddRange - ( - await this.extension.Client.BulkOverwriteGuildApplicationCommandsAsync(kv.Key, kv.Value) - ); - } - - applicationCommandMapping = MapApplicationCommands(discordCommands).ToFrozenDictionary(); - - SlashLogging.registeredCommands( - this.logger, - applicationCommandMapping.Count, - applicationCommandMapping.Values.SelectMany(command => command.Flatten()).Count(), - null - ); - } - - /// - /// Matches the application commands to the commands in the command tree. - /// - /// The application commands obtained from Discord. Accepts both global and guild commands. - /// A dictionary mapping the application command id to the command in the command tree. - public IReadOnlyDictionary MapApplicationCommands(IReadOnlyList applicationCommands) - { - if (this.extension is null) - { - throw new InvalidOperationException("SlashCommandProcessor has not been configured."); - } - - Dictionary commandsDictionary = []; - IReadOnlyList processorSpecificCommands = this.extension!.GetCommandsForProcessor(this); - IReadOnlyList flattenCommands = processorSpecificCommands.SelectMany(x => x.Flatten()).ToList(); - - foreach (DiscordApplicationCommand discordCommand in applicationCommands) - { - bool commandFound = false; - string discordCommandName; - if (discordCommand.Type is DiscordApplicationCommandType.MessageContextMenu or DiscordApplicationCommandType.UserContextMenu) - { - discordCommandName = discordCommand.Name; - foreach (Command command in flattenCommands) - { - string commandName = command.Attributes.OfType().FirstOrDefault()?.DisplayName ?? command.FullName; - if (commandName == discordCommand.Name) - { - commandsDictionary.Add(discordCommand.Id, command); - commandFound = true; - break; - } - } - } - else - { - discordCommandName = this.Configuration.NamingPolicy.TransformText(discordCommand.Name, CultureInfo.InvariantCulture); - foreach (Command command in processorSpecificCommands) - { - string commandName = this.Configuration.NamingPolicy.TransformText(command.Name, CultureInfo.InvariantCulture); - if (commandName == discordCommandName) - { - commandsDictionary.Add(discordCommand.Id, command); - commandFound = true; - break; - } - } - } - - if (!commandFound) - { - // TODO: How do we report this to the user? Return a custom object perhaps? - SlashLogging.unknownCommandName(this.logger, discordCommandName, null); - } - } - - return commandsDictionary; - } - - /// - /// Only use this for commands of type . - /// It will cut out every subcommands which are considered to be not a SlashCommand - /// - /// - /// - /// - public async ValueTask ToApplicationCommandAsync(Command command) - { - if (this.extension is null) - { - throw new InvalidOperationException("SlashCommandProcessor has not been configured."); - } - - // Translate the command's name and description. - Dictionary nameLocalizations = []; - Dictionary descriptionLocalizations = []; - if (command.Attributes.OfType().FirstOrDefault() is InteractionLocalizerAttribute localizerAttribute) - { - if (!IsLocalizationSupported()) - { - throw new InvalidOperationException("Localization is not supported because invariant mode is enabled. See https://aka.ms/GlobalizationInvariantMode for more information."); - } - - nameLocalizations = await ExecuteLocalizerAsync(localizerAttribute.LocalizerType, $"{command.FullName}.name"); - descriptionLocalizations = await ExecuteLocalizerAsync(localizerAttribute.LocalizerType, $"{command.FullName}.description"); - } - - ValidateSlashCommand(command, nameLocalizations, descriptionLocalizations); - - // Convert the subcommands or parameters into application options - List options = []; - if (command.Subcommands.Count == 0) - { - await PopulateVariadicParametersAsync(command, options); - } - else - { - foreach (Command subcommand in command.Subcommands) - { - // If there is a SlashCommandTypesAttribute, check if it contains SlashCommandTypes.ApplicationCommand - // If there isn't, default to SlashCommands - if (subcommand.Attributes.OfType().FirstOrDefault() is SlashCommandTypesAttribute slashCommandTypesAttribute - && !slashCommandTypesAttribute.ApplicationCommandTypes.Contains(DiscordApplicationCommandType.SlashCommand)) - { - continue; - } - - options.Add(await ToApplicationParameterAsync(subcommand)); - } - } - - string? description = command.Description; - if (string.IsNullOrWhiteSpace(description)) - { - description = "No description provided."; - } - - DiscordPermissions? userPermissions = command.Attributes.OfType().FirstOrDefault()?.UserPermissions; - - // Create the top level application command. - return new - ( - name: this.Configuration.NamingPolicy.TransformText(command.Name, CultureInfo.InvariantCulture), - description: description, - options: options, - type: DiscordApplicationCommandType.SlashCommand, - name_localizations: nameLocalizations, - description_localizations: descriptionLocalizations, - allowDMUsage: command.Attributes.Any(x => x is AllowDMUsageAttribute), - defaultMemberPermissions: userPermissions is not null - ? userPermissions - : new DiscordPermissions(DiscordPermission.UseApplicationCommands), - nsfw: command.Attributes.Any(x => x is RequireNsfwAttribute), - contexts: command.Attributes.OfType().FirstOrDefault()?.AllowedContexts, - integrationTypes: command.Attributes.OfType().FirstOrDefault()?.InstallTypes - ); - } - - public async ValueTask ToApplicationParameterAsync(Command command) => await ToApplicationParameterAsync(command, 0); - - private async ValueTask ToApplicationParameterAsync(Command command, int depth = 1) - { - if (this.extension is null) - { - throw new InvalidOperationException("SlashCommandProcessor has not been configured."); - } - - // Convert the subcommands or parameters into application options - List options = []; - if (command.Subcommands.Count == 0) - { - await PopulateVariadicParametersAsync(command, options); - } - else - { - if (depth >= 3) - { - throw new InvalidOperationException($"Slash command failed validation: Command '{command.Name}' nests too deeply. Discord only supports up to 3 levels of nesting."); - } - - depth++; - foreach (Command subcommand in command.Subcommands) - { - options.Add(await ToApplicationParameterAsync(subcommand, depth)); - } - } - - // Translate the subcommand's name and description. - Dictionary nameLocalizations = []; - Dictionary descriptionLocalizations = []; - if (command.Attributes.OfType().FirstOrDefault() is InteractionLocalizerAttribute localizerAttribute) - { - foreach ((string ietfTag, string name) in await ExecuteLocalizerAsync(localizerAttribute.LocalizerType, $"{command.FullName}.name")) - { - if (!IsLocalizationSupported()) - { - throw new InvalidOperationException("Localization is not supported because invariant mode is enabled. See https://aka.ms/GlobalizationInvariantMode for more information."); - } - - nameLocalizations[ietfTag] = this.Configuration.NamingPolicy.TransformText - ( - name, - ietfTag == "en-US" ? CultureInfo.InvariantCulture : CultureInfo.GetCultureInfo(ietfTag) - ); - } - - descriptionLocalizations = await ExecuteLocalizerAsync(localizerAttribute.LocalizerType, $"{command.FullName}.description"); - } - - string? description = command.Description; - if (string.IsNullOrWhiteSpace(description)) - { - description = "No description provided."; - } - - return new( - name: this.Configuration.NamingPolicy.TransformText(command.Name, CultureInfo.InvariantCulture), - description: description, - name_localizations: nameLocalizations, - description_localizations: descriptionLocalizations, - type: command.Subcommands.Count > 0 ? DiscordApplicationCommandOptionType.SubCommandGroup : DiscordApplicationCommandOptionType.SubCommand, - options: options - ); - } - - private async ValueTask ToApplicationParameterAsync(Command command, CommandParameter parameter, int i = -1) - { - if (this.extension is null) - { - throw new InvalidOperationException("SlashCommandProcessor has not been configured."); - } - - // Fucking scope man. Let me else if in peace - // We need the converter to grab the parameter type's application command option type value. - if (!this.Converters.TryGetValue(IArgumentConverter.GetConverterFriendlyBaseType(parameter.Type), out ISlashArgumentConverter? slashArgumentConverter)) - { - throw new InvalidOperationException($"No converter found for parameter type '{parameter.Type.Name}'"); - } - - // Translate the parameter's name and description. - Dictionary nameLocalizations = []; - Dictionary descriptionLocalizations = []; - if (parameter.Attributes.OfType().FirstOrDefault() is InteractionLocalizerAttribute localizerAttribute) - { - StringBuilder localeIdBuilder = new(); - localeIdBuilder.Append($"{command.FullName}.parameters.{parameter.Name}"); - if (i != -1) - { - localeIdBuilder.Append($".{i}"); - } - - foreach ((string ietfTag, string name) in await ExecuteLocalizerAsync(localizerAttribute.LocalizerType, localeIdBuilder.ToString() + ".name")) - { - nameLocalizations[ietfTag] = this.Configuration.NamingPolicy.TransformText - ( - name, - ietfTag == "en-US" ? CultureInfo.InvariantCulture : CultureInfo.GetCultureInfo(ietfTag) - ); - } - - descriptionLocalizations = await ExecuteLocalizerAsync(localizerAttribute.LocalizerType, localeIdBuilder.ToString() + ".description"); - } - - IEnumerable choices = []; - if (parameter.Attributes.OfType().FirstOrDefault() is SlashChoiceProviderAttribute choiceAttribute) - { - using AsyncServiceScope scope = this.extension.ServiceProvider.CreateAsyncScope(); - choices = await choiceAttribute.GrabChoicesAsync(scope.ServiceProvider, parameter); - } - - string? description = parameter.Description; - if (string.IsNullOrWhiteSpace(description)) - { - description = "No description provided."; - } - - MinMaxLengthAttribute? minMaxLength = parameter.Attributes.OfType().FirstOrDefault(); - MinMaxValueAttribute? minMaxValue = parameter.Attributes.OfType().FirstOrDefault(); - object maxValue = minMaxValue?.MaxValue!; - object minValue = minMaxValue?.MinValue!; - - maxValue = maxValue switch - { - byte value => Math.Min(value, byte.MaxValue), - sbyte value => Math.Min(value, sbyte.MaxValue), - short value => Math.Min(value, short.MaxValue), - ushort value => Math.Min(value, ushort.MaxValue), - int value => Math.Min(value, int.MaxValue), - uint value => Math.Min(value, uint.MaxValue), - _ => maxValue, - }; - - minValue = minValue switch - { - byte value => Math.Min(value, byte.MinValue), - sbyte value => Math.Max(value, sbyte.MinValue), - short value => Math.Max(value, short.MinValue), - ushort value => Math.Min(value, ushort.MinValue), - int value => Math.Max(value, int.MinValue), - uint value => Math.Min(value, uint.MinValue), - _ => minValue, - }; - - return new( - name: this.Configuration.NamingPolicy.GetParameterName(parameter, CultureInfo.InvariantCulture, i), - description: description, - name_localizations: nameLocalizations, - description_localizations: descriptionLocalizations, - autocomplete: parameter.Attributes.Any(x => x is SlashAutoCompleteProviderAttribute), - channelTypes: parameter.Attributes.OfType().FirstOrDefault()?.ChannelTypes ?? [], - choices: choices, - maxLength: minMaxLength?.MaxLength, - maxValue: maxValue, - minLength: minMaxLength?.MinLength, - minValue: minValue, - required: !parameter.DefaultValue.HasValue && parameter.Attributes.Select(attribute => attribute is VariadicArgumentAttribute variadicArgumentAttribute - ? variadicArgumentAttribute.MinimumArgumentCount : 0).Sum() > i, - type: slashArgumentConverter.ParameterType - ); - } - - private async ValueTask PopulateVariadicParametersAsync(Command command, List options) - { - int minimumArgumentCount = 0; - foreach (Attribute attribute in command.Parameters.SelectMany(parameter => parameter.Attributes)) - { - if (attribute is not VariadicArgumentAttribute variadicArgumentAttribute) - { - continue; - } - - /* - * Take the following scenario: - * - * public static async ValueTask ExecuteAsync( - * CommandContext context, - * [VariadicArgument(Max = 50, Minimum = 10)] DiscordMember[] members, - * [VariadicArgument(Max = 50, Minimum = 16)] DiscordRole[] roles - * ); - * - * The total minimum argument count would be 26. Discord only supports up to 25 parameters. - * There is not a feasible workaround for this, so let's yell at the user. - */ - - minimumArgumentCount += variadicArgumentAttribute.MinimumArgumentCount; - if (minimumArgumentCount > 25) - { - throw new InvalidOperationException( - $"Slash command failed validation: Command '{command.Name}' has too many minimum arguments. Discord only supports up to 25 parameters, please lower the total minimum argument count that's set through {nameof(VariadicArgumentAttribute)}." - ); - } - } - - foreach (CommandParameter parameter in command.Parameters) - { - // Check if the parameter is using a variadic argument attribute. - // If it is we need to add the parameter multiple times. - VariadicArgumentAttribute? variadicArgumentAttribute = parameter.Attributes.FirstOrDefault(attribute => attribute is VariadicArgumentAttribute) as VariadicArgumentAttribute; - if (variadicArgumentAttribute is not null) - { - // Add the variadic parameter multiple times until we reach the maximum argument count. - int maximumArgumentCount = Math.Min(variadicArgumentAttribute.MaximumArgumentCount, 25 - options.Count); - for (int i = 0; i < maximumArgumentCount; i++) - { - options.Add(await ToApplicationParameterAsync(command, parameter, i)); - } - - continue; - } - - // This is just a normal parameter. - options.Add(await ToApplicationParameterAsync(command, parameter)); - continue; - } - } - - /// - /// Only use this for commands of type . - /// It will NOT validate every subcommands which are considered to be a SlashCommand - /// - /// - /// - /// - /// - private void ValidateSlashCommand(Command command, IReadOnlyDictionary nameLocalizations, IReadOnlyDictionary descriptionLocalizations) - { - if (command.Subcommands.Count > 0) - { - foreach (Command subcommand in command.Subcommands) - { - // If there is a SlashCommandTypesAttribute, check if it contains SlashCommandTypes.ApplicationCommand - // If there isn't, default to SlashCommands - if (subcommand.Attributes.OfType().FirstOrDefault() is SlashCommandTypesAttribute slashCommandTypesAttribute - && !slashCommandTypesAttribute.ApplicationCommandTypes.Contains(DiscordApplicationCommandType.SlashCommand)) - { - continue; - } - - ValidateSlashCommand(subcommand, nameLocalizations, descriptionLocalizations); - } - } - - if (command.Name.Length > 32) - { - throw new InvalidOperationException( - $"Slash command failed validation: {command.Name} is longer than 32 characters." - + $"\n(Name is {command.Name.Length - 32} characters too long)" - ); - } - - if (command.Description?.Length > 100) - { - throw new InvalidOperationException( - $"Slash command failed validation: {command.Name} description is longer than 100 characters." - + $"\n(Description is {command.Description.Length - 100} characters too long)" - ); - } - - foreach (KeyValuePair nameLocalization in nameLocalizations) - { - if (nameLocalization.Value.Length > 32) - { - throw new InvalidOperationException( - $"Slash command failed validation: {command.Name} name localization key is longer than 32 characters.\n" - + $"(Name localization key ({nameLocalization.Key}) is {nameLocalization.Key.Length - 32} characters too long)" - ); - } - - if (!NameLocalizationRegex().IsMatch(nameLocalization.Key)) - { - throw new InvalidOperationException( - $"Slash command failed validation: {command.Name} name localization key contains invalid characters.\n" - + $"(Name localization key ({nameLocalization.Key}) contains invalid characters)" - ); - } - } - - foreach (KeyValuePair descriptionLocalization in descriptionLocalizations) - { - if (descriptionLocalization.Value.Length > 100) - { - throw new InvalidOperationException( - $"Slash command failed validation: {command.Name} description localization key is longer than 100 characters.\n" - + $"(Description localization key ({descriptionLocalization.Key}) is {descriptionLocalization.Key.Length - 100} characters too long)" - ); - } - - if (descriptionLocalization.Key.Length is < 1 or > 100) - { - throw new InvalidOperationException( - $"Slash command failed validation: {command.Name} description localization key is longer than 100 characters.\n" - + $"(Description localization key ({descriptionLocalization.Key}) is {descriptionLocalization.Key.Length - 100} characters too long)" - ); - - // Come back to this when we have actual validation that does more than a length check - //throw new InvalidOperationException - //( - // $"Slash command failed validation: {command.Name} description localization key contains invalid characters.\n" + - // $"(Description localization key ({descriptionLocalization.Key}) contains invalid characters)" - //); - } - } - - if (!NameLocalizationRegex().IsMatch(command.Name)) - { - throw new InvalidOperationException($"Slash command failed validation: {command.Name} name contains invalid characters."); - } - - if (command.Description?.Length is < 1 or > 100) - { - throw new InvalidOperationException($"Slash command failed validation: {command.Name} description is longer than 100 characters."); - - // Come back to this when we have actual validation that does more than a length check - //throw new InvalidOperationException - //( - // $"Slash command failed validation: {command.Name} description contains invalid characters." - //); - } - } - - internal async ValueTask> ExecuteLocalizerAsync(Type localizer, string name) - { - using AsyncServiceScope scope = this.extension!.ServiceProvider.CreateAsyncScope(); - IInteractionLocalizer instance; - try - { - instance = (IInteractionLocalizer)ActivatorUtilities.CreateInstance(scope.ServiceProvider, localizer); - } - catch (Exception) - { - ILogger logger = this.extension!.ServiceProvider.GetService>() ?? NullLogger.Instance; - logger.LogWarning("Failed to create an instance of {TypeName} for localization of {SymbolName}.", localizer, name); - return []; - } - - Dictionary localized = []; - foreach ((DiscordLocale locale, string translation) in await instance.TranslateAsync(name.Replace(' ', '.').ToLowerInvariant())) - { - localized.Add(locale.ToString().Replace('_', '-'), translation); - } - - return localized; - } - - internal static Task ConfigureCommands(CommandsExtension extension, ConfigureCommandsEventArgs eventArgs) - { - foreach (CommandBuilder commandBuilder in eventArgs.CommandTrees.SelectMany(command => command.Flatten())) - { - foreach (CommandParameterBuilder parameterBuilder in commandBuilder.Parameters) - { - if (parameterBuilder.Type is null || parameterBuilder.Attributes.Any(attribute => attribute is SlashAutoCompleteProviderAttribute or SlashChoiceProviderAttribute)) - { - continue; - } - - Type baseType = IArgumentConverter.GetConverterFriendlyBaseType(parameterBuilder.Type); - if (!baseType.IsEnum) - { - continue; - } - - // If the enum has less than 25 values, we can use a choice provider. If it has more than 25 values, use autocomplete. - if (Enum.GetValues(baseType).Length > 25) - { - parameterBuilder.Attributes.Add(new SlashAutoCompleteProviderAttribute(typeof(EnumAutoCompleteProvider<>).MakeGenericType(baseType))); - } - else - { - parameterBuilder.Attributes.Add(new SlashChoiceProviderAttribute()); - } - } - } - - return Task.CompletedTask; - } - - /// - /// Attempts to find the correct locale to use by searching the user's locale, falling back to the guild's locale, then to invariant. - /// - /// The interaction to resolve the locale from. - /// Which culture to use. - internal static CultureInfo ResolveCulture(DiscordInteraction interaction) - { - if (!IsLocalizationSupported()) - { - return CultureInfo.InvariantCulture; - } - - if (!string.IsNullOrWhiteSpace(interaction.Locale)) - { - return CultureInfo.GetCultureInfo(interaction.Locale); - } - else if (!string.IsNullOrWhiteSpace(interaction.GuildLocale)) - { - return CultureInfo.GetCultureInfo(interaction.GuildLocale); - } - - return CultureInfo.InvariantCulture; - } - - internal static bool IsLocalizationSupported() - { - string? invariantEnvValue = Environment.GetEnvironmentVariable("DOTNET_SYSTEM_GLOBALIZATION_INVARIANT"); - - if (invariantEnvValue is not null) - { - if (invariantEnvValue == "1" || invariantEnvValue.Equals("true", StringComparison.InvariantCultureIgnoreCase)) - { - return false; - } - - if (invariantEnvValue == "0" || invariantEnvValue.Equals("false", StringComparison.InvariantCultureIgnoreCase)) - { - return true; - } - } - - return !AppContext.TryGetSwitch("System.Globalization.Invariant", out bool value) || !value; - } -} +using System; +using System.Collections.Frozen; +using System.Collections.Generic; +using System.ComponentModel; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +using DSharpPlus.Commands.ArgumentModifiers; +using DSharpPlus.Commands.ContextChecks; +using DSharpPlus.Commands.Converters; +using DSharpPlus.Commands.EventArgs; +using DSharpPlus.Commands.Processors.SlashCommands.ArgumentModifiers; +using DSharpPlus.Commands.Processors.SlashCommands.Localization; +using DSharpPlus.Commands.Processors.SlashCommands.Metadata; +using DSharpPlus.Commands.Trees; +using DSharpPlus.Commands.Trees.Metadata; +using DSharpPlus.Entities; + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace DSharpPlus.Commands.Processors.SlashCommands; + +public sealed partial class SlashCommandProcessor : BaseCommandProcessor +{ + [GeneratedRegex(@"^[-_\p{L}\p{N}\p{IsDevanagari}\p{IsThai}]{1,32}$")] + private static partial Regex NameLocalizationRegex(); + + private static FrozenDictionary applicationCommandMapping = FrozenDictionary.Empty; + private static readonly List applicationCommands = []; + + // if registration failed, this is set to true and will trigger better error messages + private bool registrationFailed = false; + + /// + /// The mapping of application command ids to objects. + /// + public IReadOnlyDictionary ApplicationCommandMapping => applicationCommandMapping; + + public void AddApplicationCommands(params DiscordApplicationCommand[] commands) => applicationCommands.AddRange(commands); + public void AddApplicationCommands(IEnumerable commands) => applicationCommands.AddRange(commands); + + /// + /// Registers as application commands. + /// This will registers regardless of 's value. + /// + /// The extension to read the commands from. + public async ValueTask RegisterSlashCommandsAsync(CommandsExtension extension) + { + if (this.isApplicationCommandsRegistered) + { + return; + } + + this.isApplicationCommandsRegistered = true; + + IReadOnlyList processorSpecificCommands = extension.GetCommandsForProcessor(this); + List globalApplicationCommands = []; + Dictionary> guildsApplicationCommands = []; + globalApplicationCommands.AddRange(applicationCommands); + + try + { + + foreach (Command command in processorSpecificCommands) + { + // If there is a SlashCommandTypesAttribute, check if it contains SlashCommandTypes.ApplicationCommand + // If there isn't, default to SlashCommands + if (command.Attributes.OfType().FirstOrDefault() is SlashCommandTypesAttribute slashCommandTypesAttribute + && !slashCommandTypesAttribute.ApplicationCommandTypes.Contains(DiscordApplicationCommandType.SlashCommand) + ) + { + continue; + } + + DiscordApplicationCommand applicationCommand = await ToApplicationCommandAsync(command); + if (command.GuildIds.Count == 0) + { + globalApplicationCommands.Add(applicationCommand); + continue; + } + + foreach (ulong guildId in command.GuildIds) + { + if (!guildsApplicationCommands.TryGetValue(guildId, out List? guildCommands)) + { + guildCommands = []; + guildsApplicationCommands.Add(guildId, guildCommands); + } + + guildCommands.Add(applicationCommand); + } + } + } + catch (Exception e) + { + this.logger.LogError(e, "Could not build valid application commands, cancelling application command registration."); + this.registrationFailed = true; + return; + } + + // we figured our structure out, fetch discord's records of the commands and match basic criteria + // skip if we are instructed to disable this behaviour + + List discordCommands = []; + + if (this.Configuration.UnconditionallyOverwriteCommands) + { + discordCommands.AddRange + ( + this.extension.DebugGuildId == 0 + ? await this.extension.Client.BulkOverwriteGlobalApplicationCommandsAsync(globalApplicationCommands) + : await this.extension.Client.BulkOverwriteGuildApplicationCommandsAsync + ( + this.extension.DebugGuildId, + globalApplicationCommands + ) + ); + } + else + { + IReadOnlyList preexisting = this.extension.DebugGuildId == 0 + ? await this.extension.Client.GetGlobalApplicationCommandsAsync(true) + : await this.extension.Client.GetGuildApplicationCommandsAsync(this.extension.DebugGuildId, true); + + discordCommands.AddRange(await VerifyAndUpdateRemoteCommandsAsync(globalApplicationCommands, preexisting)); + } + + // for the time being, we still overwrite guilds by force + foreach (KeyValuePair> kv in guildsApplicationCommands) + { + discordCommands.AddRange + ( + await this.extension.Client.BulkOverwriteGuildApplicationCommandsAsync(kv.Key, kv.Value) + ); + } + + applicationCommandMapping = MapApplicationCommands(discordCommands).ToFrozenDictionary(); + + SlashLogging.registeredCommands( + this.logger, + applicationCommandMapping.Count, + applicationCommandMapping.Values.SelectMany(command => command.Flatten()).Count(), + null + ); + } + + /// + /// Matches the application commands to the commands in the command tree. + /// + /// The application commands obtained from Discord. Accepts both global and guild commands. + /// A dictionary mapping the application command id to the command in the command tree. + public IReadOnlyDictionary MapApplicationCommands(IReadOnlyList applicationCommands) + { + if (this.extension is null) + { + throw new InvalidOperationException("SlashCommandProcessor has not been configured."); + } + + Dictionary commandsDictionary = []; + IReadOnlyList processorSpecificCommands = this.extension!.GetCommandsForProcessor(this); + IReadOnlyList flattenCommands = processorSpecificCommands.SelectMany(x => x.Flatten()).ToList(); + + foreach (DiscordApplicationCommand discordCommand in applicationCommands) + { + bool commandFound = false; + string discordCommandName; + if (discordCommand.Type is DiscordApplicationCommandType.MessageContextMenu or DiscordApplicationCommandType.UserContextMenu) + { + discordCommandName = discordCommand.Name; + foreach (Command command in flattenCommands) + { + string commandName = command.Attributes.OfType().FirstOrDefault()?.DisplayName ?? command.FullName; + if (commandName == discordCommand.Name) + { + commandsDictionary.Add(discordCommand.Id, command); + commandFound = true; + break; + } + } + } + else + { + discordCommandName = this.Configuration.NamingPolicy.TransformText(discordCommand.Name, CultureInfo.InvariantCulture); + foreach (Command command in processorSpecificCommands) + { + string commandName = this.Configuration.NamingPolicy.TransformText(command.Name, CultureInfo.InvariantCulture); + if (commandName == discordCommandName) + { + commandsDictionary.Add(discordCommand.Id, command); + commandFound = true; + break; + } + } + } + + if (!commandFound) + { + // TODO: How do we report this to the user? Return a custom object perhaps? + SlashLogging.unknownCommandName(this.logger, discordCommandName, null); + } + } + + return commandsDictionary; + } + + /// + /// Only use this for commands of type . + /// It will cut out every subcommands which are considered to be not a SlashCommand + /// + /// + /// + /// + public async ValueTask ToApplicationCommandAsync(Command command) + { + if (this.extension is null) + { + throw new InvalidOperationException("SlashCommandProcessor has not been configured."); + } + + // Translate the command's name and description. + Dictionary nameLocalizations = []; + Dictionary descriptionLocalizations = []; + if (command.Attributes.OfType().FirstOrDefault() is InteractionLocalizerAttribute localizerAttribute) + { + if (!IsLocalizationSupported()) + { + throw new InvalidOperationException("Localization is not supported because invariant mode is enabled. See https://aka.ms/GlobalizationInvariantMode for more information."); + } + + nameLocalizations = await ExecuteLocalizerAsync(localizerAttribute.LocalizerType, $"{command.FullName}.name"); + descriptionLocalizations = await ExecuteLocalizerAsync(localizerAttribute.LocalizerType, $"{command.FullName}.description"); + } + + ValidateSlashCommand(command, nameLocalizations, descriptionLocalizations); + + // Convert the subcommands or parameters into application options + List options = []; + if (command.Subcommands.Count == 0) + { + await PopulateVariadicParametersAsync(command, options); + } + else + { + foreach (Command subcommand in command.Subcommands) + { + // If there is a SlashCommandTypesAttribute, check if it contains SlashCommandTypes.ApplicationCommand + // If there isn't, default to SlashCommands + if (subcommand.Attributes.OfType().FirstOrDefault() is SlashCommandTypesAttribute slashCommandTypesAttribute + && !slashCommandTypesAttribute.ApplicationCommandTypes.Contains(DiscordApplicationCommandType.SlashCommand)) + { + continue; + } + + options.Add(await ToApplicationParameterAsync(subcommand)); + } + } + + string? description = command.Description; + if (string.IsNullOrWhiteSpace(description)) + { + description = "No description provided."; + } + + DiscordPermissions? userPermissions = command.Attributes.OfType().FirstOrDefault()?.UserPermissions; + + // Create the top level application command. + return new + ( + name: this.Configuration.NamingPolicy.TransformText(command.Name, CultureInfo.InvariantCulture), + description: description, + options: options, + type: DiscordApplicationCommandType.SlashCommand, + name_localizations: nameLocalizations, + description_localizations: descriptionLocalizations, + allowDMUsage: command.Attributes.Any(x => x is AllowDMUsageAttribute), + defaultMemberPermissions: userPermissions is not null + ? userPermissions + : new DiscordPermissions(DiscordPermission.UseApplicationCommands), + nsfw: command.Attributes.Any(x => x is RequireNsfwAttribute), + contexts: command.Attributes.OfType().FirstOrDefault()?.AllowedContexts, + integrationTypes: command.Attributes.OfType().FirstOrDefault()?.InstallTypes + ); + } + + public async ValueTask ToApplicationParameterAsync(Command command) => await ToApplicationParameterAsync(command, 0); + + private async ValueTask ToApplicationParameterAsync(Command command, int depth = 1) + { + if (this.extension is null) + { + throw new InvalidOperationException("SlashCommandProcessor has not been configured."); + } + + // Convert the subcommands or parameters into application options + List options = []; + if (command.Subcommands.Count == 0) + { + await PopulateVariadicParametersAsync(command, options); + } + else + { + if (depth >= 3) + { + throw new InvalidOperationException($"Slash command failed validation: Command '{command.Name}' nests too deeply. Discord only supports up to 3 levels of nesting."); + } + + depth++; + foreach (Command subcommand in command.Subcommands) + { + options.Add(await ToApplicationParameterAsync(subcommand, depth)); + } + } + + // Translate the subcommand's name and description. + Dictionary nameLocalizations = []; + Dictionary descriptionLocalizations = []; + if (command.Attributes.OfType().FirstOrDefault() is InteractionLocalizerAttribute localizerAttribute) + { + foreach ((string ietfTag, string name) in await ExecuteLocalizerAsync(localizerAttribute.LocalizerType, $"{command.FullName}.name")) + { + if (!IsLocalizationSupported()) + { + throw new InvalidOperationException("Localization is not supported because invariant mode is enabled. See https://aka.ms/GlobalizationInvariantMode for more information."); + } + + nameLocalizations[ietfTag] = this.Configuration.NamingPolicy.TransformText + ( + name, + ietfTag == "en-US" ? CultureInfo.InvariantCulture : CultureInfo.GetCultureInfo(ietfTag) + ); + } + + descriptionLocalizations = await ExecuteLocalizerAsync(localizerAttribute.LocalizerType, $"{command.FullName}.description"); + } + + string? description = command.Description; + if (string.IsNullOrWhiteSpace(description)) + { + description = "No description provided."; + } + + return new( + name: this.Configuration.NamingPolicy.TransformText(command.Name, CultureInfo.InvariantCulture), + description: description, + name_localizations: nameLocalizations, + description_localizations: descriptionLocalizations, + type: command.Subcommands.Count > 0 ? DiscordApplicationCommandOptionType.SubCommandGroup : DiscordApplicationCommandOptionType.SubCommand, + options: options + ); + } + + private async ValueTask ToApplicationParameterAsync(Command command, CommandParameter parameter, int i = -1) + { + if (this.extension is null) + { + throw new InvalidOperationException("SlashCommandProcessor has not been configured."); + } + + // Fucking scope man. Let me else if in peace + // We need the converter to grab the parameter type's application command option type value. + if (!this.Converters.TryGetValue(IArgumentConverter.GetConverterFriendlyBaseType(parameter.Type), out ISlashArgumentConverter? slashArgumentConverter)) + { + throw new InvalidOperationException($"No converter found for parameter type '{parameter.Type.Name}'"); + } + + // Translate the parameter's name and description. + Dictionary nameLocalizations = []; + Dictionary descriptionLocalizations = []; + if (parameter.Attributes.OfType().FirstOrDefault() is InteractionLocalizerAttribute localizerAttribute) + { + StringBuilder localeIdBuilder = new(); + localeIdBuilder.Append($"{command.FullName}.parameters.{parameter.Name}"); + if (i != -1) + { + localeIdBuilder.Append($".{i}"); + } + + foreach ((string ietfTag, string name) in await ExecuteLocalizerAsync(localizerAttribute.LocalizerType, localeIdBuilder.ToString() + ".name")) + { + nameLocalizations[ietfTag] = this.Configuration.NamingPolicy.TransformText + ( + name, + ietfTag == "en-US" ? CultureInfo.InvariantCulture : CultureInfo.GetCultureInfo(ietfTag) + ); + } + + descriptionLocalizations = await ExecuteLocalizerAsync(localizerAttribute.LocalizerType, localeIdBuilder.ToString() + ".description"); + } + + IEnumerable choices = []; + if (parameter.Attributes.OfType().FirstOrDefault() is SlashChoiceProviderAttribute choiceAttribute) + { + using AsyncServiceScope scope = this.extension.ServiceProvider.CreateAsyncScope(); + choices = await choiceAttribute.GrabChoicesAsync(scope.ServiceProvider, parameter); + } + + string? description = parameter.Description; + if (string.IsNullOrWhiteSpace(description)) + { + description = "No description provided."; + } + + MinMaxLengthAttribute? minMaxLength = parameter.Attributes.OfType().FirstOrDefault(); + MinMaxValueAttribute? minMaxValue = parameter.Attributes.OfType().FirstOrDefault(); + object maxValue = minMaxValue?.MaxValue!; + object minValue = minMaxValue?.MinValue!; + + maxValue = maxValue switch + { + byte value => Math.Min(value, byte.MaxValue), + sbyte value => Math.Min(value, sbyte.MaxValue), + short value => Math.Min(value, short.MaxValue), + ushort value => Math.Min(value, ushort.MaxValue), + int value => Math.Min(value, int.MaxValue), + uint value => Math.Min(value, uint.MaxValue), + _ => maxValue, + }; + + minValue = minValue switch + { + byte value => Math.Min(value, byte.MinValue), + sbyte value => Math.Max(value, sbyte.MinValue), + short value => Math.Max(value, short.MinValue), + ushort value => Math.Min(value, ushort.MinValue), + int value => Math.Max(value, int.MinValue), + uint value => Math.Min(value, uint.MinValue), + _ => minValue, + }; + + return new( + name: this.Configuration.NamingPolicy.GetParameterName(parameter, CultureInfo.InvariantCulture, i), + description: description, + name_localizations: nameLocalizations, + description_localizations: descriptionLocalizations, + autocomplete: parameter.Attributes.Any(x => x is SlashAutoCompleteProviderAttribute), + channelTypes: parameter.Attributes.OfType().FirstOrDefault()?.ChannelTypes ?? [], + choices: choices, + maxLength: minMaxLength?.MaxLength, + maxValue: maxValue, + minLength: minMaxLength?.MinLength, + minValue: minValue, + required: !parameter.DefaultValue.HasValue && parameter.Attributes.Select(attribute => attribute is VariadicArgumentAttribute variadicArgumentAttribute + ? variadicArgumentAttribute.MinimumArgumentCount : 0).Sum() > i, + type: slashArgumentConverter.ParameterType + ); + } + + private async ValueTask PopulateVariadicParametersAsync(Command command, List options) + { + int minimumArgumentCount = 0; + foreach (Attribute attribute in command.Parameters.SelectMany(parameter => parameter.Attributes)) + { + if (attribute is not VariadicArgumentAttribute variadicArgumentAttribute) + { + continue; + } + + /* + * Take the following scenario: + * + * public static async ValueTask ExecuteAsync( + * CommandContext context, + * [VariadicArgument(Max = 50, Minimum = 10)] DiscordMember[] members, + * [VariadicArgument(Max = 50, Minimum = 16)] DiscordRole[] roles + * ); + * + * The total minimum argument count would be 26. Discord only supports up to 25 parameters. + * There is not a feasible workaround for this, so let's yell at the user. + */ + + minimumArgumentCount += variadicArgumentAttribute.MinimumArgumentCount; + if (minimumArgumentCount > 25) + { + throw new InvalidOperationException( + $"Slash command failed validation: Command '{command.Name}' has too many minimum arguments. Discord only supports up to 25 parameters, please lower the total minimum argument count that's set through {nameof(VariadicArgumentAttribute)}." + ); + } + } + + foreach (CommandParameter parameter in command.Parameters) + { + // Check if the parameter is using a variadic argument attribute. + // If it is we need to add the parameter multiple times. + VariadicArgumentAttribute? variadicArgumentAttribute = parameter.Attributes.FirstOrDefault(attribute => attribute is VariadicArgumentAttribute) as VariadicArgumentAttribute; + if (variadicArgumentAttribute is not null) + { + // Add the variadic parameter multiple times until we reach the maximum argument count. + int maximumArgumentCount = Math.Min(variadicArgumentAttribute.MaximumArgumentCount, 25 - options.Count); + for (int i = 0; i < maximumArgumentCount; i++) + { + options.Add(await ToApplicationParameterAsync(command, parameter, i)); + } + + continue; + } + + // This is just a normal parameter. + options.Add(await ToApplicationParameterAsync(command, parameter)); + continue; + } + } + + /// + /// Only use this for commands of type . + /// It will NOT validate every subcommands which are considered to be a SlashCommand + /// + /// + /// + /// + /// + private void ValidateSlashCommand(Command command, IReadOnlyDictionary nameLocalizations, IReadOnlyDictionary descriptionLocalizations) + { + if (command.Subcommands.Count > 0) + { + foreach (Command subcommand in command.Subcommands) + { + // If there is a SlashCommandTypesAttribute, check if it contains SlashCommandTypes.ApplicationCommand + // If there isn't, default to SlashCommands + if (subcommand.Attributes.OfType().FirstOrDefault() is SlashCommandTypesAttribute slashCommandTypesAttribute + && !slashCommandTypesAttribute.ApplicationCommandTypes.Contains(DiscordApplicationCommandType.SlashCommand)) + { + continue; + } + + ValidateSlashCommand(subcommand, nameLocalizations, descriptionLocalizations); + } + } + + if (command.Name.Length > 32) + { + throw new InvalidOperationException( + $"Slash command failed validation: {command.Name} is longer than 32 characters." + + $"\n(Name is {command.Name.Length - 32} characters too long)" + ); + } + + if (command.Description?.Length > 100) + { + throw new InvalidOperationException( + $"Slash command failed validation: {command.Name} description is longer than 100 characters." + + $"\n(Description is {command.Description.Length - 100} characters too long)" + ); + } + + foreach (KeyValuePair nameLocalization in nameLocalizations) + { + if (nameLocalization.Value.Length > 32) + { + throw new InvalidOperationException( + $"Slash command failed validation: {command.Name} name localization key is longer than 32 characters.\n" + + $"(Name localization key ({nameLocalization.Key}) is {nameLocalization.Key.Length - 32} characters too long)" + ); + } + + if (!NameLocalizationRegex().IsMatch(nameLocalization.Key)) + { + throw new InvalidOperationException( + $"Slash command failed validation: {command.Name} name localization key contains invalid characters.\n" + + $"(Name localization key ({nameLocalization.Key}) contains invalid characters)" + ); + } + } + + foreach (KeyValuePair descriptionLocalization in descriptionLocalizations) + { + if (descriptionLocalization.Value.Length > 100) + { + throw new InvalidOperationException( + $"Slash command failed validation: {command.Name} description localization key is longer than 100 characters.\n" + + $"(Description localization key ({descriptionLocalization.Key}) is {descriptionLocalization.Key.Length - 100} characters too long)" + ); + } + + if (descriptionLocalization.Key.Length is < 1 or > 100) + { + throw new InvalidOperationException( + $"Slash command failed validation: {command.Name} description localization key is longer than 100 characters.\n" + + $"(Description localization key ({descriptionLocalization.Key}) is {descriptionLocalization.Key.Length - 100} characters too long)" + ); + + // Come back to this when we have actual validation that does more than a length check + //throw new InvalidOperationException + //( + // $"Slash command failed validation: {command.Name} description localization key contains invalid characters.\n" + + // $"(Description localization key ({descriptionLocalization.Key}) contains invalid characters)" + //); + } + } + + if (!NameLocalizationRegex().IsMatch(command.Name)) + { + throw new InvalidOperationException($"Slash command failed validation: {command.Name} name contains invalid characters."); + } + + if (command.Description?.Length is < 1 or > 100) + { + throw new InvalidOperationException($"Slash command failed validation: {command.Name} description is longer than 100 characters."); + + // Come back to this when we have actual validation that does more than a length check + //throw new InvalidOperationException + //( + // $"Slash command failed validation: {command.Name} description contains invalid characters." + //); + } + } + + internal async ValueTask> ExecuteLocalizerAsync(Type localizer, string name) + { + using AsyncServiceScope scope = this.extension!.ServiceProvider.CreateAsyncScope(); + IInteractionLocalizer instance; + try + { + instance = (IInteractionLocalizer)ActivatorUtilities.CreateInstance(scope.ServiceProvider, localizer); + } + catch (Exception) + { + ILogger logger = this.extension!.ServiceProvider.GetService>() ?? NullLogger.Instance; + logger.LogWarning("Failed to create an instance of {TypeName} for localization of {SymbolName}.", localizer, name); + return []; + } + + Dictionary localized = []; + foreach ((DiscordLocale locale, string translation) in await instance.TranslateAsync(name.Replace(' ', '.').ToLowerInvariant())) + { + localized.Add(locale.ToString().Replace('_', '-'), translation); + } + + return localized; + } + + internal static Task ConfigureCommands(CommandsExtension extension, ConfigureCommandsEventArgs eventArgs) + { + foreach (CommandBuilder commandBuilder in eventArgs.CommandTrees.SelectMany(command => command.Flatten())) + { + foreach (CommandParameterBuilder parameterBuilder in commandBuilder.Parameters) + { + if (parameterBuilder.Type is null || parameterBuilder.Attributes.Any(attribute => attribute is SlashAutoCompleteProviderAttribute or SlashChoiceProviderAttribute)) + { + continue; + } + + Type baseType = IArgumentConverter.GetConverterFriendlyBaseType(parameterBuilder.Type); + if (!baseType.IsEnum) + { + continue; + } + + // If the enum has less than 25 values, we can use a choice provider. If it has more than 25 values, use autocomplete. + if (Enum.GetValues(baseType).Length > 25) + { + parameterBuilder.Attributes.Add(new SlashAutoCompleteProviderAttribute(typeof(EnumAutoCompleteProvider<>).MakeGenericType(baseType))); + } + else + { + parameterBuilder.Attributes.Add(new SlashChoiceProviderAttribute()); + } + } + } + + return Task.CompletedTask; + } + + /// + /// Attempts to find the correct locale to use by searching the user's locale, falling back to the guild's locale, then to invariant. + /// + /// The interaction to resolve the locale from. + /// Which culture to use. + internal static CultureInfo ResolveCulture(DiscordInteraction interaction) + { + if (!IsLocalizationSupported()) + { + return CultureInfo.InvariantCulture; + } + + if (!string.IsNullOrWhiteSpace(interaction.Locale)) + { + return CultureInfo.GetCultureInfo(interaction.Locale); + } + else if (!string.IsNullOrWhiteSpace(interaction.GuildLocale)) + { + return CultureInfo.GetCultureInfo(interaction.GuildLocale); + } + + return CultureInfo.InvariantCulture; + } + + internal static bool IsLocalizationSupported() + { + string? invariantEnvValue = Environment.GetEnvironmentVariable("DOTNET_SYSTEM_GLOBALIZATION_INVARIANT"); + + if (invariantEnvValue is not null) + { + if (invariantEnvValue == "1" || invariantEnvValue.Equals("true", StringComparison.InvariantCultureIgnoreCase)) + { + return false; + } + + if (invariantEnvValue == "0" || invariantEnvValue.Equals("false", StringComparison.InvariantCultureIgnoreCase)) + { + return true; + } + } + + return !AppContext.TryGetSwitch("System.Globalization.Invariant", out bool value) || !value; + } +} diff --git a/DSharpPlus.Commands/Processors/SlashCommands/SlashLogging.cs b/DSharpPlus.Commands/Processors/SlashCommands/SlashLogging.cs index 2c3dfabeb0..ce094c8e25 100644 --- a/DSharpPlus.Commands/Processors/SlashCommands/SlashLogging.cs +++ b/DSharpPlus.Commands/Processors/SlashCommands/SlashLogging.cs @@ -1,14 +1,14 @@ -using System; -using Microsoft.Extensions.Logging; - -namespace DSharpPlus.Commands.Processors.SlashCommands; - -internal static class SlashLogging -{ - // Startup logs - internal static readonly Action registeredCommands = LoggerMessage.Define(LogLevel.Information, new EventId(1, "Slash Commands Startup"), "Registered {TopLevelCommandCount:N0} top-level slash commands, {TotalCommandCount:N0} total slash commands."); - internal static readonly Action interactionReceivedBeforeConfigured = LoggerMessage.Define(LogLevel.Warning, new EventId(2, "Slash Commands Startup"), "Received an interaction before the slash commands processor was configured. This interaction will be ignored."); - internal static readonly Action unknownCommandName = LoggerMessage.Define(LogLevel.Trace, new EventId(1, "Slash Commands Runtime"), "Received Command '{CommandName}' but no matching local command was found. Was this command for a different process?"); - internal static readonly Action detectedCommandRecordChanges = LoggerMessage.Define(LogLevel.Information, default, "Detected changes in slash command records: {Unchanged} without changes, {Added} added, {Edited} edited, {Deleted} deleted"); - internal static readonly Action commandRecordsUnchanged = LoggerMessage.Define(LogLevel.Information, default, "No application command changes detected."); -} +using System; +using Microsoft.Extensions.Logging; + +namespace DSharpPlus.Commands.Processors.SlashCommands; + +internal static class SlashLogging +{ + // Startup logs + internal static readonly Action registeredCommands = LoggerMessage.Define(LogLevel.Information, new EventId(1, "Slash Commands Startup"), "Registered {TopLevelCommandCount:N0} top-level slash commands, {TotalCommandCount:N0} total slash commands."); + internal static readonly Action interactionReceivedBeforeConfigured = LoggerMessage.Define(LogLevel.Warning, new EventId(2, "Slash Commands Startup"), "Received an interaction before the slash commands processor was configured. This interaction will be ignored."); + internal static readonly Action unknownCommandName = LoggerMessage.Define(LogLevel.Trace, new EventId(1, "Slash Commands Runtime"), "Received Command '{CommandName}' but no matching local command was found. Was this command for a different process?"); + internal static readonly Action detectedCommandRecordChanges = LoggerMessage.Define(LogLevel.Information, default, "Detected changes in slash command records: {Unchanged} without changes, {Added} added, {Edited} edited, {Deleted} deleted"); + internal static readonly Action commandRecordsUnchanged = LoggerMessage.Define(LogLevel.Information, default, "No application command changes detected."); +} diff --git a/DSharpPlus.Commands/Processors/TextCommands/ContextChecks/TextChannelTypesCheck.cs b/DSharpPlus.Commands/Processors/TextCommands/ContextChecks/TextChannelTypesCheck.cs index 991cbd101c..67bd6dfb05 100644 --- a/DSharpPlus.Commands/Processors/TextCommands/ContextChecks/TextChannelTypesCheck.cs +++ b/DSharpPlus.Commands/Processors/TextCommands/ContextChecks/TextChannelTypesCheck.cs @@ -1,28 +1,28 @@ -using System.Linq; -using System.Threading.Tasks; -using DSharpPlus.Commands.ArgumentModifiers; -using DSharpPlus.Commands.ContextChecks.ParameterChecks; -using DSharpPlus.Entities; - -namespace DSharpPlus.Commands.Processors.TextCommands.ContextChecks; - -/// -/// Implements a check for channel types on text commands. -/// -internal sealed class TextChannelTypesCheck : IParameterCheck -{ - /// - public ValueTask ExecuteCheckAsync(ChannelTypesAttribute attribute, ParameterCheckInfo info, CommandContext context) - { - if (info.Value is not DiscordChannel channel) - { - return ValueTask.FromResult(null); - } - else if (attribute.ChannelTypes.Contains(channel.Type)) - { - return ValueTask.FromResult(null); - } - - return ValueTask.FromResult("The specified channel was not of one of the required types."); - } -} +using System.Linq; +using System.Threading.Tasks; +using DSharpPlus.Commands.ArgumentModifiers; +using DSharpPlus.Commands.ContextChecks.ParameterChecks; +using DSharpPlus.Entities; + +namespace DSharpPlus.Commands.Processors.TextCommands.ContextChecks; + +/// +/// Implements a check for channel types on text commands. +/// +internal sealed class TextChannelTypesCheck : IParameterCheck +{ + /// + public ValueTask ExecuteCheckAsync(ChannelTypesAttribute attribute, ParameterCheckInfo info, CommandContext context) + { + if (info.Value is not DiscordChannel channel) + { + return ValueTask.FromResult(null); + } + else if (attribute.ChannelTypes.Contains(channel.Type)) + { + return ValueTask.FromResult(null); + } + + return ValueTask.FromResult("The specified channel was not of one of the required types."); + } +} diff --git a/DSharpPlus.Commands/Processors/TextCommands/ContextChecks/TextMessageReplyAttribute.cs b/DSharpPlus.Commands/Processors/TextCommands/ContextChecks/TextMessageReplyAttribute.cs index 2215d65f49..eb45bd9c8f 100644 --- a/DSharpPlus.Commands/Processors/TextCommands/ContextChecks/TextMessageReplyAttribute.cs +++ b/DSharpPlus.Commands/Processors/TextCommands/ContextChecks/TextMessageReplyAttribute.cs @@ -1,10 +1,10 @@ -using System; -using DSharpPlus.Commands.ContextChecks; - -namespace DSharpPlus.Commands.Processors.TextCommands.ContextChecks; - -[AttributeUsage(AttributeTargets.Parameter)] -public class TextMessageReplyAttribute(bool require = false) : ContextCheckAttribute -{ - public bool RequiresReply { get; init; } = require; -} +using System; +using DSharpPlus.Commands.ContextChecks; + +namespace DSharpPlus.Commands.Processors.TextCommands.ContextChecks; + +[AttributeUsage(AttributeTargets.Parameter)] +public class TextMessageReplyAttribute(bool require = false) : ContextCheckAttribute +{ + public bool RequiresReply { get; init; } = require; +} diff --git a/DSharpPlus.Commands/Processors/TextCommands/ContextChecks/TextMessageReplyCheck.cs b/DSharpPlus.Commands/Processors/TextCommands/ContextChecks/TextMessageReplyCheck.cs index 0f527f4433..d8d7ebb0fb 100644 --- a/DSharpPlus.Commands/Processors/TextCommands/ContextChecks/TextMessageReplyCheck.cs +++ b/DSharpPlus.Commands/Processors/TextCommands/ContextChecks/TextMessageReplyCheck.cs @@ -1,13 +1,13 @@ -using System.Threading.Tasks; -using DSharpPlus.Commands.ContextChecks; - -namespace DSharpPlus.Commands.Processors.TextCommands.ContextChecks; - -internal sealed class TextMessageReplyCheck : IContextCheck -{ - public ValueTask ExecuteCheckAsync(TextMessageReplyAttribute attribute, CommandContext context) => - ValueTask.FromResult(!attribute.RequiresReply || context.As().Message.ReferencedMessage is not null - ? null - : "This command requires to be used in reply to a message." - ); -} +using System.Threading.Tasks; +using DSharpPlus.Commands.ContextChecks; + +namespace DSharpPlus.Commands.Processors.TextCommands.ContextChecks; + +internal sealed class TextMessageReplyCheck : IContextCheck +{ + public ValueTask ExecuteCheckAsync(TextMessageReplyAttribute attribute, CommandContext context) => + ValueTask.FromResult(!attribute.RequiresReply || context.As().Message.ReferencedMessage is not null + ? null + : "This command requires to be used in reply to a message." + ); +} diff --git a/DSharpPlus.Commands/Processors/TextCommands/ContextChecks/TextMinMaxLengthCheck.cs b/DSharpPlus.Commands/Processors/TextCommands/ContextChecks/TextMinMaxLengthCheck.cs index 228e00e1ea..45f12397dd 100644 --- a/DSharpPlus.Commands/Processors/TextCommands/ContextChecks/TextMinMaxLengthCheck.cs +++ b/DSharpPlus.Commands/Processors/TextCommands/ContextChecks/TextMinMaxLengthCheck.cs @@ -1,30 +1,30 @@ -using System.Threading.Tasks; -using DSharpPlus.Commands.ArgumentModifiers; -using DSharpPlus.Commands.ContextChecks.ParameterChecks; - -namespace DSharpPlus.Commands.Processors.TextCommands.ContextChecks; - -/// -/// Implements min/max length checks for strings on text commands. -/// -internal sealed class TextMinMaxLengthCheck : IParameterCheck -{ - /// - public ValueTask ExecuteCheckAsync(MinMaxLengthAttribute attribute, ParameterCheckInfo info, CommandContext context) - { - if (info.Value is not string value) - { - return ValueTask.FromResult(null); - } - else if (value.Length < attribute.MinLength) - { - return ValueTask.FromResult($"The supplied string was too short, expected a minimum length of {attribute.MinLength}."); - } - else if (value.Length > attribute.MaxLength) - { - return ValueTask.FromResult($"The supplied string was too long, expected a maximum length of {attribute.MaxLength}."); - } - - return ValueTask.FromResult(null); - } -} +using System.Threading.Tasks; +using DSharpPlus.Commands.ArgumentModifiers; +using DSharpPlus.Commands.ContextChecks.ParameterChecks; + +namespace DSharpPlus.Commands.Processors.TextCommands.ContextChecks; + +/// +/// Implements min/max length checks for strings on text commands. +/// +internal sealed class TextMinMaxLengthCheck : IParameterCheck +{ + /// + public ValueTask ExecuteCheckAsync(MinMaxLengthAttribute attribute, ParameterCheckInfo info, CommandContext context) + { + if (info.Value is not string value) + { + return ValueTask.FromResult(null); + } + else if (value.Length < attribute.MinLength) + { + return ValueTask.FromResult($"The supplied string was too short, expected a minimum length of {attribute.MinLength}."); + } + else if (value.Length > attribute.MaxLength) + { + return ValueTask.FromResult($"The supplied string was too long, expected a maximum length of {attribute.MaxLength}."); + } + + return ValueTask.FromResult(null); + } +} diff --git a/DSharpPlus.Commands/Processors/TextCommands/ContextChecks/TextMinMaxValueCheck.cs b/DSharpPlus.Commands/Processors/TextCommands/ContextChecks/TextMinMaxValueCheck.cs index e61b91b3d8..f0b37bb826 100644 --- a/DSharpPlus.Commands/Processors/TextCommands/ContextChecks/TextMinMaxValueCheck.cs +++ b/DSharpPlus.Commands/Processors/TextCommands/ContextChecks/TextMinMaxValueCheck.cs @@ -1,68 +1,68 @@ -using System.Threading.Tasks; -using DSharpPlus.Commands.ArgumentModifiers; -using DSharpPlus.Commands.ContextChecks.ParameterChecks; - -namespace DSharpPlus.Commands.Processors.TextCommands.ContextChecks; - -/// -/// Implements MinMaxValueAttribute on text commands. -/// -internal sealed class TextMinMaxValueCheck : IParameterCheck -{ - public ValueTask ExecuteCheckAsync(MinMaxValueAttribute attribute, ParameterCheckInfo info, CommandContext context) - { - if (info.Value is null) - { - // this implies a NVT - return ValueTask.FromResult(null); - } - - if (attribute.MinValue is not null) - { - bool correctlyOrdered = info.Value switch - { - byte => (byte)attribute.MinValue <= (byte)info.Value, - sbyte => (sbyte)attribute.MinValue <= (sbyte)info.Value, - short => (short)attribute.MinValue <= (short)info.Value, - ushort => (ushort)attribute.MinValue <= (ushort)info.Value, - int => (int)attribute.MinValue <= (int)info.Value, - uint => (uint)attribute.MinValue <= (uint)info.Value, - long => (long)attribute.MinValue <= (long)info.Value, - ulong => (ulong)attribute.MinValue <= (ulong)info.Value, - float => (float)attribute.MinValue <= (float)info.Value, - double => (double)attribute.MinValue <= (double)info.Value, - _ => true, - }; - - if (!correctlyOrdered) - { - return ValueTask.FromResult($"The provided value (`{info.Value}`) was less than the minimum value (`{attribute.MinValue}`)."); - } - } - - if (attribute.MaxValue is not null) - { - bool correctlyOrdered = info.Value switch - { - byte => (byte)attribute.MaxValue >= (byte)info.Value, - sbyte => (sbyte)attribute.MaxValue >= (sbyte)info.Value, - short => (short)attribute.MaxValue >= (short)info.Value, - ushort => (ushort)attribute.MaxValue >= (ushort)info.Value, - int => (int)attribute.MaxValue >= (int)info.Value, - uint => (uint)attribute.MaxValue >= (uint)info.Value, - long => (long)attribute.MaxValue >= (long)info.Value, - ulong => (ulong)attribute.MaxValue >= (ulong)info.Value, - float => (float)attribute.MaxValue >= (float)info.Value, - double => (double)attribute.MaxValue >= (double)info.Value, - _ => true, - }; - - if (!correctlyOrdered) - { - return ValueTask.FromResult($"The provided value (`{info.Value}`) was greater than the maximum value (`{attribute.MaxValue}`)."); - } - } - - return ValueTask.FromResult(null); - } -} +using System.Threading.Tasks; +using DSharpPlus.Commands.ArgumentModifiers; +using DSharpPlus.Commands.ContextChecks.ParameterChecks; + +namespace DSharpPlus.Commands.Processors.TextCommands.ContextChecks; + +/// +/// Implements MinMaxValueAttribute on text commands. +/// +internal sealed class TextMinMaxValueCheck : IParameterCheck +{ + public ValueTask ExecuteCheckAsync(MinMaxValueAttribute attribute, ParameterCheckInfo info, CommandContext context) + { + if (info.Value is null) + { + // this implies a NVT + return ValueTask.FromResult(null); + } + + if (attribute.MinValue is not null) + { + bool correctlyOrdered = info.Value switch + { + byte => (byte)attribute.MinValue <= (byte)info.Value, + sbyte => (sbyte)attribute.MinValue <= (sbyte)info.Value, + short => (short)attribute.MinValue <= (short)info.Value, + ushort => (ushort)attribute.MinValue <= (ushort)info.Value, + int => (int)attribute.MinValue <= (int)info.Value, + uint => (uint)attribute.MinValue <= (uint)info.Value, + long => (long)attribute.MinValue <= (long)info.Value, + ulong => (ulong)attribute.MinValue <= (ulong)info.Value, + float => (float)attribute.MinValue <= (float)info.Value, + double => (double)attribute.MinValue <= (double)info.Value, + _ => true, + }; + + if (!correctlyOrdered) + { + return ValueTask.FromResult($"The provided value (`{info.Value}`) was less than the minimum value (`{attribute.MinValue}`)."); + } + } + + if (attribute.MaxValue is not null) + { + bool correctlyOrdered = info.Value switch + { + byte => (byte)attribute.MaxValue >= (byte)info.Value, + sbyte => (sbyte)attribute.MaxValue >= (sbyte)info.Value, + short => (short)attribute.MaxValue >= (short)info.Value, + ushort => (ushort)attribute.MaxValue >= (ushort)info.Value, + int => (int)attribute.MaxValue >= (int)info.Value, + uint => (uint)attribute.MaxValue >= (uint)info.Value, + long => (long)attribute.MaxValue >= (long)info.Value, + ulong => (ulong)attribute.MaxValue >= (ulong)info.Value, + float => (float)attribute.MaxValue >= (float)info.Value, + double => (double)attribute.MaxValue >= (double)info.Value, + _ => true, + }; + + if (!correctlyOrdered) + { + return ValueTask.FromResult($"The provided value (`{info.Value}`) was greater than the maximum value (`{attribute.MaxValue}`)."); + } + } + + return ValueTask.FromResult(null); + } +} diff --git a/DSharpPlus.Commands/Processors/TextCommands/ConverterRequiresText.cs b/DSharpPlus.Commands/Processors/TextCommands/ConverterRequiresText.cs index 96a0ed8c79..a14fed19e7 100644 --- a/DSharpPlus.Commands/Processors/TextCommands/ConverterRequiresText.cs +++ b/DSharpPlus.Commands/Processors/TextCommands/ConverterRequiresText.cs @@ -1,27 +1,27 @@ -namespace DSharpPlus.Commands.Processors.TextCommands; - -/// -/// The requirements for a converter to require a text argument. -/// -public enum ConverterInputType -{ - /// - /// The converter does not require a text argument. - /// - Never = 0, - - /// - /// The converter will always require a text argument. - /// - Always, - - /// - /// The converter will require a text argument when a reply is missing. - /// - IfReplyMissing, - - /// - /// The converter will require a text argument when a reply is present. - /// - IfReplyPresent -} +namespace DSharpPlus.Commands.Processors.TextCommands; + +/// +/// The requirements for a converter to require a text argument. +/// +public enum ConverterInputType +{ + /// + /// The converter does not require a text argument. + /// + Never = 0, + + /// + /// The converter will always require a text argument. + /// + Always, + + /// + /// The converter will require a text argument when a reply is missing. + /// + IfReplyMissing, + + /// + /// The converter will require a text argument when a reply is present. + /// + IfReplyPresent +} diff --git a/DSharpPlus.Commands/Processors/TextCommands/ITextArgumentConverter.cs b/DSharpPlus.Commands/Processors/TextCommands/ITextArgumentConverter.cs index aa8d78c4f9..05b54966f8 100644 --- a/DSharpPlus.Commands/Processors/TextCommands/ITextArgumentConverter.cs +++ b/DSharpPlus.Commands/Processors/TextCommands/ITextArgumentConverter.cs @@ -1,10 +1,10 @@ -using DSharpPlus.Commands.Converters; - -namespace DSharpPlus.Commands.Processors.TextCommands; - -public interface ITextArgumentConverter : IArgumentConverter -{ - public ConverterInputType RequiresText { get; } -} - -public interface ITextArgumentConverter : ITextArgumentConverter, IArgumentConverter; +using DSharpPlus.Commands.Converters; + +namespace DSharpPlus.Commands.Processors.TextCommands; + +public interface ITextArgumentConverter : IArgumentConverter +{ + public ConverterInputType RequiresText { get; } +} + +public interface ITextArgumentConverter : ITextArgumentConverter, IArgumentConverter; diff --git a/DSharpPlus.Commands/Processors/TextCommands/Parsing/DefaultPrefixResolver.cs b/DSharpPlus.Commands/Processors/TextCommands/Parsing/DefaultPrefixResolver.cs index 3cb3403580..ce9b992d40 100644 --- a/DSharpPlus.Commands/Processors/TextCommands/Parsing/DefaultPrefixResolver.cs +++ b/DSharpPlus.Commands/Processors/TextCommands/Parsing/DefaultPrefixResolver.cs @@ -1,61 +1,61 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using DSharpPlus.Entities; - -namespace DSharpPlus.Commands.Processors.TextCommands.Parsing; - -public delegate ValueTask ResolvePrefixDelegateAsync(CommandsExtension extension, DiscordMessage message); - -public sealed class DefaultPrefixResolver : IPrefixResolver -{ - /// - /// Prefixes which will trigger command execution - /// - public string[] Prefixes { get; init; } - - /// - /// Setting if a mention will trigger command execution - /// - public bool AllowMention { get; init; } - - /// - /// Default prefix resolver - /// - /// Set wether mentioning the bot will count as a prefix - /// A list of prefixes which will trigger commands - /// Is thrown when no prefix is provided or any prefix is null or whitespace only - public DefaultPrefixResolver(bool allowMention, params string[] prefix) - { - if (prefix.Length == 0 || prefix.Any(string.IsNullOrWhiteSpace)) - { - throw new ArgumentException("Prefix cannot be null or whitespace.", nameof(prefix)); - } - - this.AllowMention = allowMention; - this.Prefixes = prefix; - } - - public ValueTask ResolvePrefixAsync(CommandsExtension extension, DiscordMessage message) - { - if (string.IsNullOrWhiteSpace(message.Content)) - { - return ValueTask.FromResult(-1); - } - // Mention check - else if (this.AllowMention && message.Content.StartsWith(extension.Client.CurrentUser.Mention, StringComparison.OrdinalIgnoreCase)) - { - return ValueTask.FromResult(extension.Client.CurrentUser.Mention.Length); - } - - foreach (string prefix in this.Prefixes) - { - if (message.Content.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) - { - return ValueTask.FromResult(prefix.Length); - } - } - - return ValueTask.FromResult(-1); - } -} +using System; +using System.Linq; +using System.Threading.Tasks; +using DSharpPlus.Entities; + +namespace DSharpPlus.Commands.Processors.TextCommands.Parsing; + +public delegate ValueTask ResolvePrefixDelegateAsync(CommandsExtension extension, DiscordMessage message); + +public sealed class DefaultPrefixResolver : IPrefixResolver +{ + /// + /// Prefixes which will trigger command execution + /// + public string[] Prefixes { get; init; } + + /// + /// Setting if a mention will trigger command execution + /// + public bool AllowMention { get; init; } + + /// + /// Default prefix resolver + /// + /// Set wether mentioning the bot will count as a prefix + /// A list of prefixes which will trigger commands + /// Is thrown when no prefix is provided or any prefix is null or whitespace only + public DefaultPrefixResolver(bool allowMention, params string[] prefix) + { + if (prefix.Length == 0 || prefix.Any(string.IsNullOrWhiteSpace)) + { + throw new ArgumentException("Prefix cannot be null or whitespace.", nameof(prefix)); + } + + this.AllowMention = allowMention; + this.Prefixes = prefix; + } + + public ValueTask ResolvePrefixAsync(CommandsExtension extension, DiscordMessage message) + { + if (string.IsNullOrWhiteSpace(message.Content)) + { + return ValueTask.FromResult(-1); + } + // Mention check + else if (this.AllowMention && message.Content.StartsWith(extension.Client.CurrentUser.Mention, StringComparison.OrdinalIgnoreCase)) + { + return ValueTask.FromResult(extension.Client.CurrentUser.Mention.Length); + } + + foreach (string prefix in this.Prefixes) + { + if (message.Content.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + { + return ValueTask.FromResult(prefix.Length); + } + } + + return ValueTask.FromResult(-1); + } +} diff --git a/DSharpPlus.Commands/Processors/TextCommands/Parsing/DefaultTextArgumentSplicer.cs b/DSharpPlus.Commands/Processors/TextCommands/Parsing/DefaultTextArgumentSplicer.cs index 1eb3bcb479..59da0f24cc 100644 --- a/DSharpPlus.Commands/Processors/TextCommands/Parsing/DefaultTextArgumentSplicer.cs +++ b/DSharpPlus.Commands/Processors/TextCommands/Parsing/DefaultTextArgumentSplicer.cs @@ -1,119 +1,119 @@ -using System; -using System.Linq; -using System.Text; - -namespace DSharpPlus.Commands.Processors.TextCommands.Parsing; - -public delegate string? TextArgumentSplicer(CommandsExtension extension, string text, ref int startAt); - -public class DefaultTextArgumentSplicer -{ - private enum TextState - { - None, - InBacktick, - InTripleBacktick, - InQuote - } - - public static string? Splice(CommandsExtension extension, string text, ref int startAt) - { - // We do this for no parameter overloads such as HelloWorldAsync(CommandContext context) - if (string.IsNullOrWhiteSpace(text) || startAt >= text.Length) - { - return null; - } - - int i; - char quotedCharacter = default; - TextState state = TextState.None; - StringBuilder result = new(); - ReadOnlySpan textSpan = text.AsSpan(); - char[] quoteCharacters = extension.GetProcessor().Configuration.QuoteCharacters; - for (i = startAt; i < textSpan.Length; i++) - { - char character = textSpan[i]; - if (state == TextState.None) - { - if (IsEscaped(textSpan, i)) - { - result.Append(textSpan[++i]); - continue; - } - else if (char.IsWhiteSpace(character)) - { - // Skip beginning whitespace - if (result.Length == 0) - { - continue; - } - - // End of argument - break; - } - else if (IsQuoted(textSpan, i, quoteCharacters)) - { - state = TextState.InQuote; - quotedCharacter = character; - continue; - } - else if (character == '`') - { - if (IsTripleBacktick(textSpan, i)) - { - i += 2; - result.Append("```"); - state = TextState.InTripleBacktick; - continue; - } - - state = TextState.InBacktick; - } - } - else if (state == TextState.InTripleBacktick && IsTripleBacktick(textSpan, i)) - { - i += 3; - result.Append("```"); - break; - } - else if (state == TextState.InBacktick && character == '`') - { - state = TextState.None; - } - else if (state == TextState.InQuote) - { - if (IsEscaped(textSpan, i)) - { - result.Append(textSpan[++i]); - continue; - } - else if (character == quotedCharacter) - { - state = TextState.None; - i++; - break; - } - } - - result.Append(character); - } - - if (state == TextState.InQuote) - { - // Prepend the quoted character - result.Insert(0, quotedCharacter); - } - - if (result.Length == 0) - { - return null; - } - - startAt = i; - return result.ToString(); - } - - private static bool IsTripleBacktick(ReadOnlySpan text, int index) => index + 2 < text.Length && text[index] == '`' && text[index + 1] == '`' && text[index + 2] == '`'; - private static bool IsEscaped(ReadOnlySpan text, int index) => index + 1 < text.Length && text[index] == '\\'; - private static bool IsQuoted(ReadOnlySpan text, int index, char[] quoteCharacters) => quoteCharacters.Contains(text[index]) && (index == 0 || char.IsWhiteSpace(text[index - 1])); -} +using System; +using System.Linq; +using System.Text; + +namespace DSharpPlus.Commands.Processors.TextCommands.Parsing; + +public delegate string? TextArgumentSplicer(CommandsExtension extension, string text, ref int startAt); + +public class DefaultTextArgumentSplicer +{ + private enum TextState + { + None, + InBacktick, + InTripleBacktick, + InQuote + } + + public static string? Splice(CommandsExtension extension, string text, ref int startAt) + { + // We do this for no parameter overloads such as HelloWorldAsync(CommandContext context) + if (string.IsNullOrWhiteSpace(text) || startAt >= text.Length) + { + return null; + } + + int i; + char quotedCharacter = default; + TextState state = TextState.None; + StringBuilder result = new(); + ReadOnlySpan textSpan = text.AsSpan(); + char[] quoteCharacters = extension.GetProcessor().Configuration.QuoteCharacters; + for (i = startAt; i < textSpan.Length; i++) + { + char character = textSpan[i]; + if (state == TextState.None) + { + if (IsEscaped(textSpan, i)) + { + result.Append(textSpan[++i]); + continue; + } + else if (char.IsWhiteSpace(character)) + { + // Skip beginning whitespace + if (result.Length == 0) + { + continue; + } + + // End of argument + break; + } + else if (IsQuoted(textSpan, i, quoteCharacters)) + { + state = TextState.InQuote; + quotedCharacter = character; + continue; + } + else if (character == '`') + { + if (IsTripleBacktick(textSpan, i)) + { + i += 2; + result.Append("```"); + state = TextState.InTripleBacktick; + continue; + } + + state = TextState.InBacktick; + } + } + else if (state == TextState.InTripleBacktick && IsTripleBacktick(textSpan, i)) + { + i += 3; + result.Append("```"); + break; + } + else if (state == TextState.InBacktick && character == '`') + { + state = TextState.None; + } + else if (state == TextState.InQuote) + { + if (IsEscaped(textSpan, i)) + { + result.Append(textSpan[++i]); + continue; + } + else if (character == quotedCharacter) + { + state = TextState.None; + i++; + break; + } + } + + result.Append(character); + } + + if (state == TextState.InQuote) + { + // Prepend the quoted character + result.Insert(0, quotedCharacter); + } + + if (result.Length == 0) + { + return null; + } + + startAt = i; + return result.ToString(); + } + + private static bool IsTripleBacktick(ReadOnlySpan text, int index) => index + 2 < text.Length && text[index] == '`' && text[index + 1] == '`' && text[index + 2] == '`'; + private static bool IsEscaped(ReadOnlySpan text, int index) => index + 1 < text.Length && text[index] == '\\'; + private static bool IsQuoted(ReadOnlySpan text, int index, char[] quoteCharacters) => quoteCharacters.Contains(text[index]) && (index == 0 || char.IsWhiteSpace(text[index - 1])); +} diff --git a/DSharpPlus.Commands/Processors/TextCommands/Parsing/IPrefixResolver.cs b/DSharpPlus.Commands/Processors/TextCommands/Parsing/IPrefixResolver.cs index c829907bc5..d0e0c6122e 100644 --- a/DSharpPlus.Commands/Processors/TextCommands/Parsing/IPrefixResolver.cs +++ b/DSharpPlus.Commands/Processors/TextCommands/Parsing/IPrefixResolver.cs @@ -1,18 +1,18 @@ -using System.Threading.Tasks; -using DSharpPlus.Entities; - -namespace DSharpPlus.Commands.Processors.TextCommands.Parsing; - -/// -/// Represents a resolver for command prefixes. -/// -public interface IPrefixResolver -{ - /// - /// Resolves the prefix for the command. - /// - /// The commands extension. - /// The message to resolve the prefix for. - /// An integer representing the length of the prefix. - public ValueTask ResolvePrefixAsync(CommandsExtension extension, DiscordMessage message); -} +using System.Threading.Tasks; +using DSharpPlus.Entities; + +namespace DSharpPlus.Commands.Processors.TextCommands.Parsing; + +/// +/// Represents a resolver for command prefixes. +/// +public interface IPrefixResolver +{ + /// + /// Resolves the prefix for the command. + /// + /// The commands extension. + /// The message to resolve the prefix for. + /// An integer representing the length of the prefix. + public ValueTask ResolvePrefixAsync(CommandsExtension extension, DiscordMessage message); +} diff --git a/DSharpPlus.Commands/Processors/TextCommands/TextCommandConfiguration.cs b/DSharpPlus.Commands/Processors/TextCommands/TextCommandConfiguration.cs index 7a3c2662e6..6a4568d2e5 100644 --- a/DSharpPlus.Commands/Processors/TextCommands/TextCommandConfiguration.cs +++ b/DSharpPlus.Commands/Processors/TextCommands/TextCommandConfiguration.cs @@ -1,29 +1,29 @@ -using System; -using System.Collections.Generic; -using DSharpPlus.Commands.Processors.TextCommands.Parsing; - -namespace DSharpPlus.Commands.Processors.TextCommands; - -public record TextCommandConfiguration -{ - /// - /// The function to use to resolve prefixes for commands. - /// - /// For dynamic prefix resolving, registered to the 's should be preferred. - public ResolvePrefixDelegateAsync PrefixResolver { get; init; } = new DefaultPrefixResolver(true, "!").ResolvePrefixAsync; - public TextArgumentSplicer TextArgumentSplicer { get; init; } = DefaultTextArgumentSplicer.Splice; - public char[] QuoteCharacters { get; init; } = ['"', '\'', '«', '»', '‘', '“', '„', '‟']; - public bool IgnoreBots { get; init; } = true; - - /// - /// Disables the exception thrown when a command is not found. - /// - public bool EnableCommandNotFoundException { get; init; } - - /// - /// Whether to suppress the missing message content intent warning. - /// - public bool SuppressMissingMessageContentIntentWarning { get; set; } - - public IEqualityComparer CommandNameComparer { get; init; } = StringComparer.OrdinalIgnoreCase; -} +using System; +using System.Collections.Generic; +using DSharpPlus.Commands.Processors.TextCommands.Parsing; + +namespace DSharpPlus.Commands.Processors.TextCommands; + +public record TextCommandConfiguration +{ + /// + /// The function to use to resolve prefixes for commands. + /// + /// For dynamic prefix resolving, registered to the 's should be preferred. + public ResolvePrefixDelegateAsync PrefixResolver { get; init; } = new DefaultPrefixResolver(true, "!").ResolvePrefixAsync; + public TextArgumentSplicer TextArgumentSplicer { get; init; } = DefaultTextArgumentSplicer.Splice; + public char[] QuoteCharacters { get; init; } = ['"', '\'', '«', '»', '‘', '“', '„', '‟']; + public bool IgnoreBots { get; init; } = true; + + /// + /// Disables the exception thrown when a command is not found. + /// + public bool EnableCommandNotFoundException { get; init; } + + /// + /// Whether to suppress the missing message content intent warning. + /// + public bool SuppressMissingMessageContentIntentWarning { get; set; } + + public IEqualityComparer CommandNameComparer { get; init; } = StringComparer.OrdinalIgnoreCase; +} diff --git a/DSharpPlus.Commands/Processors/TextCommands/TextCommandContext.cs b/DSharpPlus.Commands/Processors/TextCommands/TextCommandContext.cs index 8489cf27b6..282f0f5249 100644 --- a/DSharpPlus.Commands/Processors/TextCommands/TextCommandContext.cs +++ b/DSharpPlus.Commands/Processors/TextCommands/TextCommandContext.cs @@ -1,149 +1,149 @@ -using System; -using System.Threading.Tasks; -using DSharpPlus.Entities; - -namespace DSharpPlus.Commands.Processors.TextCommands; - -public sealed record TextCommandContext : CommandContext -{ - public required DiscordMessage Message { get; init; } - public required int PrefixLength { internal get; init; } - public string? Prefix => this.Message.Content?[..this.PrefixLength]; - public DiscordMessage? Response { get; private set; } - public bool Delayed { get; private set; } - - /// - public override async ValueTask RespondAsync(IDiscordMessageBuilder builder) - { - DiscordMessageBuilder messageBuilder = new(builder); - - // Reply to the message that invoked the command if no reply is set - if (messageBuilder.ReplyId is null) - { - messageBuilder.WithReply(this.Message.Id); - } - - // Don't ping anyone if no mentions are explicitly set - if (messageBuilder.Mentions?.Count is null or 0) - { - messageBuilder.WithAllowedMentions(Mentions.None); - } - - this.Response = await this.Channel.SendMessageAsync(messageBuilder); - } - - /// - public override async ValueTask EditResponseAsync(IDiscordMessageBuilder builder) - { - if (this.Response is not null) - { - this.Response = await this.Response.ModifyAsync(new DiscordMessageBuilder(builder)); - } - else if (this.Delayed) - { - await RespondAsync(builder); - } - else - { - throw new InvalidOperationException("Cannot edit a response that has not been sent yet."); - } - - return this.Response!; - } - - /// - public override async ValueTask DeleteResponseAsync() - { - if (this.Response is null) - { - throw new InvalidOperationException("Cannot delete a response that has not been sent yet."); - } - - await this.Response.DeleteAsync(); - } - - /// - public override ValueTask GetResponseAsync() => ValueTask.FromResult(this.Response); - - /// - public override async ValueTask DeferResponseAsync() - { - await this.Channel.TriggerTypingAsync(); - this.Delayed = true; - } - - public override async ValueTask FollowupAsync(IDiscordMessageBuilder builder) - { - if (this.Response is null) - { - throw new InvalidOperationException("Cannot send a followup message before the initial response."); - } - - DiscordMessageBuilder messageBuilder = new(builder); - - // Reply to the original message if no reply is set, to indicate that this message is related to the command - if (messageBuilder.ReplyId is null) - { - messageBuilder.WithReply(this.Response.Id); - } - - // Don't ping anyone if no mentions are explicitly set - if (messageBuilder.Mentions?.Count is null or 0) - { - messageBuilder.WithAllowedMentions(Mentions.None); - } - - DiscordMessage followup = await this.Channel.SendMessageAsync(messageBuilder); - this.followupMessages.Add(followup.Id, followup); - return followup; - } - - public override async ValueTask EditFollowupAsync(ulong messageId, IDiscordMessageBuilder builder) - { - if (this.Response is null) - { - throw new InvalidOperationException("Cannot edit a followup message before the initial response."); - } - - if (!this.followupMessages.TryGetValue(messageId, out DiscordMessage? message)) - { - throw new InvalidOperationException("Cannot edit a followup message that does not exist."); - } - - DiscordMessageBuilder messageBuilder = new(builder); - this.followupMessages[messageId] = await message.ModifyAsync(messageBuilder); - return this.followupMessages[messageId]; - } - - public override async ValueTask GetFollowupAsync(ulong messageId, bool ignoreCache = false) - { - if (this.Response is null) - { - throw new InvalidOperationException("Cannot get a followup message before the initial response."); - } - - // Fetch the follow up message if we don't have it cached. - if (ignoreCache || !this.followupMessages.TryGetValue(messageId, out DiscordMessage? message)) - { - message = await this.Channel.GetMessageAsync(messageId, true); - this.followupMessages[messageId] = message; - } - - return message; - } - - public override async ValueTask DeleteFollowupAsync(ulong messageId) - { - if (this.Response is null) - { - throw new InvalidOperationException("Cannot delete a followup message before the initial response."); - } - - if (!this.followupMessages.TryGetValue(messageId, out DiscordMessage? message)) - { - throw new InvalidOperationException("Cannot delete a followup message that does not exist."); - } - - await message.DeleteAsync(); - } -} +using System; +using System.Threading.Tasks; +using DSharpPlus.Entities; + +namespace DSharpPlus.Commands.Processors.TextCommands; + +public sealed record TextCommandContext : CommandContext +{ + public required DiscordMessage Message { get; init; } + public required int PrefixLength { internal get; init; } + public string? Prefix => this.Message.Content?[..this.PrefixLength]; + public DiscordMessage? Response { get; private set; } + public bool Delayed { get; private set; } + + /// + public override async ValueTask RespondAsync(IDiscordMessageBuilder builder) + { + DiscordMessageBuilder messageBuilder = new(builder); + + // Reply to the message that invoked the command if no reply is set + if (messageBuilder.ReplyId is null) + { + messageBuilder.WithReply(this.Message.Id); + } + + // Don't ping anyone if no mentions are explicitly set + if (messageBuilder.Mentions?.Count is null or 0) + { + messageBuilder.WithAllowedMentions(Mentions.None); + } + + this.Response = await this.Channel.SendMessageAsync(messageBuilder); + } + + /// + public override async ValueTask EditResponseAsync(IDiscordMessageBuilder builder) + { + if (this.Response is not null) + { + this.Response = await this.Response.ModifyAsync(new DiscordMessageBuilder(builder)); + } + else if (this.Delayed) + { + await RespondAsync(builder); + } + else + { + throw new InvalidOperationException("Cannot edit a response that has not been sent yet."); + } + + return this.Response!; + } + + /// + public override async ValueTask DeleteResponseAsync() + { + if (this.Response is null) + { + throw new InvalidOperationException("Cannot delete a response that has not been sent yet."); + } + + await this.Response.DeleteAsync(); + } + + /// + public override ValueTask GetResponseAsync() => ValueTask.FromResult(this.Response); + + /// + public override async ValueTask DeferResponseAsync() + { + await this.Channel.TriggerTypingAsync(); + this.Delayed = true; + } + + public override async ValueTask FollowupAsync(IDiscordMessageBuilder builder) + { + if (this.Response is null) + { + throw new InvalidOperationException("Cannot send a followup message before the initial response."); + } + + DiscordMessageBuilder messageBuilder = new(builder); + + // Reply to the original message if no reply is set, to indicate that this message is related to the command + if (messageBuilder.ReplyId is null) + { + messageBuilder.WithReply(this.Response.Id); + } + + // Don't ping anyone if no mentions are explicitly set + if (messageBuilder.Mentions?.Count is null or 0) + { + messageBuilder.WithAllowedMentions(Mentions.None); + } + + DiscordMessage followup = await this.Channel.SendMessageAsync(messageBuilder); + this.followupMessages.Add(followup.Id, followup); + return followup; + } + + public override async ValueTask EditFollowupAsync(ulong messageId, IDiscordMessageBuilder builder) + { + if (this.Response is null) + { + throw new InvalidOperationException("Cannot edit a followup message before the initial response."); + } + + if (!this.followupMessages.TryGetValue(messageId, out DiscordMessage? message)) + { + throw new InvalidOperationException("Cannot edit a followup message that does not exist."); + } + + DiscordMessageBuilder messageBuilder = new(builder); + this.followupMessages[messageId] = await message.ModifyAsync(messageBuilder); + return this.followupMessages[messageId]; + } + + public override async ValueTask GetFollowupAsync(ulong messageId, bool ignoreCache = false) + { + if (this.Response is null) + { + throw new InvalidOperationException("Cannot get a followup message before the initial response."); + } + + // Fetch the follow up message if we don't have it cached. + if (ignoreCache || !this.followupMessages.TryGetValue(messageId, out DiscordMessage? message)) + { + message = await this.Channel.GetMessageAsync(messageId, true); + this.followupMessages[messageId] = message; + } + + return message; + } + + public override async ValueTask DeleteFollowupAsync(ulong messageId) + { + if (this.Response is null) + { + throw new InvalidOperationException("Cannot delete a followup message before the initial response."); + } + + if (!this.followupMessages.TryGetValue(messageId, out DiscordMessage? message)) + { + throw new InvalidOperationException("Cannot delete a followup message that does not exist."); + } + + await message.DeleteAsync(); + } +} diff --git a/DSharpPlus.Commands/Processors/TextCommands/TextCommandProcessor.cs b/DSharpPlus.Commands/Processors/TextCommands/TextCommandProcessor.cs index c9ebb39315..c9986af361 100644 --- a/DSharpPlus.Commands/Processors/TextCommands/TextCommandProcessor.cs +++ b/DSharpPlus.Commands/Processors/TextCommands/TextCommandProcessor.cs @@ -1,377 +1,377 @@ -using System; -using System.Collections.Frozen; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Threading.Tasks; -using DSharpPlus.Commands.Converters.Results; -using DSharpPlus.Commands.EventArgs; -using DSharpPlus.Commands.Exceptions; -using DSharpPlus.Commands.Processors.TextCommands.Parsing; -using DSharpPlus.Commands.Trees; -using DSharpPlus.Commands.Trees.Metadata; -using DSharpPlus.Entities; -using DSharpPlus.EventArgs; -using Microsoft.Extensions.DependencyInjection; - -namespace DSharpPlus.Commands.Processors.TextCommands; - -public sealed class TextCommandProcessor : BaseCommandProcessor -{ - public const DiscordIntents RequiredIntents = - DiscordIntents.DirectMessages // Required for commands executed in DMs - | DiscordIntents.GuildMessages; // Required for commands that are executed via bot ping - - public TextCommandConfiguration Configuration { get; init; } - - public override IReadOnlyList Commands => this.commands.Values; - private FrozenDictionary commands = FrozenDictionary.Empty; - - /// - /// Creates a new instance of with the default configuration. - /// - public TextCommandProcessor() : this(new TextCommandConfiguration()) { } - - /// - /// Creates a new instance of with the specified configuration. - /// - /// The configuration to use with this processor. - public TextCommandProcessor(TextCommandConfiguration configuration) => this.Configuration = configuration; - - /// - public override async ValueTask ConfigureAsync(CommandsExtension extension) - { - Dictionary textCommands = []; - foreach (Command command in extension.GetCommandsForProcessor(this)) - { - textCommands.Add(command.Name, command); - } - - this.commands = textCommands.ToFrozenDictionary(this.Configuration.CommandNameComparer); - if (this.extension is null) - { - // Put these logs here so that they only appear when the processor is configured the first time. - if (!extension.Client.Intents.HasIntent(DiscordIntents.GuildMessages) && !extension.Client.Intents.HasIntent(DiscordIntents.DirectMessages)) - { - TextLogging.missingRequiredIntents(this.logger, RequiredIntents, null); - } - else if (!extension.Client.Intents.HasIntent(DiscordIntents.MessageContents) && !this.Configuration.SuppressMissingMessageContentIntentWarning) - { - TextLogging.missingMessageContentIntent(this.logger, null); - } - } - - await base.ConfigureAsync(extension); - } - - public async Task ExecuteTextCommandAsync(DiscordClient client, MessageCreatedEventArgs eventArgs) - { - if (this.extension is null) - { - throw new InvalidOperationException("TextCommandProcessor has not been configured."); - } - else if (string.IsNullOrWhiteSpace(eventArgs.Message.Content) - || (eventArgs.Author.IsBot && this.Configuration.IgnoreBots) - || (this.extension.DebugGuildId != 0 && this.extension.DebugGuildId != eventArgs.Guild?.Id)) - { - return; - } - - AsyncServiceScope serviceScope = this.extension.ServiceProvider.CreateAsyncScope(); - ResolvePrefixDelegateAsync resolvePrefix = serviceScope.ServiceProvider.GetService() is IPrefixResolver prefixResolver - ? prefixResolver.ResolvePrefixAsync - : this.Configuration.PrefixResolver; - - int prefixLength = await resolvePrefix(this.extension, eventArgs.Message); - if (prefixLength < 0) - { - return; - } - - // Remove the prefix - string commandText = eventArgs.Message.Content[prefixLength..].TrimStart(); - - // Parse the full command name - if (!TryGetCommand(commandText, eventArgs.Guild?.Id ?? 0, out int index, out Command? command)) - { - if (this.Configuration!.EnableCommandNotFoundException) - { - await this.extension.commandErrored.InvokeAsync( - this.extension, - new CommandErroredEventArgs() - { - Context = new TextCommandContext() - { - Arguments = new Dictionary(), - Channel = eventArgs.Channel, - Command = null!, - Extension = this.extension, - Message = eventArgs.Message, - PrefixLength = prefixLength, - ServiceScope = serviceScope, - User = eventArgs.Author, - }, - Exception = new CommandNotFoundException(commandText[..index]), - CommandObject = null, - } - ); - } - - await serviceScope.DisposeAsync(); - return; - } - - // If this is a group command, try to see if it's executable. - if (command.Method is null) - { - Command? defaultGroupCommand = command.Subcommands.FirstOrDefault(subcommand => subcommand.Attributes.OfType().Any()); - if (defaultGroupCommand is null) - { - if (this.Configuration!.EnableCommandNotFoundException) - { - await this.extension.commandErrored.InvokeAsync(this.extension, new CommandErroredEventArgs() - { - Context = new TextCommandContext() - { - Arguments = new Dictionary(), - Channel = eventArgs.Channel, - Command = command, - Extension = this.extension, - Message = eventArgs.Message, - PrefixLength = prefixLength, - ServiceScope = serviceScope, - User = eventArgs.Author, - }, - Exception = new CommandNotExecutableException(command, "Unable to execute a command that has no method. Is this command a group command?"), - CommandObject = null, - } - ); - } - - await serviceScope.DisposeAsync(); - return; - } - - command = defaultGroupCommand; - } - - TextConverterContext converterContext = new() - { - Channel = eventArgs.Channel, - Command = command, - Extension = this.extension, - Message = eventArgs.Message, - RawArguments = commandText[index..], - PrefixLength = prefixLength, - ServiceScope = serviceScope, - Splicer = this.Configuration.TextArgumentSplicer, - User = eventArgs.Author, - }; - - IReadOnlyDictionary parsedArguments = await ParseParametersAsync(converterContext); - TextCommandContext commandContext = CreateCommandContext(converterContext, parsedArguments); - - // Iterate over all arguments and check if any of them failed to parse. - foreach (KeyValuePair argument in parsedArguments) - { - if (argument.Value is ArgumentFailedConversionResult || argument.Value is Optional) - { - ArgumentFailedConversionResult? argumentFailedConversionResultValue = null; - if (argument.Value is ArgumentFailedConversionResult argumentFailedConversionResult) - { - argumentFailedConversionResultValue = argumentFailedConversionResult; - } - else if (argument.Value is Optional optionalArgumentFailedConversionResult && optionalArgumentFailedConversionResult.HasValue) - { - argumentFailedConversionResultValue = optionalArgumentFailedConversionResult.Value; - } - - await this.extension.commandErrored.InvokeAsync(this.extension, new CommandErroredEventArgs() - { - Context = commandContext, - CommandObject = null, - Exception = new ArgumentParseException(argument.Key, argumentFailedConversionResultValue), - }); - - await serviceScope.DisposeAsync(); - return; - } - else if (argument.Value is ArgumentNotParsedResult || argument.Value is Optional) - { - await this.extension.commandErrored.InvokeAsync(this.extension, new CommandErroredEventArgs() - { - Context = commandContext, - CommandObject = null, - Exception = new ArgumentParseException(argument.Key, null, "An earlier argument failed to parse, causing this argument to not be parsed."), - }); - - await serviceScope.DisposeAsync(); - return; - } - } - - await this.extension.CommandExecutor.ExecuteAsync(commandContext); - } - - public override TextCommandContext CreateCommandContext(TextConverterContext converterContext, IReadOnlyDictionary parsedArguments) - { - return new() - { - Arguments = parsedArguments, - Channel = converterContext.Channel, - Command = converterContext.Command, - Extension = this.extension ?? throw new InvalidOperationException("TextCommandProcessor has not been configured."), - Message = converterContext.Message, - PrefixLength = converterContext.PrefixLength, - ServiceScope = converterContext.ServiceScope, - User = converterContext.User, - }; - } - - /// - protected override async ValueTask ExecuteConverterAsync(ITextArgumentConverter converter, TextConverterContext context) - { - // Store the current argument index to restore it later. - int currentArgumentIndex = context.CurrentArgumentIndex; - - // Switch to the original message before checking if it has a reply. - context.SwitchToReply(false); - if (converter.RequiresText is ConverterInputType.Never - || (converter.RequiresText is ConverterInputType.IfReplyPresent && context.Message.ReferencedMessage is null) - || (converter.RequiresText is ConverterInputType.IfReplyMissing && context.Message.ReferencedMessage is not null)) - { - // Go to the previous argument if the converter does not require text. - context.CurrentArgumentIndex = -1; - } - - // Execute the converter - IOptional value = await base.ExecuteConverterAsync(converter, context); - - // Restore the current argument index - context.CurrentArgumentIndex = currentArgumentIndex; - - // Return the result - return value; - } - - /// - /// Attempts to retrieve a command from the provided command text. Searches for the command by name, then by alias. Subcommands are also resolved. - /// This method ignores 's and will instead return the group command instead of the default subcommand. - /// - /// The full command name and optionally it's arguments. - /// The guild ID to check if the command is available in the guild. Pass 0 if not applicable. - /// The index of that the command name ends at. - /// The resolved command. - /// If the command was found. - public bool TryGetCommand(string commandText, ulong guildId, out int index, [NotNullWhen(true)] out Command? command) - { - // Declare the index here for scope, keep reading until a whitespace character is found. - for (index = 0; index < commandText.Length; index++) - { - if (char.IsWhiteSpace(commandText[index])) - { - break; - } - } - - string rootCommandText = commandText[..index]; - if (!this.commands.TryGetValue(rootCommandText, out command)) - { - // Search for any aliases - foreach (Command officialCommand in this.commands.Values) - { - TextAliasAttribute? aliasAttribute = officialCommand.Attributes.OfType().FirstOrDefault(); - if (aliasAttribute is not null && aliasAttribute.Aliases.Any(alias => this.Configuration.CommandNameComparer.Equals(alias, rootCommandText))) - { - command = officialCommand; - break; - } - } - } - - // No alias was found - if (command is null || (command.GuildIds.Count > 0 && !command.GuildIds.Contains(guildId))) - { - return false; - } - - // If there is a space after the command's name, skip it. - if (index < commandText.Length && commandText[index] == ' ') - { - index++; - } - - // Recursively resolve subcommands - int nextIndex = index; - while (nextIndex != -1) - { - // If the index is at the end of the string, break - if (nextIndex >= commandText.Length) - { - break; - } - - // If there was no space found after the subcommand, break - nextIndex = commandText.IndexOf(' ', nextIndex + 1); - if (nextIndex == -1) - { - // No more spaces. Search the rest of the string to see if there is a subcommand that matches. - nextIndex = commandText.Length; - } - - // Resolve subcommands - string subcommandName = commandText[index..nextIndex]; - - // Try searching for the subcommand by name, then by alias - // We prioritize the name over the aliases to avoid a poor dev debugging experience - Command? foundCommand = command.Subcommands.FirstOrDefault(subcommand => this.Configuration.CommandNameComparer.Equals(subcommand.Name, subcommandName.Trim())); - if (foundCommand is null) - { - // Search for any aliases that the subcommand may have - foreach (Command subcommand in command.Subcommands) - { - foreach (Attribute attribute in subcommand.Attributes) - { - if (attribute is not TextAliasAttribute aliasAttribute) - { - continue; - } - - foreach (string alias in aliasAttribute.Aliases) - { - if (this.Configuration.CommandNameComparer.Equals(alias, subcommandName)) - { - foundCommand = subcommand; - break; - } - } - - if (foundCommand is not null) - { - break; - } - } - - if (foundCommand is not null) - { - break; - } - } - - if (foundCommand is null) - { - // There was no subcommand found by name or by alias. - // Maybe the index is on an argument for the command? - return true; - } - } - - // Try to parse the next subcommand - index = nextIndex; - command = foundCommand; - } - - // We found it!! Good job! - return true; - } -} +using System; +using System.Collections.Frozen; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading.Tasks; +using DSharpPlus.Commands.Converters.Results; +using DSharpPlus.Commands.EventArgs; +using DSharpPlus.Commands.Exceptions; +using DSharpPlus.Commands.Processors.TextCommands.Parsing; +using DSharpPlus.Commands.Trees; +using DSharpPlus.Commands.Trees.Metadata; +using DSharpPlus.Entities; +using DSharpPlus.EventArgs; +using Microsoft.Extensions.DependencyInjection; + +namespace DSharpPlus.Commands.Processors.TextCommands; + +public sealed class TextCommandProcessor : BaseCommandProcessor +{ + public const DiscordIntents RequiredIntents = + DiscordIntents.DirectMessages // Required for commands executed in DMs + | DiscordIntents.GuildMessages; // Required for commands that are executed via bot ping + + public TextCommandConfiguration Configuration { get; init; } + + public override IReadOnlyList Commands => this.commands.Values; + private FrozenDictionary commands = FrozenDictionary.Empty; + + /// + /// Creates a new instance of with the default configuration. + /// + public TextCommandProcessor() : this(new TextCommandConfiguration()) { } + + /// + /// Creates a new instance of with the specified configuration. + /// + /// The configuration to use with this processor. + public TextCommandProcessor(TextCommandConfiguration configuration) => this.Configuration = configuration; + + /// + public override async ValueTask ConfigureAsync(CommandsExtension extension) + { + Dictionary textCommands = []; + foreach (Command command in extension.GetCommandsForProcessor(this)) + { + textCommands.Add(command.Name, command); + } + + this.commands = textCommands.ToFrozenDictionary(this.Configuration.CommandNameComparer); + if (this.extension is null) + { + // Put these logs here so that they only appear when the processor is configured the first time. + if (!extension.Client.Intents.HasIntent(DiscordIntents.GuildMessages) && !extension.Client.Intents.HasIntent(DiscordIntents.DirectMessages)) + { + TextLogging.missingRequiredIntents(this.logger, RequiredIntents, null); + } + else if (!extension.Client.Intents.HasIntent(DiscordIntents.MessageContents) && !this.Configuration.SuppressMissingMessageContentIntentWarning) + { + TextLogging.missingMessageContentIntent(this.logger, null); + } + } + + await base.ConfigureAsync(extension); + } + + public async Task ExecuteTextCommandAsync(DiscordClient client, MessageCreatedEventArgs eventArgs) + { + if (this.extension is null) + { + throw new InvalidOperationException("TextCommandProcessor has not been configured."); + } + else if (string.IsNullOrWhiteSpace(eventArgs.Message.Content) + || (eventArgs.Author.IsBot && this.Configuration.IgnoreBots) + || (this.extension.DebugGuildId != 0 && this.extension.DebugGuildId != eventArgs.Guild?.Id)) + { + return; + } + + AsyncServiceScope serviceScope = this.extension.ServiceProvider.CreateAsyncScope(); + ResolvePrefixDelegateAsync resolvePrefix = serviceScope.ServiceProvider.GetService() is IPrefixResolver prefixResolver + ? prefixResolver.ResolvePrefixAsync + : this.Configuration.PrefixResolver; + + int prefixLength = await resolvePrefix(this.extension, eventArgs.Message); + if (prefixLength < 0) + { + return; + } + + // Remove the prefix + string commandText = eventArgs.Message.Content[prefixLength..].TrimStart(); + + // Parse the full command name + if (!TryGetCommand(commandText, eventArgs.Guild?.Id ?? 0, out int index, out Command? command)) + { + if (this.Configuration!.EnableCommandNotFoundException) + { + await this.extension.commandErrored.InvokeAsync( + this.extension, + new CommandErroredEventArgs() + { + Context = new TextCommandContext() + { + Arguments = new Dictionary(), + Channel = eventArgs.Channel, + Command = null!, + Extension = this.extension, + Message = eventArgs.Message, + PrefixLength = prefixLength, + ServiceScope = serviceScope, + User = eventArgs.Author, + }, + Exception = new CommandNotFoundException(commandText[..index]), + CommandObject = null, + } + ); + } + + await serviceScope.DisposeAsync(); + return; + } + + // If this is a group command, try to see if it's executable. + if (command.Method is null) + { + Command? defaultGroupCommand = command.Subcommands.FirstOrDefault(subcommand => subcommand.Attributes.OfType().Any()); + if (defaultGroupCommand is null) + { + if (this.Configuration!.EnableCommandNotFoundException) + { + await this.extension.commandErrored.InvokeAsync(this.extension, new CommandErroredEventArgs() + { + Context = new TextCommandContext() + { + Arguments = new Dictionary(), + Channel = eventArgs.Channel, + Command = command, + Extension = this.extension, + Message = eventArgs.Message, + PrefixLength = prefixLength, + ServiceScope = serviceScope, + User = eventArgs.Author, + }, + Exception = new CommandNotExecutableException(command, "Unable to execute a command that has no method. Is this command a group command?"), + CommandObject = null, + } + ); + } + + await serviceScope.DisposeAsync(); + return; + } + + command = defaultGroupCommand; + } + + TextConverterContext converterContext = new() + { + Channel = eventArgs.Channel, + Command = command, + Extension = this.extension, + Message = eventArgs.Message, + RawArguments = commandText[index..], + PrefixLength = prefixLength, + ServiceScope = serviceScope, + Splicer = this.Configuration.TextArgumentSplicer, + User = eventArgs.Author, + }; + + IReadOnlyDictionary parsedArguments = await ParseParametersAsync(converterContext); + TextCommandContext commandContext = CreateCommandContext(converterContext, parsedArguments); + + // Iterate over all arguments and check if any of them failed to parse. + foreach (KeyValuePair argument in parsedArguments) + { + if (argument.Value is ArgumentFailedConversionResult || argument.Value is Optional) + { + ArgumentFailedConversionResult? argumentFailedConversionResultValue = null; + if (argument.Value is ArgumentFailedConversionResult argumentFailedConversionResult) + { + argumentFailedConversionResultValue = argumentFailedConversionResult; + } + else if (argument.Value is Optional optionalArgumentFailedConversionResult && optionalArgumentFailedConversionResult.HasValue) + { + argumentFailedConversionResultValue = optionalArgumentFailedConversionResult.Value; + } + + await this.extension.commandErrored.InvokeAsync(this.extension, new CommandErroredEventArgs() + { + Context = commandContext, + CommandObject = null, + Exception = new ArgumentParseException(argument.Key, argumentFailedConversionResultValue), + }); + + await serviceScope.DisposeAsync(); + return; + } + else if (argument.Value is ArgumentNotParsedResult || argument.Value is Optional) + { + await this.extension.commandErrored.InvokeAsync(this.extension, new CommandErroredEventArgs() + { + Context = commandContext, + CommandObject = null, + Exception = new ArgumentParseException(argument.Key, null, "An earlier argument failed to parse, causing this argument to not be parsed."), + }); + + await serviceScope.DisposeAsync(); + return; + } + } + + await this.extension.CommandExecutor.ExecuteAsync(commandContext); + } + + public override TextCommandContext CreateCommandContext(TextConverterContext converterContext, IReadOnlyDictionary parsedArguments) + { + return new() + { + Arguments = parsedArguments, + Channel = converterContext.Channel, + Command = converterContext.Command, + Extension = this.extension ?? throw new InvalidOperationException("TextCommandProcessor has not been configured."), + Message = converterContext.Message, + PrefixLength = converterContext.PrefixLength, + ServiceScope = converterContext.ServiceScope, + User = converterContext.User, + }; + } + + /// + protected override async ValueTask ExecuteConverterAsync(ITextArgumentConverter converter, TextConverterContext context) + { + // Store the current argument index to restore it later. + int currentArgumentIndex = context.CurrentArgumentIndex; + + // Switch to the original message before checking if it has a reply. + context.SwitchToReply(false); + if (converter.RequiresText is ConverterInputType.Never + || (converter.RequiresText is ConverterInputType.IfReplyPresent && context.Message.ReferencedMessage is null) + || (converter.RequiresText is ConverterInputType.IfReplyMissing && context.Message.ReferencedMessage is not null)) + { + // Go to the previous argument if the converter does not require text. + context.CurrentArgumentIndex = -1; + } + + // Execute the converter + IOptional value = await base.ExecuteConverterAsync(converter, context); + + // Restore the current argument index + context.CurrentArgumentIndex = currentArgumentIndex; + + // Return the result + return value; + } + + /// + /// Attempts to retrieve a command from the provided command text. Searches for the command by name, then by alias. Subcommands are also resolved. + /// This method ignores 's and will instead return the group command instead of the default subcommand. + /// + /// The full command name and optionally it's arguments. + /// The guild ID to check if the command is available in the guild. Pass 0 if not applicable. + /// The index of that the command name ends at. + /// The resolved command. + /// If the command was found. + public bool TryGetCommand(string commandText, ulong guildId, out int index, [NotNullWhen(true)] out Command? command) + { + // Declare the index here for scope, keep reading until a whitespace character is found. + for (index = 0; index < commandText.Length; index++) + { + if (char.IsWhiteSpace(commandText[index])) + { + break; + } + } + + string rootCommandText = commandText[..index]; + if (!this.commands.TryGetValue(rootCommandText, out command)) + { + // Search for any aliases + foreach (Command officialCommand in this.commands.Values) + { + TextAliasAttribute? aliasAttribute = officialCommand.Attributes.OfType().FirstOrDefault(); + if (aliasAttribute is not null && aliasAttribute.Aliases.Any(alias => this.Configuration.CommandNameComparer.Equals(alias, rootCommandText))) + { + command = officialCommand; + break; + } + } + } + + // No alias was found + if (command is null || (command.GuildIds.Count > 0 && !command.GuildIds.Contains(guildId))) + { + return false; + } + + // If there is a space after the command's name, skip it. + if (index < commandText.Length && commandText[index] == ' ') + { + index++; + } + + // Recursively resolve subcommands + int nextIndex = index; + while (nextIndex != -1) + { + // If the index is at the end of the string, break + if (nextIndex >= commandText.Length) + { + break; + } + + // If there was no space found after the subcommand, break + nextIndex = commandText.IndexOf(' ', nextIndex + 1); + if (nextIndex == -1) + { + // No more spaces. Search the rest of the string to see if there is a subcommand that matches. + nextIndex = commandText.Length; + } + + // Resolve subcommands + string subcommandName = commandText[index..nextIndex]; + + // Try searching for the subcommand by name, then by alias + // We prioritize the name over the aliases to avoid a poor dev debugging experience + Command? foundCommand = command.Subcommands.FirstOrDefault(subcommand => this.Configuration.CommandNameComparer.Equals(subcommand.Name, subcommandName.Trim())); + if (foundCommand is null) + { + // Search for any aliases that the subcommand may have + foreach (Command subcommand in command.Subcommands) + { + foreach (Attribute attribute in subcommand.Attributes) + { + if (attribute is not TextAliasAttribute aliasAttribute) + { + continue; + } + + foreach (string alias in aliasAttribute.Aliases) + { + if (this.Configuration.CommandNameComparer.Equals(alias, subcommandName)) + { + foundCommand = subcommand; + break; + } + } + + if (foundCommand is not null) + { + break; + } + } + + if (foundCommand is not null) + { + break; + } + } + + if (foundCommand is null) + { + // There was no subcommand found by name or by alias. + // Maybe the index is on an argument for the command? + return true; + } + } + + // Try to parse the next subcommand + index = nextIndex; + command = foundCommand; + } + + // We found it!! Good job! + return true; + } +} diff --git a/DSharpPlus.Commands/Processors/TextCommands/TextConverterContext.cs b/DSharpPlus.Commands/Processors/TextCommands/TextConverterContext.cs index 8289bdcb5a..cefc148ca1 100644 --- a/DSharpPlus.Commands/Processors/TextCommands/TextConverterContext.cs +++ b/DSharpPlus.Commands/Processors/TextCommands/TextConverterContext.cs @@ -1,143 +1,143 @@ -using System.Diagnostics; -using System.Linq; -using DSharpPlus.Commands.Converters; -using DSharpPlus.Commands.Processors.TextCommands.ContextChecks; -using DSharpPlus.Commands.Processors.TextCommands.Parsing; -using DSharpPlus.Entities; - -namespace DSharpPlus.Commands.Processors.TextCommands; - -public record TextConverterContext : ConverterContext -{ - public required string RawArguments { get => this.rawArguments; init => this.rawArguments = value; } - public required DiscordMessage Message { get => this.message; init => this.message = value; } - public required TextArgumentSplicer Splicer { get; init; } - public required int PrefixLength { internal get; init; } - public string? Prefix => this.Message.Content?[..this.PrefixLength]; - public new string Argument => base.Argument as string ?? string.Empty; - public int CurrentArgumentIndex { get; internal set; } - public int NextArgumentIndex { get; internal set; } - public bool IsOnMessageReply { get; private set; } - - // We don't use an auto-property here because we - // want a public init and private set at the same time. - private string rawArguments = null!; - private DiscordMessage message = null!; - private TextMessageReplyAttribute? replyAttribute; - private TextConverterContext? replyConverterContext; - - public override bool NextParameter() - { - // If there's not another parameter, don't try to - // resolve require reply attribute logic. - if (!base.NextParameter()) - { - return false; - } - - // If the parameter wants a reply, switch to it. - this.replyAttribute = this.Parameter.Attributes.OfType().FirstOrDefault(); - if (this.replyAttribute is not null && this.IsOnMessageReply) - { - return false; - } - - SwitchToReply(this.IsOnMessageReply); - return true; - } - - public override bool NextArgument() - { - if (this.replyAttribute is not null && this.CurrentArgumentIndex == -1) - { - // If the argument converter does not require text, TextCommandProcessor.ExecuteConverter - // will set the argument index to -1 and change it back to the previous index after the conversion. - // However if this is called twice, that means the argument converter required text only if the reply doesn't exist. - // In that case, we should skip the reply and move to the next argument. - this.CurrentArgumentIndex = 0; - return true; - } - else if (this.NextArgumentIndex >= this.RawArguments.Length || this.NextArgumentIndex == -1) - { - return false; - } - - this.CurrentArgumentIndex = this.NextArgumentIndex; - int nextTextIndex = this.NextArgumentIndex; - string? nextText = this.Splicer(this.Extension, this.RawArguments, ref nextTextIndex); - if (string.IsNullOrEmpty(nextText)) - { - base.Argument = string.Empty; - return false; - } - - this.NextArgumentIndex = nextTextIndex; - base.Argument = nextText; - return true; - } - - /// - /// Whether to switch to the original message or the reply that message references. - /// - /// Whether to switch to the original message from the reply. - public void SwitchToReply(bool value) - { - // If the value is the same as the current state, don't do anything - if (this.IsOnMessageReply == value) - { - return; - } - - // If we're not on the reply and we need to switch to the reply, - // copy this context's state to the reply context. - if (value && !this.IsOnMessageReply) - { - // If the reply context is null, create a new one - if (this.replyConverterContext is null) - { - // Copy this context to the reply context - this.replyConverterContext = this with - { - CurrentArgumentIndex = this.CurrentArgumentIndex, - NextArgumentIndex = this.NextArgumentIndex, - }; - } - else - { - // Copy the state of the reply context to the current context - this.replyConverterContext.CurrentArgumentIndex = this.CurrentArgumentIndex; - this.replyConverterContext.NextArgumentIndex = this.NextArgumentIndex; - } - - // Set this context to the reply's properties - this.message = this.message.ReferencedMessage!; - this.rawArguments = this.message?.Content!; - this.CurrentArgumentIndex = 0; - this.NextArgumentIndex = 0; - - this.IsOnMessageReply = value; - return; - } - - // If we're on the reply and we need to switch to the original message, - // copy the other context's state to this context. - if (this.replyConverterContext is null) - { - throw new UnreachableException("The reply context should not be null when switching to the original message."); - } - - // We're no longer on a reply parameter, so we need to switch back to the original context - int currentArgumentIndex = this.CurrentArgumentIndex; - int nextArgumentIndex = this.NextArgumentIndex; - - this.message = this.replyConverterContext.Message; - this.rawArguments = this.replyConverterContext.RawArguments; - this.CurrentArgumentIndex = this.replyConverterContext.CurrentArgumentIndex; - this.NextArgumentIndex = this.replyConverterContext.NextArgumentIndex; - - // Set the state in the reply context - this.replyConverterContext.CurrentArgumentIndex = currentArgumentIndex; - this.replyConverterContext.NextArgumentIndex = nextArgumentIndex; - this.IsOnMessageReply = value; - } -} +using System.Diagnostics; +using System.Linq; +using DSharpPlus.Commands.Converters; +using DSharpPlus.Commands.Processors.TextCommands.ContextChecks; +using DSharpPlus.Commands.Processors.TextCommands.Parsing; +using DSharpPlus.Entities; + +namespace DSharpPlus.Commands.Processors.TextCommands; + +public record TextConverterContext : ConverterContext +{ + public required string RawArguments { get => this.rawArguments; init => this.rawArguments = value; } + public required DiscordMessage Message { get => this.message; init => this.message = value; } + public required TextArgumentSplicer Splicer { get; init; } + public required int PrefixLength { internal get; init; } + public string? Prefix => this.Message.Content?[..this.PrefixLength]; + public new string Argument => base.Argument as string ?? string.Empty; + public int CurrentArgumentIndex { get; internal set; } + public int NextArgumentIndex { get; internal set; } + public bool IsOnMessageReply { get; private set; } + + // We don't use an auto-property here because we + // want a public init and private set at the same time. + private string rawArguments = null!; + private DiscordMessage message = null!; + private TextMessageReplyAttribute? replyAttribute; + private TextConverterContext? replyConverterContext; + + public override bool NextParameter() + { + // If there's not another parameter, don't try to + // resolve require reply attribute logic. + if (!base.NextParameter()) + { + return false; + } + + // If the parameter wants a reply, switch to it. + this.replyAttribute = this.Parameter.Attributes.OfType().FirstOrDefault(); + if (this.replyAttribute is not null && this.IsOnMessageReply) + { + return false; + } + + SwitchToReply(this.IsOnMessageReply); + return true; + } + + public override bool NextArgument() + { + if (this.replyAttribute is not null && this.CurrentArgumentIndex == -1) + { + // If the argument converter does not require text, TextCommandProcessor.ExecuteConverter + // will set the argument index to -1 and change it back to the previous index after the conversion. + // However if this is called twice, that means the argument converter required text only if the reply doesn't exist. + // In that case, we should skip the reply and move to the next argument. + this.CurrentArgumentIndex = 0; + return true; + } + else if (this.NextArgumentIndex >= this.RawArguments.Length || this.NextArgumentIndex == -1) + { + return false; + } + + this.CurrentArgumentIndex = this.NextArgumentIndex; + int nextTextIndex = this.NextArgumentIndex; + string? nextText = this.Splicer(this.Extension, this.RawArguments, ref nextTextIndex); + if (string.IsNullOrEmpty(nextText)) + { + base.Argument = string.Empty; + return false; + } + + this.NextArgumentIndex = nextTextIndex; + base.Argument = nextText; + return true; + } + + /// + /// Whether to switch to the original message or the reply that message references. + /// + /// Whether to switch to the original message from the reply. + public void SwitchToReply(bool value) + { + // If the value is the same as the current state, don't do anything + if (this.IsOnMessageReply == value) + { + return; + } + + // If we're not on the reply and we need to switch to the reply, + // copy this context's state to the reply context. + if (value && !this.IsOnMessageReply) + { + // If the reply context is null, create a new one + if (this.replyConverterContext is null) + { + // Copy this context to the reply context + this.replyConverterContext = this with + { + CurrentArgumentIndex = this.CurrentArgumentIndex, + NextArgumentIndex = this.NextArgumentIndex, + }; + } + else + { + // Copy the state of the reply context to the current context + this.replyConverterContext.CurrentArgumentIndex = this.CurrentArgumentIndex; + this.replyConverterContext.NextArgumentIndex = this.NextArgumentIndex; + } + + // Set this context to the reply's properties + this.message = this.message.ReferencedMessage!; + this.rawArguments = this.message?.Content!; + this.CurrentArgumentIndex = 0; + this.NextArgumentIndex = 0; + + this.IsOnMessageReply = value; + return; + } + + // If we're on the reply and we need to switch to the original message, + // copy the other context's state to this context. + if (this.replyConverterContext is null) + { + throw new UnreachableException("The reply context should not be null when switching to the original message."); + } + + // We're no longer on a reply parameter, so we need to switch back to the original context + int currentArgumentIndex = this.CurrentArgumentIndex; + int nextArgumentIndex = this.NextArgumentIndex; + + this.message = this.replyConverterContext.Message; + this.rawArguments = this.replyConverterContext.RawArguments; + this.CurrentArgumentIndex = this.replyConverterContext.CurrentArgumentIndex; + this.NextArgumentIndex = this.replyConverterContext.NextArgumentIndex; + + // Set the state in the reply context + this.replyConverterContext.CurrentArgumentIndex = currentArgumentIndex; + this.replyConverterContext.NextArgumentIndex = nextArgumentIndex; + this.IsOnMessageReply = value; + } +} diff --git a/DSharpPlus.Commands/Processors/TextCommands/TextLogging.cs b/DSharpPlus.Commands/Processors/TextCommands/TextLogging.cs index 52df1319f0..b85d15896b 100644 --- a/DSharpPlus.Commands/Processors/TextCommands/TextLogging.cs +++ b/DSharpPlus.Commands/Processors/TextCommands/TextLogging.cs @@ -1,11 +1,11 @@ -using System; -using Microsoft.Extensions.Logging; - -namespace DSharpPlus.Commands.Processors.TextCommands; - -internal static class TextLogging -{ - // Startup logs - internal static readonly Action missingRequiredIntents = LoggerMessage.Define(LogLevel.Error, new EventId(0, "Text Commands Startup"), "To make the bot work properly with text commands, the following intents need to be enabled in your DiscordClientConfiguration: {Intents}. Without these intents, text commands will not function AT ALL. Please ensure that the intents are enabled in your Discord configuration."); - internal static readonly Action missingMessageContentIntent = LoggerMessage.Define(LogLevel.Warning, new EventId(1, "Text Commands Startup"), "To make the bot work properly with command prefixes, the MessageContents intent needs to be enabled in your DiscordClientConfiguration. Without this intent, commands invoked through prefixes will not function, and the bot will only respond to mentions and DMs. Please ensure that the MessageContents intent is enabled in your configuration. To suppress this warning, set 'CommandsConfiguration.SuppressMissingMessageContentIntentWarning' to true."); -} +using System; +using Microsoft.Extensions.Logging; + +namespace DSharpPlus.Commands.Processors.TextCommands; + +internal static class TextLogging +{ + // Startup logs + internal static readonly Action missingRequiredIntents = LoggerMessage.Define(LogLevel.Error, new EventId(0, "Text Commands Startup"), "To make the bot work properly with text commands, the following intents need to be enabled in your DiscordClientConfiguration: {Intents}. Without these intents, text commands will not function AT ALL. Please ensure that the intents are enabled in your Discord configuration."); + internal static readonly Action missingMessageContentIntent = LoggerMessage.Define(LogLevel.Warning, new EventId(1, "Text Commands Startup"), "To make the bot work properly with command prefixes, the MessageContents intent needs to be enabled in your DiscordClientConfiguration. Without this intent, commands invoked through prefixes will not function, and the bot will only respond to mentions and DMs. Please ensure that the MessageContents intent is enabled in your configuration. To suppress this warning, set 'CommandsConfiguration.SuppressMissingMessageContentIntentWarning' to true."); +} diff --git a/DSharpPlus.Commands/Processors/UserCommands/UserCommandContext.cs b/DSharpPlus.Commands/Processors/UserCommands/UserCommandContext.cs index 2a972e9e97..214054d58b 100644 --- a/DSharpPlus.Commands/Processors/UserCommands/UserCommandContext.cs +++ b/DSharpPlus.Commands/Processors/UserCommands/UserCommandContext.cs @@ -1,8 +1,8 @@ -using DSharpPlus.Commands.Processors.SlashCommands; - -namespace DSharpPlus.Commands.Processors.UserCommands; - -/// -/// Indicates that the command was invoked via a user interaction. -/// -public record UserCommandContext : SlashCommandContext; +using DSharpPlus.Commands.Processors.SlashCommands; + +namespace DSharpPlus.Commands.Processors.UserCommands; + +/// +/// Indicates that the command was invoked via a user interaction. +/// +public record UserCommandContext : SlashCommandContext; diff --git a/DSharpPlus.Commands/Processors/UserCommands/UserCommandLogging.cs b/DSharpPlus.Commands/Processors/UserCommands/UserCommandLogging.cs index 7ffca0ed62..4583368043 100644 --- a/DSharpPlus.Commands/Processors/UserCommands/UserCommandLogging.cs +++ b/DSharpPlus.Commands/Processors/UserCommands/UserCommandLogging.cs @@ -1,14 +1,14 @@ -using System; -using DSharpPlus.Commands.Processors.SlashCommands; -using Microsoft.Extensions.Logging; - -namespace DSharpPlus.Commands.Processors.UserCommands; - -internal static class UserCommandLogging -{ - internal static readonly Action interactionReceivedBeforeConfigured = LoggerMessage.Define(LogLevel.Warning, new EventId(1, "User Commands Startup"), "Received an interaction before the user commands processor was configured. This interaction will be ignored."); - internal static readonly Action userCommandCannotHaveSubcommands = LoggerMessage.Define(LogLevel.Warning, new EventId(4, "User Commands Startup"), "The user context menu command '{CommandName}' cannot have subcommands."); - internal static readonly Action userCommandContextParameterType = LoggerMessage.Define(LogLevel.Warning, new EventId(5, "User Commands Startup"), $"The first parameter of '{{CommandName}}' does not implement {nameof(SlashCommandContext)}. Since this command is being registered as a user context menu command, it's first parameter must inherit the {nameof(SlashCommandContext)} class."); - internal static readonly Action invalidParameterType = LoggerMessage.Define(LogLevel.Warning, new EventId(2, "User Commands Startup"), "The second parameter of '{CommandName}' is not a DiscordUser or DiscordMember. Since this command is being registered as a user context menu command, it's second parameter must be a DiscordUser or a DiscordMember."); - internal static readonly Action invalidParameterMissingDefaultValue = LoggerMessage.Define(LogLevel.Warning, new EventId(3, "User Commands Startup"), "Parameter {ParameterIndex} of '{CommandName}' does not have a default value. Since this command is being registered as a user context menu command, any additional parameters must have a default value."); -} +using System; +using DSharpPlus.Commands.Processors.SlashCommands; +using Microsoft.Extensions.Logging; + +namespace DSharpPlus.Commands.Processors.UserCommands; + +internal static class UserCommandLogging +{ + internal static readonly Action interactionReceivedBeforeConfigured = LoggerMessage.Define(LogLevel.Warning, new EventId(1, "User Commands Startup"), "Received an interaction before the user commands processor was configured. This interaction will be ignored."); + internal static readonly Action userCommandCannotHaveSubcommands = LoggerMessage.Define(LogLevel.Warning, new EventId(4, "User Commands Startup"), "The user context menu command '{CommandName}' cannot have subcommands."); + internal static readonly Action userCommandContextParameterType = LoggerMessage.Define(LogLevel.Warning, new EventId(5, "User Commands Startup"), $"The first parameter of '{{CommandName}}' does not implement {nameof(SlashCommandContext)}. Since this command is being registered as a user context menu command, it's first parameter must inherit the {nameof(SlashCommandContext)} class."); + internal static readonly Action invalidParameterType = LoggerMessage.Define(LogLevel.Warning, new EventId(2, "User Commands Startup"), "The second parameter of '{CommandName}' is not a DiscordUser or DiscordMember. Since this command is being registered as a user context menu command, it's second parameter must be a DiscordUser or a DiscordMember."); + internal static readonly Action invalidParameterMissingDefaultValue = LoggerMessage.Define(LogLevel.Warning, new EventId(3, "User Commands Startup"), "Parameter {ParameterIndex} of '{CommandName}' does not have a default value. Since this command is being registered as a user context menu command, any additional parameters must have a default value."); +} diff --git a/DSharpPlus.Commands/Processors/UserCommands/UserCommandProcessor.cs b/DSharpPlus.Commands/Processors/UserCommands/UserCommandProcessor.cs index 9b48940343..938f456c43 100644 --- a/DSharpPlus.Commands/Processors/UserCommands/UserCommandProcessor.cs +++ b/DSharpPlus.Commands/Processors/UserCommands/UserCommandProcessor.cs @@ -1,197 +1,197 @@ -using System; -using System.Collections.Frozen; -using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; -using DSharpPlus.Commands.ContextChecks; -using DSharpPlus.Commands.Converters; -using DSharpPlus.Commands.EventArgs; -using DSharpPlus.Commands.Exceptions; -using DSharpPlus.Commands.Processors.SlashCommands; -using DSharpPlus.Commands.Processors.SlashCommands.Localization; -using DSharpPlus.Commands.Processors.SlashCommands.Metadata; -using DSharpPlus.Commands.Trees; -using DSharpPlus.Commands.Trees.Metadata; -using DSharpPlus.Entities; -using DSharpPlus.EventArgs; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; - -namespace DSharpPlus.Commands.Processors.UserCommands; - -public sealed class UserCommandProcessor : ICommandProcessor -{ - /// - public Type ContextType => typeof(SlashCommandContext); - - /// - public IReadOnlyDictionary Converters => this.slashCommandProcessor is not null - ? Unsafe.As>(this.slashCommandProcessor.Converters) - : FrozenDictionary.Empty; - - /// - public IReadOnlyList Commands => this.commands; - private readonly List commands = []; - - private CommandsExtension? extension; - private SlashCommandProcessor? slashCommandProcessor; - - /// - public async ValueTask ConfigureAsync(CommandsExtension extension) - { - this.extension = extension; - this.slashCommandProcessor = this.extension.GetProcessor() ?? new SlashCommandProcessor(); - - ILogger logger = this.extension.ServiceProvider.GetService>() ?? NullLogger.Instance; - List applicationCommands = []; - - IReadOnlyList commands = this.extension.GetCommandsForProcessor(this); - IEnumerable flattenCommands = commands.SelectMany(x => x.Flatten()); - foreach (Command command in flattenCommands) - { - // Message commands must be explicitly defined as such, otherwise they are ignored. - if (!command.Attributes.Any(x => x is SlashCommandTypesAttribute slashCommandTypesAttribute - && slashCommandTypesAttribute.ApplicationCommandTypes.Contains(DiscordApplicationCommandType.UserContextMenu))) - { - continue; - } - // Ensure there are no subcommands. - else if (command.Subcommands.Count != 0) - { - UserCommandLogging.userCommandCannotHaveSubcommands(logger, command.FullName, null); - continue; - } - else if (!command.Method!.GetParameters()[0].ParameterType.IsAssignableFrom(typeof(UserCommandContext))) - { - UserCommandLogging.userCommandContextParameterType(logger, command.FullName, null); - continue; - } - - // Check to see if the method signature is valid. - Type firstParameterType = IArgumentConverter.GetConverterFriendlyBaseType(command.Parameters[0].Type); - if (command.Parameters.Count < 1 || !firstParameterType.IsAssignableTo(typeof(DiscordUser))) - { - UserCommandLogging.invalidParameterType(logger, command.FullName, null); - continue; - } - - // Iterate over all parameters and ensure they have default values. - for (int i = 1; i < command.Parameters.Count; i++) - { - if (!command.Parameters[i].DefaultValue.HasValue) - { - UserCommandLogging.invalidParameterMissingDefaultValue(logger, i, command.FullName, null); - continue; - } - } - - this.commands.Add(command); - applicationCommands.Add(await ToApplicationCommandAsync(command)); - } - - this.slashCommandProcessor.AddApplicationCommands(applicationCommands); - } - - public async Task ExecuteInteractionAsync(DiscordClient client, ContextMenuInteractionCreatedEventArgs eventArgs) - { - if (this.extension is null || this.slashCommandProcessor is null) - { - throw new InvalidOperationException("SlashCommandProcessor has not been configured."); - } - else if (eventArgs.Interaction.Type is not DiscordInteractionType.ApplicationCommand || eventArgs.Interaction.Data.Type is not DiscordApplicationCommandType.UserContextMenu) - { - return; - } - - AsyncServiceScope scope = this.extension.ServiceProvider.CreateAsyncScope(); - if (this.slashCommandProcessor.ApplicationCommandMapping.Count == 0) - { - ILogger logger = this.extension.ServiceProvider.GetService>() ?? NullLogger.Instance; - logger.LogWarning("Received an interaction for a user command, but commands have not been registered yet. Ignoring interaction"); - } - - if (!this.slashCommandProcessor.TryFindCommand(eventArgs.Interaction, out Command? command, out _)) - { - await this.extension.commandErrored.InvokeAsync(this.extension, new CommandErroredEventArgs() - { - Context = new UserCommandContext() - { - Arguments = new Dictionary(), - Channel = eventArgs.Interaction.Channel, - Command = null!, - Extension = this.extension, - Interaction = eventArgs.Interaction, - Options = eventArgs.Interaction.Data.Options ?? [], - ServiceScope = scope, - User = eventArgs.Interaction.User, - }, - CommandObject = null, - Exception = new CommandNotFoundException(eventArgs.Interaction.Data.Name), - }); - - await scope.DisposeAsync(); - return; - } - - // The first parameter for MessageContextMenu commands is always the DiscordMessage. - Dictionary arguments = new() { { command.Parameters[0], eventArgs.TargetUser } }; - - // Because methods can have multiple interaction invocation types, - // there has been a demand to be able to register methods with multiple - // parameters, even for MessageContextMenu commands. - // The condition is that all the parameters on the method must have default values. - for (int i = 1; i < command.Parameters.Count; i++) - { - // We verify at startup that all parameters have default values. - arguments.Add(command.Parameters[i], command.Parameters[i].DefaultValue.Value); - } - - UserCommandContext commandContext = new() - { - Arguments = arguments, - Channel = eventArgs.Interaction.Channel, - Command = command, - Extension = this.extension, - Interaction = eventArgs.Interaction, - Options = [], - ServiceScope = scope, - User = eventArgs.Interaction.User, - }; - - await this.extension.CommandExecutor.ExecuteAsync(commandContext); - } - - public async Task ToApplicationCommandAsync(Command command) - { - if (this.slashCommandProcessor is null) - { - throw new InvalidOperationException("SlashCommandProcessor has not been configured."); - } - - IReadOnlyDictionary nameLocalizations = FrozenDictionary.Empty; - if (command.Attributes.OfType().FirstOrDefault() is InteractionLocalizerAttribute localizerAttribute) - { - nameLocalizations = await this.slashCommandProcessor.ExecuteLocalizerAsync(localizerAttribute.LocalizerType, $"{command.FullName}.name"); - } - - DiscordPermissions? userPermissions = command.Attributes.OfType().FirstOrDefault()?.UserPermissions; - - return new - ( - name: command.Attributes.OfType().FirstOrDefault()?.DisplayName ?? command.FullName, - description: string.Empty, - type: DiscordApplicationCommandType.UserContextMenu, - name_localizations: nameLocalizations, - allowDMUsage: command.Attributes.Any(x => x is AllowDMUsageAttribute), - defaultMemberPermissions: userPermissions is not null - ? userPermissions - : new DiscordPermissions(DiscordPermission.UseApplicationCommands), - nsfw: command.Attributes.Any(x => x is RequireNsfwAttribute), - contexts: command.Attributes.OfType().FirstOrDefault()?.AllowedContexts, - integrationTypes: command.Attributes.OfType().FirstOrDefault()?.InstallTypes - ); - } -} +using System; +using System.Collections.Frozen; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using DSharpPlus.Commands.ContextChecks; +using DSharpPlus.Commands.Converters; +using DSharpPlus.Commands.EventArgs; +using DSharpPlus.Commands.Exceptions; +using DSharpPlus.Commands.Processors.SlashCommands; +using DSharpPlus.Commands.Processors.SlashCommands.Localization; +using DSharpPlus.Commands.Processors.SlashCommands.Metadata; +using DSharpPlus.Commands.Trees; +using DSharpPlus.Commands.Trees.Metadata; +using DSharpPlus.Entities; +using DSharpPlus.EventArgs; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace DSharpPlus.Commands.Processors.UserCommands; + +public sealed class UserCommandProcessor : ICommandProcessor +{ + /// + public Type ContextType => typeof(SlashCommandContext); + + /// + public IReadOnlyDictionary Converters => this.slashCommandProcessor is not null + ? Unsafe.As>(this.slashCommandProcessor.Converters) + : FrozenDictionary.Empty; + + /// + public IReadOnlyList Commands => this.commands; + private readonly List commands = []; + + private CommandsExtension? extension; + private SlashCommandProcessor? slashCommandProcessor; + + /// + public async ValueTask ConfigureAsync(CommandsExtension extension) + { + this.extension = extension; + this.slashCommandProcessor = this.extension.GetProcessor() ?? new SlashCommandProcessor(); + + ILogger logger = this.extension.ServiceProvider.GetService>() ?? NullLogger.Instance; + List applicationCommands = []; + + IReadOnlyList commands = this.extension.GetCommandsForProcessor(this); + IEnumerable flattenCommands = commands.SelectMany(x => x.Flatten()); + foreach (Command command in flattenCommands) + { + // Message commands must be explicitly defined as such, otherwise they are ignored. + if (!command.Attributes.Any(x => x is SlashCommandTypesAttribute slashCommandTypesAttribute + && slashCommandTypesAttribute.ApplicationCommandTypes.Contains(DiscordApplicationCommandType.UserContextMenu))) + { + continue; + } + // Ensure there are no subcommands. + else if (command.Subcommands.Count != 0) + { + UserCommandLogging.userCommandCannotHaveSubcommands(logger, command.FullName, null); + continue; + } + else if (!command.Method!.GetParameters()[0].ParameterType.IsAssignableFrom(typeof(UserCommandContext))) + { + UserCommandLogging.userCommandContextParameterType(logger, command.FullName, null); + continue; + } + + // Check to see if the method signature is valid. + Type firstParameterType = IArgumentConverter.GetConverterFriendlyBaseType(command.Parameters[0].Type); + if (command.Parameters.Count < 1 || !firstParameterType.IsAssignableTo(typeof(DiscordUser))) + { + UserCommandLogging.invalidParameterType(logger, command.FullName, null); + continue; + } + + // Iterate over all parameters and ensure they have default values. + for (int i = 1; i < command.Parameters.Count; i++) + { + if (!command.Parameters[i].DefaultValue.HasValue) + { + UserCommandLogging.invalidParameterMissingDefaultValue(logger, i, command.FullName, null); + continue; + } + } + + this.commands.Add(command); + applicationCommands.Add(await ToApplicationCommandAsync(command)); + } + + this.slashCommandProcessor.AddApplicationCommands(applicationCommands); + } + + public async Task ExecuteInteractionAsync(DiscordClient client, ContextMenuInteractionCreatedEventArgs eventArgs) + { + if (this.extension is null || this.slashCommandProcessor is null) + { + throw new InvalidOperationException("SlashCommandProcessor has not been configured."); + } + else if (eventArgs.Interaction.Type is not DiscordInteractionType.ApplicationCommand || eventArgs.Interaction.Data.Type is not DiscordApplicationCommandType.UserContextMenu) + { + return; + } + + AsyncServiceScope scope = this.extension.ServiceProvider.CreateAsyncScope(); + if (this.slashCommandProcessor.ApplicationCommandMapping.Count == 0) + { + ILogger logger = this.extension.ServiceProvider.GetService>() ?? NullLogger.Instance; + logger.LogWarning("Received an interaction for a user command, but commands have not been registered yet. Ignoring interaction"); + } + + if (!this.slashCommandProcessor.TryFindCommand(eventArgs.Interaction, out Command? command, out _)) + { + await this.extension.commandErrored.InvokeAsync(this.extension, new CommandErroredEventArgs() + { + Context = new UserCommandContext() + { + Arguments = new Dictionary(), + Channel = eventArgs.Interaction.Channel, + Command = null!, + Extension = this.extension, + Interaction = eventArgs.Interaction, + Options = eventArgs.Interaction.Data.Options ?? [], + ServiceScope = scope, + User = eventArgs.Interaction.User, + }, + CommandObject = null, + Exception = new CommandNotFoundException(eventArgs.Interaction.Data.Name), + }); + + await scope.DisposeAsync(); + return; + } + + // The first parameter for MessageContextMenu commands is always the DiscordMessage. + Dictionary arguments = new() { { command.Parameters[0], eventArgs.TargetUser } }; + + // Because methods can have multiple interaction invocation types, + // there has been a demand to be able to register methods with multiple + // parameters, even for MessageContextMenu commands. + // The condition is that all the parameters on the method must have default values. + for (int i = 1; i < command.Parameters.Count; i++) + { + // We verify at startup that all parameters have default values. + arguments.Add(command.Parameters[i], command.Parameters[i].DefaultValue.Value); + } + + UserCommandContext commandContext = new() + { + Arguments = arguments, + Channel = eventArgs.Interaction.Channel, + Command = command, + Extension = this.extension, + Interaction = eventArgs.Interaction, + Options = [], + ServiceScope = scope, + User = eventArgs.Interaction.User, + }; + + await this.extension.CommandExecutor.ExecuteAsync(commandContext); + } + + public async Task ToApplicationCommandAsync(Command command) + { + if (this.slashCommandProcessor is null) + { + throw new InvalidOperationException("SlashCommandProcessor has not been configured."); + } + + IReadOnlyDictionary nameLocalizations = FrozenDictionary.Empty; + if (command.Attributes.OfType().FirstOrDefault() is InteractionLocalizerAttribute localizerAttribute) + { + nameLocalizations = await this.slashCommandProcessor.ExecuteLocalizerAsync(localizerAttribute.LocalizerType, $"{command.FullName}.name"); + } + + DiscordPermissions? userPermissions = command.Attributes.OfType().FirstOrDefault()?.UserPermissions; + + return new + ( + name: command.Attributes.OfType().FirstOrDefault()?.DisplayName ?? command.FullName, + description: string.Empty, + type: DiscordApplicationCommandType.UserContextMenu, + name_localizations: nameLocalizations, + allowDMUsage: command.Attributes.Any(x => x is AllowDMUsageAttribute), + defaultMemberPermissions: userPermissions is not null + ? userPermissions + : new DiscordPermissions(DiscordPermission.UseApplicationCommands), + nsfw: command.Attributes.Any(x => x is RequireNsfwAttribute), + contexts: command.Attributes.OfType().FirstOrDefault()?.AllowedContexts, + integrationTypes: command.Attributes.OfType().FirstOrDefault()?.InstallTypes + ); + } +} diff --git a/DSharpPlus.Commands/Properties/AssemblyProperties.cs b/DSharpPlus.Commands/Properties/AssemblyProperties.cs index f6564baedb..fc5bf54493 100644 --- a/DSharpPlus.Commands/Properties/AssemblyProperties.cs +++ b/DSharpPlus.Commands/Properties/AssemblyProperties.cs @@ -1,3 +1,3 @@ -using System.Runtime.CompilerServices; - -[assembly: InternalsVisibleTo("DSharpPlus.Tests")] +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("DSharpPlus.Tests")] diff --git a/DSharpPlus.Commands/RegisterToGuildsAttribute.cs b/DSharpPlus.Commands/RegisterToGuildsAttribute.cs index 584004fa32..d3fdc0448e 100644 --- a/DSharpPlus.Commands/RegisterToGuildsAttribute.cs +++ b/DSharpPlus.Commands/RegisterToGuildsAttribute.cs @@ -1,27 +1,27 @@ -using System; -using System.Collections.Generic; - -namespace DSharpPlus.Commands; - -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Delegate)] -public class RegisterToGuildsAttribute : Attribute -{ - /// - /// The guild ids to register this command to. - /// - public IReadOnlyList GuildIds { get; init; } - - /// - /// Creates a new instance of the class. - /// - /// The guild ids to register this command to. - public RegisterToGuildsAttribute(params ulong[] guildIds) - { - if (guildIds.Length == 0) - { - throw new ArgumentException("You must provide at least one guild ID."); - } - - this.GuildIds = guildIds; - } -} +using System; +using System.Collections.Generic; + +namespace DSharpPlus.Commands; + +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Delegate)] +public class RegisterToGuildsAttribute : Attribute +{ + /// + /// The guild ids to register this command to. + /// + public IReadOnlyList GuildIds { get; init; } + + /// + /// Creates a new instance of the class. + /// + /// The guild ids to register this command to. + public RegisterToGuildsAttribute(params ulong[] guildIds) + { + if (guildIds.Length == 0) + { + throw new ArgumentException("You must provide at least one guild ID."); + } + + this.GuildIds = guildIds; + } +} diff --git a/DSharpPlus.Commands/Trees/Command.cs b/DSharpPlus.Commands/Trees/Command.cs index 75e01e8f16..754f0167d0 100644 --- a/DSharpPlus.Commands/Trees/Command.cs +++ b/DSharpPlus.Commands/Trees/Command.cs @@ -1,59 +1,59 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Reflection; -using System.Text; - -namespace DSharpPlus.Commands.Trees; - -[DebuggerDisplay("{ToString()}")] -public record Command -{ - public required string Name { get; init; } - public string? Description { get; init; } - public required MethodInfo? Method { get; init; } - public required Ulid Id { get; init; } - public object? Target { get; init; } - public Command? Parent { get; init; } - public IReadOnlyList Subcommands { get; init; } - public IReadOnlyList Parameters { get; init; } - public required IReadOnlyList Attributes { get; init; } - public IReadOnlyList GuildIds { get; init; } = []; - public string FullName => this.Parent is null ? this.Name : $"{this.Parent.FullName} {this.Name}"; - - public Command(IEnumerable subcommandBuilders, IEnumerable parameterBuilders) - { - this.Subcommands = subcommandBuilders.Select(x => x.Build(this)).ToArray(); - this.Parameters = parameterBuilders.Select(x => x.Build(this)).ToArray(); - } - - /// - /// Traverses this command tree, returning this command and all subcommands recursively. - /// - /// A list of all commands in this tree. - public IReadOnlyList Flatten() - { - List commands = [this]; - foreach (Command subcommand in this.Subcommands) - { - commands.AddRange(subcommand.Flatten()); - } - - return commands; - } - - public override string ToString() - { - StringBuilder stringBuilder = new(); - stringBuilder.Append(this.FullName); - if (this.Subcommands.Count == 0) - { - stringBuilder.Append('('); - stringBuilder.AppendJoin(", ", this.Parameters.Select(x => $"{x.Type.Name} {x.Name}")); - stringBuilder.Append(')'); - } - - return stringBuilder.ToString(); - } -} +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using System.Text; + +namespace DSharpPlus.Commands.Trees; + +[DebuggerDisplay("{ToString()}")] +public record Command +{ + public required string Name { get; init; } + public string? Description { get; init; } + public required MethodInfo? Method { get; init; } + public required Ulid Id { get; init; } + public object? Target { get; init; } + public Command? Parent { get; init; } + public IReadOnlyList Subcommands { get; init; } + public IReadOnlyList Parameters { get; init; } + public required IReadOnlyList Attributes { get; init; } + public IReadOnlyList GuildIds { get; init; } = []; + public string FullName => this.Parent is null ? this.Name : $"{this.Parent.FullName} {this.Name}"; + + public Command(IEnumerable subcommandBuilders, IEnumerable parameterBuilders) + { + this.Subcommands = subcommandBuilders.Select(x => x.Build(this)).ToArray(); + this.Parameters = parameterBuilders.Select(x => x.Build(this)).ToArray(); + } + + /// + /// Traverses this command tree, returning this command and all subcommands recursively. + /// + /// A list of all commands in this tree. + public IReadOnlyList Flatten() + { + List commands = [this]; + foreach (Command subcommand in this.Subcommands) + { + commands.AddRange(subcommand.Flatten()); + } + + return commands; + } + + public override string ToString() + { + StringBuilder stringBuilder = new(); + stringBuilder.Append(this.FullName); + if (this.Subcommands.Count == 0) + { + stringBuilder.Append('('); + stringBuilder.AppendJoin(", ", this.Parameters.Select(x => $"{x.Type.Name} {x.Name}")); + stringBuilder.Append(')'); + } + + return stringBuilder.ToString(); + } +} diff --git a/DSharpPlus.Commands/Trees/CommandBuilder.cs b/DSharpPlus.Commands/Trees/CommandBuilder.cs index f9f61d07ef..1fd69a70f6 100644 --- a/DSharpPlus.Commands/Trees/CommandBuilder.cs +++ b/DSharpPlus.Commands/Trees/CommandBuilder.cs @@ -1,288 +1,288 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Reflection; -using System.Text; -using DSharpPlus.Commands.ContextChecks; - -namespace DSharpPlus.Commands.Trees; - -[DebuggerDisplay("{ToString()}")] -public class CommandBuilder -{ - public string? Name { get; set; } - public string? Description { get; set; } - public MethodInfo? Method { get; set; } - public object? Target { get; set; } - public CommandBuilder? Parent { get; set; } - public List Subcommands { get; set; } = []; - public List Parameters { get; set; } = []; - public List Attributes { get; set; } = []; - public List GuildIds { get; set; } = []; - public string? FullName => this.Parent is not null ? $"{this.Parent.FullName}.{this.Name}" : this.Name; - - public CommandBuilder WithName(string name) - { - if (string.IsNullOrWhiteSpace(name)) - { - throw new ArgumentNullException(nameof(name), "The name of the command cannot be null or whitespace."); - } - - this.Name = name; - return this; - } - - public CommandBuilder WithDescription(string? description) - { - this.Description = description; - return this; - } - - public CommandBuilder WithDelegate(Delegate? method) => WithDelegate(method?.Method, method?.Target); - public CommandBuilder WithDelegate(MethodInfo? method, object? target = null) - { - if (method is not null) - { - ParameterInfo[] parameters = method.GetParameters(); - if (parameters.Length == 0 || !parameters[0].ParameterType.IsAssignableTo(typeof(CommandContext))) - { - throw new ArgumentException($"The command method \"{(method.DeclaringType is not null ? $"{method.DeclaringType.FullName}.{method.Name}" : method.Name)}\" must have it's first parameter be a CommandContext.", nameof(method)); - } - } - - this.Method = method; - this.Target = target; - return this; - } - - public CommandBuilder WithParent(CommandBuilder? parent) - { - this.Parent = parent; - return this; - } - - public CommandBuilder WithSubcommands(IEnumerable subcommands) - { - this.Subcommands = new(subcommands); - return this; - } - - public CommandBuilder WithParameters(IEnumerable parameters) - { - this.Parameters = new(parameters); - foreach (CommandParameterBuilder parameter in this.Parameters) - { - parameter.Parent ??= this; - } - - return this; - } - - public CommandBuilder WithAttributes(IEnumerable attributes) - { - this.Attributes = new(attributes); - - foreach (Attribute attribute in this.Attributes) - { - if (attribute is CommandAttribute commandAttribute) - { - WithName(commandAttribute.Name); - } - else if (attribute is DescriptionAttribute descriptionAttribute) - { - WithDescription(descriptionAttribute.Description); - } - else if (attribute is RegisterToGuildsAttribute registerToGuildsAttribute) - { - WithGuildIds(registerToGuildsAttribute.GuildIds); - } - } - - return this; - } - - public CommandBuilder WithGuildIds(IEnumerable guildIds) - { - this.GuildIds = new(guildIds); - return this; - } - - [MemberNotNull(nameof(Name), nameof(Subcommands), nameof(Parameters), nameof(Attributes))] - public Command Build(Command? parent = null) - { - ArgumentNullException.ThrowIfNull(this.Name, nameof(this.Name)); - ArgumentNullException.ThrowIfNull(this.Subcommands, nameof(this.Subcommands)); - ArgumentNullException.ThrowIfNull(this.Parameters, nameof(this.Parameters)); - ArgumentNullException.ThrowIfNull(this.Attributes, nameof(this.Attributes)); - - // Push it through the With* methods again, which contain validation. - WithName(this.Name); - WithDescription(this.Description); - WithDelegate(this.Method, this.Target); - WithSubcommands(this.Subcommands); - WithParameters(this.Parameters); - WithAttributes(this.Attributes); - WithGuildIds(this.GuildIds); - - return new(this.Subcommands, this.Parameters) - { - Name = this.Name, - Description = this.Description, - Method = this.Method, - Id = Ulid.NewUlid(), - Target = this.Target, - Parent = parent, - Attributes = this.Attributes, - GuildIds = this.GuildIds, - }; - } - - /// - /// Traverses this command tree, returning this command builder and all subcommands recursively. - /// - /// A list of all command builders in this tree. - public IReadOnlyList Flatten() - { - List commands = [this]; - foreach (CommandBuilder subcommand in this.Subcommands) - { - commands.AddRange(subcommand.Flatten()); - } - - return commands; - } - - /// - public static CommandBuilder From() => From([]); - - /// - /// The type that'll be searched for subcommands. - public static CommandBuilder From(params ulong[] guildIds) => From(typeof(T), guildIds); - - /// - public static CommandBuilder From(Type type) => From(type, []); - - /// - /// Creates a new group from the specified . - /// - /// The type that'll be searched for subcommands. - /// The guild IDs that this command will be registered in. - /// A new which does it's best to build a pre-filled from the specified . - public static CommandBuilder From(Type type, params ulong[] guildIds) - { - ArgumentNullException.ThrowIfNull(type, nameof(type)); - - CommandBuilder commandBuilder = new(); - commandBuilder.WithAttributes(type.GetCustomAttributes()); - commandBuilder.GuildIds.AddRange(guildIds); - - // Add subcommands - List subcommandBuilders = []; - foreach (Type subcommand in type.GetNestedTypes(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static)) - { - if (subcommand.GetCustomAttribute() is null) - { - continue; - } - - subcommandBuilders.Add(From(subcommand, [.. commandBuilder.GuildIds]).WithParent(commandBuilder)); - } - - // Add methods - foreach (MethodInfo method in type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static)) - { - if (method.GetCustomAttribute() is null) - { - continue; - } - - subcommandBuilders.Add(From(method, guildIds: [.. commandBuilder.GuildIds]).WithParent(commandBuilder)); - } - - if (type.GetCustomAttribute() is not null && subcommandBuilders.Count == 0) - { - throw new ArgumentException($"The type \"{type.FullName ?? type.Name}\" does not have any subcommands or methods with a CommandAttribute.", nameof(type)); - } - - commandBuilder.WithSubcommands(subcommandBuilders); - - // Might be set through the `DescriptionAttribute` - if (string.IsNullOrEmpty(commandBuilder.Description)) - { - commandBuilder.WithDescription("No description provided."); - } - - return commandBuilder; - } - - /// - public static CommandBuilder From(Delegate method) => From(method.Method, method.Target, []); - - /// - public static CommandBuilder From(Delegate method, params ulong[] guildIds) => From(method.Method, method.Target, guildIds); - - /// - public static CommandBuilder From(MethodInfo method, object? target = null) => From(method, target, []); - - /// - /// Creates a new from the specified . - /// - /// The method that'll be invoked when the command is executed. - /// The object/class instance of which will create a delegate with. - /// The guild IDs that this command will be registered in. - /// A new which does it's best to build a pre-filled from the specified . - public static CommandBuilder From(MethodInfo method, object? target = null, params ulong[] guildIds) - { - ArgumentNullException.ThrowIfNull(method, nameof(method)); - if (method.GetCustomAttribute() is null) - { - throw new ArgumentException($"The method \"{(method.DeclaringType is not null ? $"{method.DeclaringType.FullName}.{method.Name}" : method.Name)}\" does not have a CommandAttribute.", nameof(method)); - } - - ParameterInfo[] parameters = method.GetParameters(); - if (parameters.Length == 0 || !parameters[0].ParameterType.IsAssignableTo(typeof(CommandContext))) - { - throw new ArgumentException($"The command method \"{(method.DeclaringType is not null ? $"{method.DeclaringType.FullName}.{method.Name}" : method.Name)}\" must have a parameter and it must be a type of {nameof(CommandContext)}.", nameof(method)); - } - - CommandBuilder commandBuilder = new(); - commandBuilder.WithAttributes(AggregateCustomAttributes(method)); - commandBuilder.WithDelegate(method, target); - commandBuilder.WithParameters(parameters[1..].Select(parameterInfo => CommandParameterBuilder.From(parameterInfo).WithParent(commandBuilder))); - commandBuilder.GuildIds.AddRange(guildIds); - return commandBuilder; - } - - /// - public override string ToString() - { - StringBuilder stringBuilder = new(); - if (this.Parent is not null) - { - stringBuilder.Append(this.Parent.FullName); - stringBuilder.Append('.'); - } - - stringBuilder.Append(this.Name ?? ""); - return stringBuilder.ToString(); - } - - public static IEnumerable AggregateCustomAttributes(MethodInfo info) - { - IEnumerable methodAttributes = info.GetCustomAttributes(); - return methodAttributes.Concat(AggregateCustomAttributesFromType(info.DeclaringType)); - - static IEnumerable AggregateCustomAttributesFromType(Type? type) - { - return type is null - ? [] - : type.GetCustomAttributes(true) - .Where(obj => obj is ContextCheckAttribute) - .Concat(AggregateCustomAttributesFromType(type.DeclaringType)) - .Cast(); - } - } -} +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Reflection; +using System.Text; +using DSharpPlus.Commands.ContextChecks; + +namespace DSharpPlus.Commands.Trees; + +[DebuggerDisplay("{ToString()}")] +public class CommandBuilder +{ + public string? Name { get; set; } + public string? Description { get; set; } + public MethodInfo? Method { get; set; } + public object? Target { get; set; } + public CommandBuilder? Parent { get; set; } + public List Subcommands { get; set; } = []; + public List Parameters { get; set; } = []; + public List Attributes { get; set; } = []; + public List GuildIds { get; set; } = []; + public string? FullName => this.Parent is not null ? $"{this.Parent.FullName}.{this.Name}" : this.Name; + + public CommandBuilder WithName(string name) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentNullException(nameof(name), "The name of the command cannot be null or whitespace."); + } + + this.Name = name; + return this; + } + + public CommandBuilder WithDescription(string? description) + { + this.Description = description; + return this; + } + + public CommandBuilder WithDelegate(Delegate? method) => WithDelegate(method?.Method, method?.Target); + public CommandBuilder WithDelegate(MethodInfo? method, object? target = null) + { + if (method is not null) + { + ParameterInfo[] parameters = method.GetParameters(); + if (parameters.Length == 0 || !parameters[0].ParameterType.IsAssignableTo(typeof(CommandContext))) + { + throw new ArgumentException($"The command method \"{(method.DeclaringType is not null ? $"{method.DeclaringType.FullName}.{method.Name}" : method.Name)}\" must have it's first parameter be a CommandContext.", nameof(method)); + } + } + + this.Method = method; + this.Target = target; + return this; + } + + public CommandBuilder WithParent(CommandBuilder? parent) + { + this.Parent = parent; + return this; + } + + public CommandBuilder WithSubcommands(IEnumerable subcommands) + { + this.Subcommands = new(subcommands); + return this; + } + + public CommandBuilder WithParameters(IEnumerable parameters) + { + this.Parameters = new(parameters); + foreach (CommandParameterBuilder parameter in this.Parameters) + { + parameter.Parent ??= this; + } + + return this; + } + + public CommandBuilder WithAttributes(IEnumerable attributes) + { + this.Attributes = new(attributes); + + foreach (Attribute attribute in this.Attributes) + { + if (attribute is CommandAttribute commandAttribute) + { + WithName(commandAttribute.Name); + } + else if (attribute is DescriptionAttribute descriptionAttribute) + { + WithDescription(descriptionAttribute.Description); + } + else if (attribute is RegisterToGuildsAttribute registerToGuildsAttribute) + { + WithGuildIds(registerToGuildsAttribute.GuildIds); + } + } + + return this; + } + + public CommandBuilder WithGuildIds(IEnumerable guildIds) + { + this.GuildIds = new(guildIds); + return this; + } + + [MemberNotNull(nameof(Name), nameof(Subcommands), nameof(Parameters), nameof(Attributes))] + public Command Build(Command? parent = null) + { + ArgumentNullException.ThrowIfNull(this.Name, nameof(this.Name)); + ArgumentNullException.ThrowIfNull(this.Subcommands, nameof(this.Subcommands)); + ArgumentNullException.ThrowIfNull(this.Parameters, nameof(this.Parameters)); + ArgumentNullException.ThrowIfNull(this.Attributes, nameof(this.Attributes)); + + // Push it through the With* methods again, which contain validation. + WithName(this.Name); + WithDescription(this.Description); + WithDelegate(this.Method, this.Target); + WithSubcommands(this.Subcommands); + WithParameters(this.Parameters); + WithAttributes(this.Attributes); + WithGuildIds(this.GuildIds); + + return new(this.Subcommands, this.Parameters) + { + Name = this.Name, + Description = this.Description, + Method = this.Method, + Id = Ulid.NewUlid(), + Target = this.Target, + Parent = parent, + Attributes = this.Attributes, + GuildIds = this.GuildIds, + }; + } + + /// + /// Traverses this command tree, returning this command builder and all subcommands recursively. + /// + /// A list of all command builders in this tree. + public IReadOnlyList Flatten() + { + List commands = [this]; + foreach (CommandBuilder subcommand in this.Subcommands) + { + commands.AddRange(subcommand.Flatten()); + } + + return commands; + } + + /// + public static CommandBuilder From() => From([]); + + /// + /// The type that'll be searched for subcommands. + public static CommandBuilder From(params ulong[] guildIds) => From(typeof(T), guildIds); + + /// + public static CommandBuilder From(Type type) => From(type, []); + + /// + /// Creates a new group from the specified . + /// + /// The type that'll be searched for subcommands. + /// The guild IDs that this command will be registered in. + /// A new which does it's best to build a pre-filled from the specified . + public static CommandBuilder From(Type type, params ulong[] guildIds) + { + ArgumentNullException.ThrowIfNull(type, nameof(type)); + + CommandBuilder commandBuilder = new(); + commandBuilder.WithAttributes(type.GetCustomAttributes()); + commandBuilder.GuildIds.AddRange(guildIds); + + // Add subcommands + List subcommandBuilders = []; + foreach (Type subcommand in type.GetNestedTypes(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static)) + { + if (subcommand.GetCustomAttribute() is null) + { + continue; + } + + subcommandBuilders.Add(From(subcommand, [.. commandBuilder.GuildIds]).WithParent(commandBuilder)); + } + + // Add methods + foreach (MethodInfo method in type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static)) + { + if (method.GetCustomAttribute() is null) + { + continue; + } + + subcommandBuilders.Add(From(method, guildIds: [.. commandBuilder.GuildIds]).WithParent(commandBuilder)); + } + + if (type.GetCustomAttribute() is not null && subcommandBuilders.Count == 0) + { + throw new ArgumentException($"The type \"{type.FullName ?? type.Name}\" does not have any subcommands or methods with a CommandAttribute.", nameof(type)); + } + + commandBuilder.WithSubcommands(subcommandBuilders); + + // Might be set through the `DescriptionAttribute` + if (string.IsNullOrEmpty(commandBuilder.Description)) + { + commandBuilder.WithDescription("No description provided."); + } + + return commandBuilder; + } + + /// + public static CommandBuilder From(Delegate method) => From(method.Method, method.Target, []); + + /// + public static CommandBuilder From(Delegate method, params ulong[] guildIds) => From(method.Method, method.Target, guildIds); + + /// + public static CommandBuilder From(MethodInfo method, object? target = null) => From(method, target, []); + + /// + /// Creates a new from the specified . + /// + /// The method that'll be invoked when the command is executed. + /// The object/class instance of which will create a delegate with. + /// The guild IDs that this command will be registered in. + /// A new which does it's best to build a pre-filled from the specified . + public static CommandBuilder From(MethodInfo method, object? target = null, params ulong[] guildIds) + { + ArgumentNullException.ThrowIfNull(method, nameof(method)); + if (method.GetCustomAttribute() is null) + { + throw new ArgumentException($"The method \"{(method.DeclaringType is not null ? $"{method.DeclaringType.FullName}.{method.Name}" : method.Name)}\" does not have a CommandAttribute.", nameof(method)); + } + + ParameterInfo[] parameters = method.GetParameters(); + if (parameters.Length == 0 || !parameters[0].ParameterType.IsAssignableTo(typeof(CommandContext))) + { + throw new ArgumentException($"The command method \"{(method.DeclaringType is not null ? $"{method.DeclaringType.FullName}.{method.Name}" : method.Name)}\" must have a parameter and it must be a type of {nameof(CommandContext)}.", nameof(method)); + } + + CommandBuilder commandBuilder = new(); + commandBuilder.WithAttributes(AggregateCustomAttributes(method)); + commandBuilder.WithDelegate(method, target); + commandBuilder.WithParameters(parameters[1..].Select(parameterInfo => CommandParameterBuilder.From(parameterInfo).WithParent(commandBuilder))); + commandBuilder.GuildIds.AddRange(guildIds); + return commandBuilder; + } + + /// + public override string ToString() + { + StringBuilder stringBuilder = new(); + if (this.Parent is not null) + { + stringBuilder.Append(this.Parent.FullName); + stringBuilder.Append('.'); + } + + stringBuilder.Append(this.Name ?? ""); + return stringBuilder.ToString(); + } + + public static IEnumerable AggregateCustomAttributes(MethodInfo info) + { + IEnumerable methodAttributes = info.GetCustomAttributes(); + return methodAttributes.Concat(AggregateCustomAttributesFromType(info.DeclaringType)); + + static IEnumerable AggregateCustomAttributesFromType(Type? type) + { + return type is null + ? [] + : type.GetCustomAttributes(true) + .Where(obj => obj is ContextCheckAttribute) + .Concat(AggregateCustomAttributesFromType(type.DeclaringType)) + .Cast(); + } + } +} diff --git a/DSharpPlus.Commands/Trees/CommandParameter.cs b/DSharpPlus.Commands/Trees/CommandParameter.cs index d8be7b4e27..6339513fd8 100644 --- a/DSharpPlus.Commands/Trees/CommandParameter.cs +++ b/DSharpPlus.Commands/Trees/CommandParameter.cs @@ -1,28 +1,28 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Text; -using DSharpPlus.Entities; - -namespace DSharpPlus.Commands.Trees; - -[DebuggerDisplay("{ToString()}")] -public record CommandParameter -{ - public required string Name { get; init; } - public string? Description { get; init; } - public required Type Type { get; init; } - public IReadOnlyList Attributes { get; init; } = new List(); - public Optional DefaultValue { get; init; } = Optional.FromNoValue(); - public required Command Parent { get; init; } - - /// - public override string ToString() - { - StringBuilder stringBuilder = new(); - stringBuilder.Append(this.Parent.FullName); - stringBuilder.Append('.'); - stringBuilder.Append(this.Name); - return stringBuilder.ToString(); - } -} +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; +using DSharpPlus.Entities; + +namespace DSharpPlus.Commands.Trees; + +[DebuggerDisplay("{ToString()}")] +public record CommandParameter +{ + public required string Name { get; init; } + public string? Description { get; init; } + public required Type Type { get; init; } + public IReadOnlyList Attributes { get; init; } = new List(); + public Optional DefaultValue { get; init; } = Optional.FromNoValue(); + public required Command Parent { get; init; } + + /// + public override string ToString() + { + StringBuilder stringBuilder = new(); + stringBuilder.Append(this.Parent.FullName); + stringBuilder.Append('.'); + stringBuilder.Append(this.Name); + return stringBuilder.ToString(); + } +} diff --git a/DSharpPlus.Commands/Trees/CommandParameterBuilder.cs b/DSharpPlus.Commands/Trees/CommandParameterBuilder.cs index 497153ec29..871051028a 100644 --- a/DSharpPlus.Commands/Trees/CommandParameterBuilder.cs +++ b/DSharpPlus.Commands/Trees/CommandParameterBuilder.cs @@ -1,161 +1,161 @@ -#pragma warning disable CA2264 - -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Reflection; -using System.Text; -using DSharpPlus.Commands.ArgumentModifiers; -using DSharpPlus.Entities; - -namespace DSharpPlus.Commands.Trees; - -[DebuggerDisplay("{ToString()}")] -public partial class CommandParameterBuilder -{ - public string? Name { get; set; } - public string? Description { get; set; } - public Type? Type { get; set; } - public List Attributes { get; set; } = []; - public Optional DefaultValue { get; set; } = Optional.FromNoValue(); - public CommandBuilder? Parent { get; set; } - - public CommandParameterBuilder WithName(string name) - { - if (string.IsNullOrWhiteSpace(name)) - { - throw new ArgumentNullException(nameof(name), "The name of the command cannot be null or whitespace."); - } - - this.Name = name; - return this; - } - - public CommandParameterBuilder WithDescription(string? description) - { - this.Description = description; - return this; - } - - public CommandParameterBuilder WithType(Type type) - { - this.Type = type; - return this; - } - - public CommandParameterBuilder WithAttributes(IEnumerable attributes) - { - List listedAttributes = []; - foreach (Attribute attribute in attributes) - { - if (attribute is CommandAttribute commandAttribute) - { - WithName(commandAttribute.Name); - } - else if (attribute is DescriptionAttribute descriptionAttribute) - { - WithDescription(descriptionAttribute.Description); - } - else if (attribute is ParamArrayAttribute && !this.Attributes.Any(attribute => attribute is VariadicArgumentAttribute)) - { - // Transform the params into a VariadicArgumentAttribute - listedAttributes.Add(new VariadicArgumentAttribute(int.MaxValue)); - } - - listedAttributes.Add(attribute); - } - - this.Attributes = listedAttributes; - return this; - } - - public CommandParameterBuilder WithDefaultValue(Optional defaultValue) - { - this.DefaultValue = defaultValue; - return this; - } - - public CommandParameterBuilder WithParent(CommandBuilder parent) - { - this.Parent = parent; - return this; - } - - [MemberNotNull(nameof(Name), nameof(Description), nameof(Type), nameof(Attributes))] - public CommandParameter Build(Command command) - { - ArgumentNullException.ThrowIfNull(this.Name, nameof(this.Name)); - ArgumentNullException.ThrowIfNull(this.Description, nameof(this.Description)); - ArgumentNullException.ThrowIfNull(this.Type, nameof(this.Type)); - ArgumentNullException.ThrowIfNull(this.Attributes, nameof(this.Attributes)); - ArgumentNullException.ThrowIfNull(this.DefaultValue, nameof(this.DefaultValue)); - - // Push it through the With* methods again, which contain validation. - WithName(this.Name); - WithDescription(this.Description); - WithAttributes(this.Attributes); - WithType(this.Type); - WithDefaultValue(this.DefaultValue); - - return new CommandParameter() - { - Name = this.Name, - Description = this.Description, - Type = this.Type, - Attributes = this.Attributes, - DefaultValue = this.DefaultValue, - Parent = command, - }; - } - - public static CommandParameterBuilder From(ParameterInfo parameterInfo) - { - ArgumentNullException.ThrowIfNull(parameterInfo, nameof(parameterInfo)); - if (parameterInfo.ParameterType.IsAssignableTo(typeof(CommandContext))) - { - throw new ArgumentException("The parameter cannot be a CommandContext.", nameof(parameterInfo)); - } - - CommandParameterBuilder commandParameterBuilder = new(); - commandParameterBuilder.WithAttributes(parameterInfo.GetCustomAttributes()); - commandParameterBuilder.WithType(parameterInfo.ParameterType); - if (parameterInfo.HasDefaultValue) - { - commandParameterBuilder.WithDefaultValue(parameterInfo.DefaultValue); - } - - if (parameterInfo.GetCustomAttribute() is ParameterAttribute attribute) - { - commandParameterBuilder.WithName(attribute.Name); - } - else if (!string.IsNullOrWhiteSpace(parameterInfo.Name)) - { - commandParameterBuilder.WithName(parameterInfo.Name); - } - - // Might be set by the `DescriptionAttribute` - if (string.IsNullOrWhiteSpace(commandParameterBuilder.Description)) - { - commandParameterBuilder.WithDescription("No description provided."); - } - - return commandParameterBuilder; - } - - /// - public override string ToString() - { - StringBuilder stringBuilder = new(); - if (this.Parent is not null) - { - stringBuilder.Append(this.Parent.FullName); - stringBuilder.Append('.'); - } - - stringBuilder.Append(this.Name ?? "Unnamed Parameter"); - return stringBuilder.ToString(); - } -} +#pragma warning disable CA2264 + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Reflection; +using System.Text; +using DSharpPlus.Commands.ArgumentModifiers; +using DSharpPlus.Entities; + +namespace DSharpPlus.Commands.Trees; + +[DebuggerDisplay("{ToString()}")] +public partial class CommandParameterBuilder +{ + public string? Name { get; set; } + public string? Description { get; set; } + public Type? Type { get; set; } + public List Attributes { get; set; } = []; + public Optional DefaultValue { get; set; } = Optional.FromNoValue(); + public CommandBuilder? Parent { get; set; } + + public CommandParameterBuilder WithName(string name) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentNullException(nameof(name), "The name of the command cannot be null or whitespace."); + } + + this.Name = name; + return this; + } + + public CommandParameterBuilder WithDescription(string? description) + { + this.Description = description; + return this; + } + + public CommandParameterBuilder WithType(Type type) + { + this.Type = type; + return this; + } + + public CommandParameterBuilder WithAttributes(IEnumerable attributes) + { + List listedAttributes = []; + foreach (Attribute attribute in attributes) + { + if (attribute is CommandAttribute commandAttribute) + { + WithName(commandAttribute.Name); + } + else if (attribute is DescriptionAttribute descriptionAttribute) + { + WithDescription(descriptionAttribute.Description); + } + else if (attribute is ParamArrayAttribute && !this.Attributes.Any(attribute => attribute is VariadicArgumentAttribute)) + { + // Transform the params into a VariadicArgumentAttribute + listedAttributes.Add(new VariadicArgumentAttribute(int.MaxValue)); + } + + listedAttributes.Add(attribute); + } + + this.Attributes = listedAttributes; + return this; + } + + public CommandParameterBuilder WithDefaultValue(Optional defaultValue) + { + this.DefaultValue = defaultValue; + return this; + } + + public CommandParameterBuilder WithParent(CommandBuilder parent) + { + this.Parent = parent; + return this; + } + + [MemberNotNull(nameof(Name), nameof(Description), nameof(Type), nameof(Attributes))] + public CommandParameter Build(Command command) + { + ArgumentNullException.ThrowIfNull(this.Name, nameof(this.Name)); + ArgumentNullException.ThrowIfNull(this.Description, nameof(this.Description)); + ArgumentNullException.ThrowIfNull(this.Type, nameof(this.Type)); + ArgumentNullException.ThrowIfNull(this.Attributes, nameof(this.Attributes)); + ArgumentNullException.ThrowIfNull(this.DefaultValue, nameof(this.DefaultValue)); + + // Push it through the With* methods again, which contain validation. + WithName(this.Name); + WithDescription(this.Description); + WithAttributes(this.Attributes); + WithType(this.Type); + WithDefaultValue(this.DefaultValue); + + return new CommandParameter() + { + Name = this.Name, + Description = this.Description, + Type = this.Type, + Attributes = this.Attributes, + DefaultValue = this.DefaultValue, + Parent = command, + }; + } + + public static CommandParameterBuilder From(ParameterInfo parameterInfo) + { + ArgumentNullException.ThrowIfNull(parameterInfo, nameof(parameterInfo)); + if (parameterInfo.ParameterType.IsAssignableTo(typeof(CommandContext))) + { + throw new ArgumentException("The parameter cannot be a CommandContext.", nameof(parameterInfo)); + } + + CommandParameterBuilder commandParameterBuilder = new(); + commandParameterBuilder.WithAttributes(parameterInfo.GetCustomAttributes()); + commandParameterBuilder.WithType(parameterInfo.ParameterType); + if (parameterInfo.HasDefaultValue) + { + commandParameterBuilder.WithDefaultValue(parameterInfo.DefaultValue); + } + + if (parameterInfo.GetCustomAttribute() is ParameterAttribute attribute) + { + commandParameterBuilder.WithName(attribute.Name); + } + else if (!string.IsNullOrWhiteSpace(parameterInfo.Name)) + { + commandParameterBuilder.WithName(parameterInfo.Name); + } + + // Might be set by the `DescriptionAttribute` + if (string.IsNullOrWhiteSpace(commandParameterBuilder.Description)) + { + commandParameterBuilder.WithDescription("No description provided."); + } + + return commandParameterBuilder; + } + + /// + public override string ToString() + { + StringBuilder stringBuilder = new(); + if (this.Parent is not null) + { + stringBuilder.Append(this.Parent.FullName); + stringBuilder.Append('.'); + } + + stringBuilder.Append(this.Name ?? "Unnamed Parameter"); + return stringBuilder.ToString(); + } +} diff --git a/DSharpPlus.Commands/Trees/Metadata/AllowDMUsageAttribute.cs b/DSharpPlus.Commands/Trees/Metadata/AllowDMUsageAttribute.cs index cd472726da..1bd20a33e5 100644 --- a/DSharpPlus.Commands/Trees/Metadata/AllowDMUsageAttribute.cs +++ b/DSharpPlus.Commands/Trees/Metadata/AllowDMUsageAttribute.cs @@ -1,6 +1,6 @@ -using System; - -namespace DSharpPlus.Commands.Trees.Metadata; - -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] -public sealed class AllowDMUsageAttribute : Attribute; +using System; + +namespace DSharpPlus.Commands.Trees.Metadata; + +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] +public sealed class AllowDMUsageAttribute : Attribute; diff --git a/DSharpPlus.Commands/Trees/Metadata/AllowedProcessorsAttribute.cs b/DSharpPlus.Commands/Trees/Metadata/AllowedProcessorsAttribute.cs index a0cf4fd7fc..56a444a359 100644 --- a/DSharpPlus.Commands/Trees/Metadata/AllowedProcessorsAttribute.cs +++ b/DSharpPlus.Commands/Trees/Metadata/AllowedProcessorsAttribute.cs @@ -1,102 +1,102 @@ -using System; -using System.Linq; -using DSharpPlus.Commands.Processors; -using DSharpPlus.Commands.Processors.MessageCommands; -using DSharpPlus.Commands.Processors.SlashCommands; -using DSharpPlus.Commands.Processors.UserCommands; - -namespace DSharpPlus.Commands.Trees.Metadata; - -/// -/// Allows to restrict commands to certain processors. -/// -/// -/// This attribute only works on top-level commands. -/// -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] -public class AllowedProcessorsAttribute : Attribute -{ - /// - /// Specifies which processors are allowed to execute this command. - /// - /// Types of processors that are allowed to execute this command. - public AllowedProcessorsAttribute(params Type[] processors) - { - if (processors.Length < 1) - { - throw new ArgumentException("Provide atleast one processor", nameof(processors)); - } - - if (!processors.All(x => x.IsAssignableTo(typeof(ICommandProcessor)))) - { - throw new ArgumentException( - "All processors must implement ICommandProcessor.", - nameof(processors) - ); - } - - this.Processors = (processors.Contains(typeof(MessageCommandProcessor)) - || processors.Contains(typeof(UserCommandProcessor))) && !processors.Contains(typeof(SlashCommandProcessor)) - ? [.. processors, typeof(SlashCommandProcessor)] - : processors; - } - - /// - /// Types of allowed processors - /// - public Type[] Processors { get; private set; } -} - -/// -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] -public class AllowedProcessorsAttribute : AllowedProcessorsAttribute where T : ICommandProcessor -{ - /// - public AllowedProcessorsAttribute() : base(typeof(T)) { } -} - -/// -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] -public class AllowedProcessorsAttribute : AllowedProcessorsAttribute - where T1 : ICommandProcessor - where T2 : ICommandProcessor -{ - /// - public AllowedProcessorsAttribute() : base(typeof(T1), typeof(T2)) { } -} - -/// -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] -public class AllowedProcessorsAttribute : AllowedProcessorsAttribute - where T1 : ICommandProcessor - where T2 : ICommandProcessor - where T3 : ICommandProcessor -{ - /// - public AllowedProcessorsAttribute() : base(typeof(T1), typeof(T2), typeof(T3)) { } -} - -/// -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] -public class AllowedProcessorsAttribute : AllowedProcessorsAttribute - where T1 : ICommandProcessor - where T2 : ICommandProcessor - where T3 : ICommandProcessor - where T4 : ICommandProcessor -{ - /// - public AllowedProcessorsAttribute() : base(typeof(T1), typeof(T2), typeof(T3), typeof(T4)) { } -} - -/// -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] -public class AllowedProcessorsAttribute : AllowedProcessorsAttribute - where T1 : ICommandProcessor - where T2 : ICommandProcessor - where T3 : ICommandProcessor - where T4 : ICommandProcessor - where T5 : ICommandProcessor -{ - /// - public AllowedProcessorsAttribute() : base(typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5)) { } -} +using System; +using System.Linq; +using DSharpPlus.Commands.Processors; +using DSharpPlus.Commands.Processors.MessageCommands; +using DSharpPlus.Commands.Processors.SlashCommands; +using DSharpPlus.Commands.Processors.UserCommands; + +namespace DSharpPlus.Commands.Trees.Metadata; + +/// +/// Allows to restrict commands to certain processors. +/// +/// +/// This attribute only works on top-level commands. +/// +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] +public class AllowedProcessorsAttribute : Attribute +{ + /// + /// Specifies which processors are allowed to execute this command. + /// + /// Types of processors that are allowed to execute this command. + public AllowedProcessorsAttribute(params Type[] processors) + { + if (processors.Length < 1) + { + throw new ArgumentException("Provide atleast one processor", nameof(processors)); + } + + if (!processors.All(x => x.IsAssignableTo(typeof(ICommandProcessor)))) + { + throw new ArgumentException( + "All processors must implement ICommandProcessor.", + nameof(processors) + ); + } + + this.Processors = (processors.Contains(typeof(MessageCommandProcessor)) + || processors.Contains(typeof(UserCommandProcessor))) && !processors.Contains(typeof(SlashCommandProcessor)) + ? [.. processors, typeof(SlashCommandProcessor)] + : processors; + } + + /// + /// Types of allowed processors + /// + public Type[] Processors { get; private set; } +} + +/// +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] +public class AllowedProcessorsAttribute : AllowedProcessorsAttribute where T : ICommandProcessor +{ + /// + public AllowedProcessorsAttribute() : base(typeof(T)) { } +} + +/// +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] +public class AllowedProcessorsAttribute : AllowedProcessorsAttribute + where T1 : ICommandProcessor + where T2 : ICommandProcessor +{ + /// + public AllowedProcessorsAttribute() : base(typeof(T1), typeof(T2)) { } +} + +/// +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] +public class AllowedProcessorsAttribute : AllowedProcessorsAttribute + where T1 : ICommandProcessor + where T2 : ICommandProcessor + where T3 : ICommandProcessor +{ + /// + public AllowedProcessorsAttribute() : base(typeof(T1), typeof(T2), typeof(T3)) { } +} + +/// +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] +public class AllowedProcessorsAttribute : AllowedProcessorsAttribute + where T1 : ICommandProcessor + where T2 : ICommandProcessor + where T3 : ICommandProcessor + where T4 : ICommandProcessor +{ + /// + public AllowedProcessorsAttribute() : base(typeof(T1), typeof(T2), typeof(T3), typeof(T4)) { } +} + +/// +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] +public class AllowedProcessorsAttribute : AllowedProcessorsAttribute + where T1 : ICommandProcessor + where T2 : ICommandProcessor + where T3 : ICommandProcessor + where T4 : ICommandProcessor + where T5 : ICommandProcessor +{ + /// + public AllowedProcessorsAttribute() : base(typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5)) { } +} diff --git a/DSharpPlus.Commands/Trees/Metadata/DefaultGroupCommandAttribute.cs b/DSharpPlus.Commands/Trees/Metadata/DefaultGroupCommandAttribute.cs index 05ae9c98bb..ee5209da9d 100644 --- a/DSharpPlus.Commands/Trees/Metadata/DefaultGroupCommandAttribute.cs +++ b/DSharpPlus.Commands/Trees/Metadata/DefaultGroupCommandAttribute.cs @@ -1,6 +1,6 @@ -using System; - -namespace DSharpPlus.Commands.Trees.Metadata; - -[AttributeUsage(AttributeTargets.Delegate | AttributeTargets.Method)] -public sealed class DefaultGroupCommandAttribute : Attribute; +using System; + +namespace DSharpPlus.Commands.Trees.Metadata; + +[AttributeUsage(AttributeTargets.Delegate | AttributeTargets.Method)] +public sealed class DefaultGroupCommandAttribute : Attribute; diff --git a/DSharpPlus.Commands/Trees/Metadata/TextAliasAttribute.cs b/DSharpPlus.Commands/Trees/Metadata/TextAliasAttribute.cs index cbeb5af058..b230d76d8a 100644 --- a/DSharpPlus.Commands/Trees/Metadata/TextAliasAttribute.cs +++ b/DSharpPlus.Commands/Trees/Metadata/TextAliasAttribute.cs @@ -1,9 +1,9 @@ -using System; - -namespace DSharpPlus.Commands.Trees.Metadata; - -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Delegate)] -public sealed class TextAliasAttribute(params string[] aliases) : Attribute -{ - public string[] Aliases { get; init; } = aliases; -} +using System; + +namespace DSharpPlus.Commands.Trees.Metadata; + +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Delegate)] +public sealed class TextAliasAttribute(params string[] aliases) : Attribute +{ + public string[] Aliases { get; init; } = aliases; +} diff --git a/DSharpPlus.CommandsNext/Attributes/AliasesAttribute.cs b/DSharpPlus.CommandsNext/Attributes/AliasesAttribute.cs index c65df24352..1ea538655f 100644 --- a/DSharpPlus.CommandsNext/Attributes/AliasesAttribute.cs +++ b/DSharpPlus.CommandsNext/Attributes/AliasesAttribute.cs @@ -1,32 +1,32 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; - -namespace DSharpPlus.CommandsNext.Attributes; - -/// -/// Adds aliases to this command or group. -/// -[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false)] -public sealed class AliasesAttribute : Attribute -{ - /// - /// Gets this group's aliases. - /// - public IReadOnlyList Aliases { get; } - - /// - /// Adds aliases to this command or group. - /// - /// Aliases to add to this command or group. - public AliasesAttribute(params string[] aliases) - { - if (aliases.Any(xa => xa == null || xa.Any(xc => char.IsWhiteSpace(xc)))) - { - throw new ArgumentException("Aliases cannot contain whitespace characters or null strings.", nameof(aliases)); - } - - this.Aliases = new ReadOnlyCollection(aliases); - } -} +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; + +namespace DSharpPlus.CommandsNext.Attributes; + +/// +/// Adds aliases to this command or group. +/// +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false)] +public sealed class AliasesAttribute : Attribute +{ + /// + /// Gets this group's aliases. + /// + public IReadOnlyList Aliases { get; } + + /// + /// Adds aliases to this command or group. + /// + /// Aliases to add to this command or group. + public AliasesAttribute(params string[] aliases) + { + if (aliases.Any(xa => xa == null || xa.Any(xc => char.IsWhiteSpace(xc)))) + { + throw new ArgumentException("Aliases cannot contain whitespace characters or null strings.", nameof(aliases)); + } + + this.Aliases = new ReadOnlyCollection(aliases); + } +} diff --git a/DSharpPlus.CommandsNext/Attributes/CategoryAttribute.cs b/DSharpPlus.CommandsNext/Attributes/CategoryAttribute.cs index ef4790ac31..ae9e4ff015 100644 --- a/DSharpPlus.CommandsNext/Attributes/CategoryAttribute.cs +++ b/DSharpPlus.CommandsNext/Attributes/CategoryAttribute.cs @@ -1,19 +1,19 @@ -using System; - -namespace DSharpPlus.CommandsNext.Attributes; - -[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = true)] -public sealed class CategoryAttribute : Attribute -{ - public string? Name { get; } - - public CategoryAttribute(string? name) - { - if (string.IsNullOrWhiteSpace(name)) - { - throw new ArgumentNullException(nameof(name), "Command category names cannot be null, empty, or all-whitespace."); - } - - this.Name = name; - } -} +using System; + +namespace DSharpPlus.CommandsNext.Attributes; + +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = true)] +public sealed class CategoryAttribute : Attribute +{ + public string? Name { get; } + + public CategoryAttribute(string? name) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentNullException(nameof(name), "Command category names cannot be null, empty, or all-whitespace."); + } + + this.Name = name; + } +} diff --git a/DSharpPlus.CommandsNext/Attributes/CheckBaseAttribute.cs b/DSharpPlus.CommandsNext/Attributes/CheckBaseAttribute.cs index 9bfaa64830..53351883d7 100644 --- a/DSharpPlus.CommandsNext/Attributes/CheckBaseAttribute.cs +++ b/DSharpPlus.CommandsNext/Attributes/CheckBaseAttribute.cs @@ -1,19 +1,19 @@ -using System; -using System.Threading.Tasks; - -namespace DSharpPlus.CommandsNext.Attributes; - -/// -/// Represents a base for all command pre-execution check attributes. -/// -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] -public abstract class CheckBaseAttribute : Attribute -{ - /// - /// Asynchronously checks whether this command can be executed within given context. - /// - /// Context to check execution ability for. - /// Whether this check is being executed from help or not. This can be used to probe whether command can be run without setting off certain fail conditions (such as cooldowns). - /// Whether the command can be executed in given context. - public abstract Task ExecuteCheckAsync(CommandContext ctx, bool help); -} +using System; +using System.Threading.Tasks; + +namespace DSharpPlus.CommandsNext.Attributes; + +/// +/// Represents a base for all command pre-execution check attributes. +/// +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] +public abstract class CheckBaseAttribute : Attribute +{ + /// + /// Asynchronously checks whether this command can be executed within given context. + /// + /// Context to check execution ability for. + /// Whether this check is being executed from help or not. This can be used to probe whether command can be run without setting off certain fail conditions (such as cooldowns). + /// Whether the command can be executed in given context. + public abstract Task ExecuteCheckAsync(CommandContext ctx, bool help); +} diff --git a/DSharpPlus.CommandsNext/Attributes/CommandAttribute.cs b/DSharpPlus.CommandsNext/Attributes/CommandAttribute.cs index 477220c3fb..254d9bed1e 100644 --- a/DSharpPlus.CommandsNext/Attributes/CommandAttribute.cs +++ b/DSharpPlus.CommandsNext/Attributes/CommandAttribute.cs @@ -1,53 +1,53 @@ -using System; -using System.Linq; - -namespace DSharpPlus.CommandsNext.Attributes; - -/// -/// Marks this method as a command. -/// -[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] -public sealed class CommandAttribute : Attribute -{ - /// - /// Gets the name of this command. - /// - public string? Name { get; } - - /// - /// Marks this method as a command, using the method's name as command name. - /// - public CommandAttribute() => this.Name = null; - - /// - /// Marks this method as a command with specified name. - /// - /// Name of this command. - public CommandAttribute(string name) - { - if (string.IsNullOrWhiteSpace(name)) - { - throw new ArgumentNullException(nameof(name), "Command names cannot be null, empty, or all-whitespace."); - } - - if (name.Any(xc => char.IsWhiteSpace(xc))) - { - throw new ArgumentException("Command names cannot contain whitespace characters.", nameof(name)); - } - - this.Name = name; - } -} - -/// -/// Marks this method as a group command. -/// -[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] -public sealed class GroupCommandAttribute : Attribute -{ - /// - /// Marks this method as a group command. - /// - public GroupCommandAttribute() - { } -} +using System; +using System.Linq; + +namespace DSharpPlus.CommandsNext.Attributes; + +/// +/// Marks this method as a command. +/// +[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] +public sealed class CommandAttribute : Attribute +{ + /// + /// Gets the name of this command. + /// + public string? Name { get; } + + /// + /// Marks this method as a command, using the method's name as command name. + /// + public CommandAttribute() => this.Name = null; + + /// + /// Marks this method as a command with specified name. + /// + /// Name of this command. + public CommandAttribute(string name) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentNullException(nameof(name), "Command names cannot be null, empty, or all-whitespace."); + } + + if (name.Any(xc => char.IsWhiteSpace(xc))) + { + throw new ArgumentException("Command names cannot contain whitespace characters.", nameof(name)); + } + + this.Name = name; + } +} + +/// +/// Marks this method as a group command. +/// +[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] +public sealed class GroupCommandAttribute : Attribute +{ + /// + /// Marks this method as a group command. + /// + public GroupCommandAttribute() + { } +} diff --git a/DSharpPlus.CommandsNext/Attributes/CooldownAttribute.cs b/DSharpPlus.CommandsNext/Attributes/CooldownAttribute.cs index 0ba44e6452..5ce21cfb29 100644 --- a/DSharpPlus.CommandsNext/Attributes/CooldownAttribute.cs +++ b/DSharpPlus.CommandsNext/Attributes/CooldownAttribute.cs @@ -1,330 +1,330 @@ -using System; -using System.Collections.Concurrent; -using System.Globalization; -using System.Threading; -using System.Threading.Tasks; - -namespace DSharpPlus.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 static readonly ConcurrentDictionary buckets = new(); - - /// - /// 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; - } - - /// - /// 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) - { - string bid = GetBucketId(ctx, out _, out _, out _); - buckets.TryGetValue(bid, out CommandCooldownBucket? 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) - { - CommandCooldownBucket? bucket = GetBucket(ctx); - return bucket is null || 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.HasFlag(CooldownBucketType.User)) - { - userId = ctx.User.Id; - } - - channelId = 0ul; - if (this.BucketType.HasFlag(CooldownBucketType.Channel)) - { - channelId = ctx.Channel.Id; - } - - guildId = 0ul; - if (this.BucketType.HasFlag(CooldownBucketType.Guild)) - { - if (ctx.Guild == null) - { - channelId = ctx.Channel.Id; - } - else - { - guildId = ctx.Guild.Id; - } - } - - string bucketId = CommandCooldownBucket.MakeId(ctx.Command!.QualifiedName, ctx.Client.CurrentUser.Id, userId, channelId, guildId); - return bucketId; - } - - public override async Task ExecuteCheckAsync(CommandContext ctx, bool help) - { - if (help) - { - return true; - } - - string bucketId = GetBucketId(ctx, out ulong userId, out ulong channelId, out ulong guildId); - if (!buckets.TryGetValue(bucketId, out CommandCooldownBucket? bucket)) - { - bucket = new CommandCooldownBucket(ctx.Command!.QualifiedName, ctx.Client.CurrentUser.Id, this.MaxUses, this.Reset, userId, channelId, guildId); - buckets.AddOrUpdate(bucketId, bucket, (_, _) => bucket); - } - - return await bucket.DecrementUseAsync(); - } -} - -/// -/// 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 -{ - /// - /// The command's full name (includes groups and subcommands). - /// - public string FullCommandName { get; } - - /// - /// The Id of the bot. - /// - public ulong BotId { get; } - - /// - /// 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; } - - /// - /// Creates a new command cooldown bucket. - /// - /// Full name of the command. - /// ID of the bot. - /// 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(string fullCommandName, ulong botId, int maxUses, TimeSpan resetAfter, ulong userId = 0, ulong channelId = 0, ulong guildId = 0) - { - this.FullCommandName = fullCommandName; - this.BotId = botId; - this.MaxUses = maxUses; - this.ResetsAt = DateTimeOffset.UtcNow + resetAfter; - this.Reset = resetAfter; - this.UserId = userId; - this.ChannelId = channelId; - this.GuildId = guildId; - this.BucketId = MakeId(fullCommandName, botId, userId, channelId, guildId); - this.remainingUses = maxUses; - this.usageSemaphore = new SemaphoreSlim(1, 1); - } - - /// - /// Decrements the remaining use counter. - /// - /// Whether decrement succeded or not. - internal async Task DecrementUseAsync() - { - await this.usageSemaphore.WaitAsync(); - - // if we're past reset time... - DateTimeOffset 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... - bool success = false; - if (this.RemainingUses > 0) - { - // ...decrement, and return success... - Interlocked.Decrement(ref this.remainingUses); - success = true; - } - - // ...otherwise just fail - 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) => obj is CommandCooldownBucket cooldownBucket && Equals(cooldownBucket); - - /// - /// 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) - { - bool null1 = bucket1 is null; - bool 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. - /// - /// Full name of the command with which this cooldown is associated. - /// ID of the bot with which this cooldown is associated. - /// 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(string fullCommandName, ulong botId, ulong userId = 0, ulong channelId = 0, ulong guildId = 0) - => $"{userId.ToString(CultureInfo.InvariantCulture)}:{channelId.ToString(CultureInfo.InvariantCulture)}:{guildId.ToString(CultureInfo.InvariantCulture)}:{botId}:{fullCommandName}"; -} +using System; +using System.Collections.Concurrent; +using System.Globalization; +using System.Threading; +using System.Threading.Tasks; + +namespace DSharpPlus.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 static readonly ConcurrentDictionary buckets = new(); + + /// + /// 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; + } + + /// + /// 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) + { + string bid = GetBucketId(ctx, out _, out _, out _); + buckets.TryGetValue(bid, out CommandCooldownBucket? 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) + { + CommandCooldownBucket? bucket = GetBucket(ctx); + return bucket is null || 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.HasFlag(CooldownBucketType.User)) + { + userId = ctx.User.Id; + } + + channelId = 0ul; + if (this.BucketType.HasFlag(CooldownBucketType.Channel)) + { + channelId = ctx.Channel.Id; + } + + guildId = 0ul; + if (this.BucketType.HasFlag(CooldownBucketType.Guild)) + { + if (ctx.Guild == null) + { + channelId = ctx.Channel.Id; + } + else + { + guildId = ctx.Guild.Id; + } + } + + string bucketId = CommandCooldownBucket.MakeId(ctx.Command!.QualifiedName, ctx.Client.CurrentUser.Id, userId, channelId, guildId); + return bucketId; + } + + public override async Task ExecuteCheckAsync(CommandContext ctx, bool help) + { + if (help) + { + return true; + } + + string bucketId = GetBucketId(ctx, out ulong userId, out ulong channelId, out ulong guildId); + if (!buckets.TryGetValue(bucketId, out CommandCooldownBucket? bucket)) + { + bucket = new CommandCooldownBucket(ctx.Command!.QualifiedName, ctx.Client.CurrentUser.Id, this.MaxUses, this.Reset, userId, channelId, guildId); + buckets.AddOrUpdate(bucketId, bucket, (_, _) => bucket); + } + + return await bucket.DecrementUseAsync(); + } +} + +/// +/// 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 +{ + /// + /// The command's full name (includes groups and subcommands). + /// + public string FullCommandName { get; } + + /// + /// The Id of the bot. + /// + public ulong BotId { get; } + + /// + /// 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; } + + /// + /// Creates a new command cooldown bucket. + /// + /// Full name of the command. + /// ID of the bot. + /// 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(string fullCommandName, ulong botId, int maxUses, TimeSpan resetAfter, ulong userId = 0, ulong channelId = 0, ulong guildId = 0) + { + this.FullCommandName = fullCommandName; + this.BotId = botId; + this.MaxUses = maxUses; + this.ResetsAt = DateTimeOffset.UtcNow + resetAfter; + this.Reset = resetAfter; + this.UserId = userId; + this.ChannelId = channelId; + this.GuildId = guildId; + this.BucketId = MakeId(fullCommandName, botId, userId, channelId, guildId); + this.remainingUses = maxUses; + this.usageSemaphore = new SemaphoreSlim(1, 1); + } + + /// + /// Decrements the remaining use counter. + /// + /// Whether decrement succeded or not. + internal async Task DecrementUseAsync() + { + await this.usageSemaphore.WaitAsync(); + + // if we're past reset time... + DateTimeOffset 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... + bool success = false; + if (this.RemainingUses > 0) + { + // ...decrement, and return success... + Interlocked.Decrement(ref this.remainingUses); + success = true; + } + + // ...otherwise just fail + 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) => obj is CommandCooldownBucket cooldownBucket && Equals(cooldownBucket); + + /// + /// 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) + { + bool null1 = bucket1 is null; + bool 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. + /// + /// Full name of the command with which this cooldown is associated. + /// ID of the bot with which this cooldown is associated. + /// 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(string fullCommandName, ulong botId, ulong userId = 0, ulong channelId = 0, ulong guildId = 0) + => $"{userId.ToString(CultureInfo.InvariantCulture)}:{channelId.ToString(CultureInfo.InvariantCulture)}:{guildId.ToString(CultureInfo.InvariantCulture)}:{botId}:{fullCommandName}"; +} diff --git a/DSharpPlus.CommandsNext/Attributes/DescriptionAttribute.cs b/DSharpPlus.CommandsNext/Attributes/DescriptionAttribute.cs index b14c44ad7a..333f511fa3 100644 --- a/DSharpPlus.CommandsNext/Attributes/DescriptionAttribute.cs +++ b/DSharpPlus.CommandsNext/Attributes/DescriptionAttribute.cs @@ -1,21 +1,21 @@ -using System; - -namespace DSharpPlus.CommandsNext.Attributes; - -/// -/// Gives this command, group, or argument a description, which is used when listing help. -/// -[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Parameter, AllowMultiple = false)] -public sealed class DescriptionAttribute : Attribute -{ - /// - /// Gets the description for this command, group, or argument. - /// - public string Description { get; } - - /// - /// Gives this command, group, or argument a description, which is used when listing help. - /// - /// - public DescriptionAttribute(string description) => this.Description = description; -} +using System; + +namespace DSharpPlus.CommandsNext.Attributes; + +/// +/// Gives this command, group, or argument a description, which is used when listing help. +/// +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Parameter, AllowMultiple = false)] +public sealed class DescriptionAttribute : Attribute +{ + /// + /// Gets the description for this command, group, or argument. + /// + public string Description { get; } + + /// + /// Gives this command, group, or argument a description, which is used when listing help. + /// + /// + public DescriptionAttribute(string description) => this.Description = description; +} diff --git a/DSharpPlus.CommandsNext/Attributes/DontInjectAttribute.cs b/DSharpPlus.CommandsNext/Attributes/DontInjectAttribute.cs index 9a2982f13a..ff2a0d7e5d 100644 --- a/DSharpPlus.CommandsNext/Attributes/DontInjectAttribute.cs +++ b/DSharpPlus.CommandsNext/Attributes/DontInjectAttribute.cs @@ -1,10 +1,10 @@ -using System; - -namespace DSharpPlus.CommandsNext.Attributes; - -/// -/// Prevents this field or property from having its value injected by dependency injection. -/// -[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false, Inherited = true)] -public class DontInjectAttribute : Attribute -{ } +using System; + +namespace DSharpPlus.CommandsNext.Attributes; + +/// +/// Prevents this field or property from having its value injected by dependency injection. +/// +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false, Inherited = true)] +public class DontInjectAttribute : Attribute +{ } diff --git a/DSharpPlus.CommandsNext/Attributes/GroupAttribute.cs b/DSharpPlus.CommandsNext/Attributes/GroupAttribute.cs index 5122db727a..27833c792b 100644 --- a/DSharpPlus.CommandsNext/Attributes/GroupAttribute.cs +++ b/DSharpPlus.CommandsNext/Attributes/GroupAttribute.cs @@ -1,40 +1,40 @@ -using System; -using System.Linq; - -namespace DSharpPlus.CommandsNext.Attributes; - -/// -/// Marks this class as a command group. -/// -[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] -public sealed class GroupAttribute : Attribute -{ - /// - /// Gets the name of this group. - /// - public string? Name { get; } - - /// - /// Marks this class as a command group, using the class' name as group name. - /// - public GroupAttribute() => this.Name = null; - - /// - /// Marks this class as a command group with specified name. - /// - /// Name of this group. - public GroupAttribute(string name) - { - if (string.IsNullOrWhiteSpace(name)) - { - throw new ArgumentNullException(nameof(name), "Group names cannot be null, empty, or all-whitespace."); - } - - if (name.Any(xc => char.IsWhiteSpace(xc))) - { - throw new ArgumentException("Group names cannot contain whitespace characters.", nameof(name)); - } - - this.Name = name; - } -} +using System; +using System.Linq; + +namespace DSharpPlus.CommandsNext.Attributes; + +/// +/// Marks this class as a command group. +/// +[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] +public sealed class GroupAttribute : Attribute +{ + /// + /// Gets the name of this group. + /// + public string? Name { get; } + + /// + /// Marks this class as a command group, using the class' name as group name. + /// + public GroupAttribute() => this.Name = null; + + /// + /// Marks this class as a command group with specified name. + /// + /// Name of this group. + public GroupAttribute(string name) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentNullException(nameof(name), "Group names cannot be null, empty, or all-whitespace."); + } + + if (name.Any(xc => char.IsWhiteSpace(xc))) + { + throw new ArgumentException("Group names cannot contain whitespace characters.", nameof(name)); + } + + this.Name = name; + } +} diff --git a/DSharpPlus.CommandsNext/Attributes/HiddenAttribute.cs b/DSharpPlus.CommandsNext/Attributes/HiddenAttribute.cs index b98a9b7972..0362ca35c1 100644 --- a/DSharpPlus.CommandsNext/Attributes/HiddenAttribute.cs +++ b/DSharpPlus.CommandsNext/Attributes/HiddenAttribute.cs @@ -1,10 +1,10 @@ -using System; - -namespace DSharpPlus.CommandsNext.Attributes; - -/// -/// Marks this command or group as hidden. -/// -[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false)] -public sealed class HiddenAttribute : Attribute -{ } +using System; + +namespace DSharpPlus.CommandsNext.Attributes; + +/// +/// Marks this command or group as hidden. +/// +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false)] +public sealed class HiddenAttribute : Attribute +{ } diff --git a/DSharpPlus.CommandsNext/Attributes/ModuleLifespanAttribute.cs b/DSharpPlus.CommandsNext/Attributes/ModuleLifespanAttribute.cs index 7d4490cfcf..56df14cc01 100644 --- a/DSharpPlus.CommandsNext/Attributes/ModuleLifespanAttribute.cs +++ b/DSharpPlus.CommandsNext/Attributes/ModuleLifespanAttribute.cs @@ -1,37 +1,37 @@ -using System; - -namespace DSharpPlus.CommandsNext.Attributes; - -/// -/// Defines a lifespan for this command module. -/// -[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] -public class ModuleLifespanAttribute : Attribute -{ - /// - /// Gets the lifespan defined for this module. - /// - public ModuleLifespan Lifespan { get; } - - /// - /// Defines a lifespan for this command module. - /// - /// Lifespan for this module. - public ModuleLifespanAttribute(ModuleLifespan lifespan) => this.Lifespan = lifespan; -} - -/// -/// Defines lifespan of a command module. -/// -public enum ModuleLifespan : int -{ - /// - /// Defines that this module will be instantiated once. - /// - Singleton = 0, - - /// - /// Defines that this module will be instantiated every time a containing command is called. - /// - Transient = 1 -} +using System; + +namespace DSharpPlus.CommandsNext.Attributes; + +/// +/// Defines a lifespan for this command module. +/// +[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] +public class ModuleLifespanAttribute : Attribute +{ + /// + /// Gets the lifespan defined for this module. + /// + public ModuleLifespan Lifespan { get; } + + /// + /// Defines a lifespan for this command module. + /// + /// Lifespan for this module. + public ModuleLifespanAttribute(ModuleLifespan lifespan) => this.Lifespan = lifespan; +} + +/// +/// Defines lifespan of a command module. +/// +public enum ModuleLifespan : int +{ + /// + /// Defines that this module will be instantiated once. + /// + Singleton = 0, + + /// + /// Defines that this module will be instantiated every time a containing command is called. + /// + Transient = 1 +} diff --git a/DSharpPlus.CommandsNext/Attributes/PriorityAttribute.cs b/DSharpPlus.CommandsNext/Attributes/PriorityAttribute.cs index 034eede1f8..849826b2d4 100644 --- a/DSharpPlus.CommandsNext/Attributes/PriorityAttribute.cs +++ b/DSharpPlus.CommandsNext/Attributes/PriorityAttribute.cs @@ -1,21 +1,21 @@ -using System; - -namespace DSharpPlus.CommandsNext.Attributes; - -/// -/// Defines this command overload's priority. This determines the order in which overloads will be attempted to be called. Commands will be attempted in order of priority, in descending order. -/// -[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] -public sealed class PriorityAttribute : Attribute -{ - /// - /// Gets the priority of this command overload. - /// - public int Priority { get; } - - /// - /// Defines this command overload's priority. This determines the order in which overloads will be attempted to be called. Commands will be attempted in order of priority, in descending order. - /// - /// Priority of this command overload. - public PriorityAttribute(int priority) => this.Priority = priority; -} +using System; + +namespace DSharpPlus.CommandsNext.Attributes; + +/// +/// Defines this command overload's priority. This determines the order in which overloads will be attempted to be called. Commands will be attempted in order of priority, in descending order. +/// +[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] +public sealed class PriorityAttribute : Attribute +{ + /// + /// Gets the priority of this command overload. + /// + public int Priority { get; } + + /// + /// Defines this command overload's priority. This determines the order in which overloads will be attempted to be called. Commands will be attempted in order of priority, in descending order. + /// + /// Priority of this command overload. + public PriorityAttribute(int priority) => this.Priority = priority; +} diff --git a/DSharpPlus.CommandsNext/Attributes/RemainingTextAttribute.cs b/DSharpPlus.CommandsNext/Attributes/RemainingTextAttribute.cs index fac04a4022..ee47a2ae73 100644 --- a/DSharpPlus.CommandsNext/Attributes/RemainingTextAttribute.cs +++ b/DSharpPlus.CommandsNext/Attributes/RemainingTextAttribute.cs @@ -1,10 +1,10 @@ -using System; - -namespace DSharpPlus.CommandsNext.Attributes; - -/// -/// Indicates that the command argument takes the rest of the input without parsing. -/// -[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)] -public class RemainingTextAttribute : Attribute -{ } +using System; + +namespace DSharpPlus.CommandsNext.Attributes; + +/// +/// Indicates that the command argument takes the rest of the input without parsing. +/// +[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)] +public class RemainingTextAttribute : Attribute +{ } diff --git a/DSharpPlus.CommandsNext/Attributes/RequireBotPermissionsAttribute.cs b/DSharpPlus.CommandsNext/Attributes/RequireBotPermissionsAttribute.cs index cb5c272355..e4fb78793f 100644 --- a/DSharpPlus.CommandsNext/Attributes/RequireBotPermissionsAttribute.cs +++ b/DSharpPlus.CommandsNext/Attributes/RequireBotPermissionsAttribute.cs @@ -1,56 +1,56 @@ -using System; -using System.Threading.Tasks; -using DSharpPlus.Entities; - -namespace DSharpPlus.CommandsNext.Attributes; - -/// -/// Defines that usage of this command is only possible when the bot is granted a specific permission. -/// -[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = false)] -public sealed class RequireBotPermissionsAttribute : CheckBaseAttribute -{ - /// - /// Gets the permissions required by this attribute. - /// - public DiscordPermission[] Permissions { get; } - - /// - /// Gets this check's behaviour in DMs. True means the check will always pass in DMs, whereas false means that it will always fail. - /// - public bool IgnoreDms { get; } = true; - - /// - /// Defines that usage of this command is only possible when the bot is granted a specific permission. - /// - /// Permissions required to execute this command. - /// Sets this check's behaviour in DMs. True means the check will always pass in DMs, whereas false means that it will always fail. - public RequireBotPermissionsAttribute(bool ignoreDms = true, params DiscordPermission[] permissions) - { - this.Permissions = permissions; - this.IgnoreDms = ignoreDms; - } - - public override async Task ExecuteCheckAsync(CommandContext ctx, bool help) - { - if (ctx.Guild == null) - { - return this.IgnoreDms; - } - - DiscordMember bot = await ctx.Guild.GetMemberAsync(ctx.Client.CurrentUser.Id); - if (bot == null) - { - return false; - } - - if (bot.Id == ctx.Guild.OwnerId) - { - return true; - } - - DiscordPermissions pbot = ctx.Channel.PermissionsFor(bot); - - return pbot.HasAllPermissions(this.Permissions); - } -} +using System; +using System.Threading.Tasks; +using DSharpPlus.Entities; + +namespace DSharpPlus.CommandsNext.Attributes; + +/// +/// Defines that usage of this command is only possible when the bot is granted a specific permission. +/// +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = false)] +public sealed class RequireBotPermissionsAttribute : CheckBaseAttribute +{ + /// + /// Gets the permissions required by this attribute. + /// + public DiscordPermission[] Permissions { get; } + + /// + /// Gets this check's behaviour in DMs. True means the check will always pass in DMs, whereas false means that it will always fail. + /// + public bool IgnoreDms { get; } = true; + + /// + /// Defines that usage of this command is only possible when the bot is granted a specific permission. + /// + /// Permissions required to execute this command. + /// Sets this check's behaviour in DMs. True means the check will always pass in DMs, whereas false means that it will always fail. + public RequireBotPermissionsAttribute(bool ignoreDms = true, params DiscordPermission[] permissions) + { + this.Permissions = permissions; + this.IgnoreDms = ignoreDms; + } + + public override async Task ExecuteCheckAsync(CommandContext ctx, bool help) + { + if (ctx.Guild == null) + { + return this.IgnoreDms; + } + + DiscordMember bot = await ctx.Guild.GetMemberAsync(ctx.Client.CurrentUser.Id); + if (bot == null) + { + return false; + } + + if (bot.Id == ctx.Guild.OwnerId) + { + return true; + } + + DiscordPermissions pbot = ctx.Channel.PermissionsFor(bot); + + return pbot.HasAllPermissions(this.Permissions); + } +} diff --git a/DSharpPlus.CommandsNext/Attributes/RequireDirectMessageAttribute.cs b/DSharpPlus.CommandsNext/Attributes/RequireDirectMessageAttribute.cs index aa0e7aeae3..67bc862957 100644 --- a/DSharpPlus.CommandsNext/Attributes/RequireDirectMessageAttribute.cs +++ b/DSharpPlus.CommandsNext/Attributes/RequireDirectMessageAttribute.cs @@ -1,19 +1,19 @@ -using System.Threading.Tasks; -using DSharpPlus.Entities; - -namespace DSharpPlus.CommandsNext.Attributes; - -/// -/// Defines that a command is only usable within a direct message channel. -/// -public sealed class RequireDirectMessageAttribute : CheckBaseAttribute -{ - /// - /// Defines that this command is only usable within a direct message channel. - /// - public RequireDirectMessageAttribute() - { } - - public override Task ExecuteCheckAsync(CommandContext ctx, bool help) - => Task.FromResult(ctx.Channel is DiscordDmChannel); -} +using System.Threading.Tasks; +using DSharpPlus.Entities; + +namespace DSharpPlus.CommandsNext.Attributes; + +/// +/// Defines that a command is only usable within a direct message channel. +/// +public sealed class RequireDirectMessageAttribute : CheckBaseAttribute +{ + /// + /// Defines that this command is only usable within a direct message channel. + /// + public RequireDirectMessageAttribute() + { } + + public override Task ExecuteCheckAsync(CommandContext ctx, bool help) + => Task.FromResult(ctx.Channel is DiscordDmChannel); +} diff --git a/DSharpPlus.CommandsNext/Attributes/RequireGuildAttribute.cs b/DSharpPlus.CommandsNext/Attributes/RequireGuildAttribute.cs index 30bb238a07..14bbaf9f4e 100644 --- a/DSharpPlus.CommandsNext/Attributes/RequireGuildAttribute.cs +++ b/DSharpPlus.CommandsNext/Attributes/RequireGuildAttribute.cs @@ -1,18 +1,18 @@ -using System.Threading.Tasks; - -namespace DSharpPlus.CommandsNext.Attributes; - -/// -/// Defines that a command is only usable within a guild. -/// -public sealed class RequireGuildAttribute : CheckBaseAttribute -{ - /// - /// Defines that this command is only usable within a guild. - /// - public RequireGuildAttribute() - { } - - public override Task ExecuteCheckAsync(CommandContext ctx, bool help) - => Task.FromResult(ctx.Guild != null); -} +using System.Threading.Tasks; + +namespace DSharpPlus.CommandsNext.Attributes; + +/// +/// Defines that a command is only usable within a guild. +/// +public sealed class RequireGuildAttribute : CheckBaseAttribute +{ + /// + /// Defines that this command is only usable within a guild. + /// + public RequireGuildAttribute() + { } + + public override Task ExecuteCheckAsync(CommandContext ctx, bool help) + => Task.FromResult(ctx.Guild != null); +} diff --git a/DSharpPlus.CommandsNext/Attributes/RequireNsfwAttribute.cs b/DSharpPlus.CommandsNext/Attributes/RequireNsfwAttribute.cs index 7747f676a4..aae01a7ae1 100644 --- a/DSharpPlus.CommandsNext/Attributes/RequireNsfwAttribute.cs +++ b/DSharpPlus.CommandsNext/Attributes/RequireNsfwAttribute.cs @@ -1,14 +1,14 @@ -using System; -using System.Threading.Tasks; - -namespace DSharpPlus.CommandsNext.Attributes; - -/// -/// Defines that usage of this command is restricted to NSFW channels. -/// -[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = false)] -public sealed class RequireNsfwAttribute : CheckBaseAttribute -{ - public override Task ExecuteCheckAsync(CommandContext ctx, bool help) - => Task.FromResult(ctx.Channel.Guild == null || ctx.Channel.IsNSFW); -} +using System; +using System.Threading.Tasks; + +namespace DSharpPlus.CommandsNext.Attributes; + +/// +/// Defines that usage of this command is restricted to NSFW channels. +/// +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = false)] +public sealed class RequireNsfwAttribute : CheckBaseAttribute +{ + public override Task ExecuteCheckAsync(CommandContext ctx, bool help) + => Task.FromResult(ctx.Channel.Guild == null || ctx.Channel.IsNSFW); +} diff --git a/DSharpPlus.CommandsNext/Attributes/RequireOwnerAttribute.cs b/DSharpPlus.CommandsNext/Attributes/RequireOwnerAttribute.cs index e41d6fbf7a..a99a2f384d 100644 --- a/DSharpPlus.CommandsNext/Attributes/RequireOwnerAttribute.cs +++ b/DSharpPlus.CommandsNext/Attributes/RequireOwnerAttribute.cs @@ -1,20 +1,20 @@ -using System; -using System.Linq; -using System.Threading.Tasks; - -namespace DSharpPlus.CommandsNext.Attributes; - -/// -/// Defines that usage of this command is restricted to the owner of the bot. -/// -[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = false)] -public sealed class RequireOwnerAttribute : CheckBaseAttribute -{ - public override Task ExecuteCheckAsync(CommandContext ctx, bool help) - { - DSharpPlus.Entities.DiscordApplication app = ctx.Client.CurrentApplication; - DSharpPlus.Entities.DiscordUser me = ctx.Client.CurrentUser; - - return app != null ? Task.FromResult(app.Owners.Any(x => x.Id == ctx.User.Id)) : Task.FromResult(ctx.User.Id == me.Id); - } -} +using System; +using System.Linq; +using System.Threading.Tasks; + +namespace DSharpPlus.CommandsNext.Attributes; + +/// +/// Defines that usage of this command is restricted to the owner of the bot. +/// +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = false)] +public sealed class RequireOwnerAttribute : CheckBaseAttribute +{ + public override Task ExecuteCheckAsync(CommandContext ctx, bool help) + { + DSharpPlus.Entities.DiscordApplication app = ctx.Client.CurrentApplication; + DSharpPlus.Entities.DiscordUser me = ctx.Client.CurrentUser; + + return app != null ? Task.FromResult(app.Owners.Any(x => x.Id == ctx.User.Id)) : Task.FromResult(ctx.User.Id == me.Id); + } +} diff --git a/DSharpPlus.CommandsNext/Attributes/RequirePermissionsAttribute.cs b/DSharpPlus.CommandsNext/Attributes/RequirePermissionsAttribute.cs index da3cd84c75..6c3dc84d42 100644 --- a/DSharpPlus.CommandsNext/Attributes/RequirePermissionsAttribute.cs +++ b/DSharpPlus.CommandsNext/Attributes/RequirePermissionsAttribute.cs @@ -1,72 +1,72 @@ -using System; -using System.Threading.Tasks; -using DSharpPlus.Entities; - -namespace DSharpPlus.CommandsNext.Attributes; - -/// -/// Defines that usage of this command is restricted to members with specified permissions. This check also verifies that the bot has the same permissions. -/// -[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = false)] -public sealed class RequirePermissionsAttribute : CheckBaseAttribute -{ - /// - /// Gets the permissions required by this attribute. - /// - public DiscordPermission[] Permissions { get; } - - /// - /// Gets this check's behaviour in DMs. True means the check will always pass in DMs, whereas false means that it will always fail. - /// - public bool IgnoreDms { get; } = true; - - /// - /// Defines that usage of this command is restricted to members with specified permissions. This check also verifies that the bot has the same permissions. - /// - /// Permissions required to execute this command. - /// Sets this check's behaviour in DMs. True means the check will always pass in DMs, whereas false means that it will always fail. - public RequirePermissionsAttribute(bool ignoreDms = true, params DiscordPermission[] permissions) - { - this.Permissions = permissions; - this.IgnoreDms = ignoreDms; - } - - public override async Task ExecuteCheckAsync(CommandContext ctx, bool help) - { - if (ctx.Guild is null) - { - return this.IgnoreDms; - } - - DiscordMember? user = ctx.Member; - if (user is null) - { - return false; - } - - DiscordPermissions userPermissions = ctx.Channel.PermissionsFor(user); - - DiscordMember bot = await ctx.Guild.GetMemberAsync(ctx.Client.CurrentUser.Id); - if (bot is null) - { - return false; - } - - DiscordPermissions botPermissions = ctx.Channel.PermissionsFor(bot); - - bool userIsOwner = ctx.Guild.OwnerId == user.Id; - bool botIsOwner = ctx.Guild.OwnerId == bot.Id; - - if (!userIsOwner) - { - userIsOwner = userPermissions.HasAllPermissions(this.Permissions); - } - - if (!botIsOwner) - { - botIsOwner = botPermissions.HasAllPermissions(this.Permissions); - } - - return userIsOwner && botIsOwner; - } -} +using System; +using System.Threading.Tasks; +using DSharpPlus.Entities; + +namespace DSharpPlus.CommandsNext.Attributes; + +/// +/// Defines that usage of this command is restricted to members with specified permissions. This check also verifies that the bot has the same permissions. +/// +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = false)] +public sealed class RequirePermissionsAttribute : CheckBaseAttribute +{ + /// + /// Gets the permissions required by this attribute. + /// + public DiscordPermission[] Permissions { get; } + + /// + /// Gets this check's behaviour in DMs. True means the check will always pass in DMs, whereas false means that it will always fail. + /// + public bool IgnoreDms { get; } = true; + + /// + /// Defines that usage of this command is restricted to members with specified permissions. This check also verifies that the bot has the same permissions. + /// + /// Permissions required to execute this command. + /// Sets this check's behaviour in DMs. True means the check will always pass in DMs, whereas false means that it will always fail. + public RequirePermissionsAttribute(bool ignoreDms = true, params DiscordPermission[] permissions) + { + this.Permissions = permissions; + this.IgnoreDms = ignoreDms; + } + + public override async Task ExecuteCheckAsync(CommandContext ctx, bool help) + { + if (ctx.Guild is null) + { + return this.IgnoreDms; + } + + DiscordMember? user = ctx.Member; + if (user is null) + { + return false; + } + + DiscordPermissions userPermissions = ctx.Channel.PermissionsFor(user); + + DiscordMember bot = await ctx.Guild.GetMemberAsync(ctx.Client.CurrentUser.Id); + if (bot is null) + { + return false; + } + + DiscordPermissions botPermissions = ctx.Channel.PermissionsFor(bot); + + bool userIsOwner = ctx.Guild.OwnerId == user.Id; + bool botIsOwner = ctx.Guild.OwnerId == bot.Id; + + if (!userIsOwner) + { + userIsOwner = userPermissions.HasAllPermissions(this.Permissions); + } + + if (!botIsOwner) + { + botIsOwner = botPermissions.HasAllPermissions(this.Permissions); + } + + return userIsOwner && botIsOwner; + } +} diff --git a/DSharpPlus.CommandsNext/Attributes/RequirePrefixesAttribute.cs b/DSharpPlus.CommandsNext/Attributes/RequirePrefixesAttribute.cs index 1b0c284126..7e9224190e 100644 --- a/DSharpPlus.CommandsNext/Attributes/RequirePrefixesAttribute.cs +++ b/DSharpPlus.CommandsNext/Attributes/RequirePrefixesAttribute.cs @@ -1,40 +1,40 @@ -using System; -using System.Linq; -using System.Threading.Tasks; - -namespace DSharpPlus.CommandsNext.Attributes; - -/// -/// Defines that usage of this command is only allowed with specific prefixes. -/// -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] -public sealed class RequirePrefixesAttribute : CheckBaseAttribute -{ - /// - /// Gets the array of prefixes with which execution of this command is allowed. - /// - public string[] Prefixes { get; } - - /// - /// Gets or sets default help behaviour for this check. When this is enabled, invoking help without matching prefix will show the commands. - /// Defaults to false. - /// - public bool ShowInHelp { get; set; } = false; - - /// - /// Defines that usage of this command is only allowed with specific prefixes. - /// - /// Prefixes with which the execution of this command is allowed. - public RequirePrefixesAttribute(params string[] prefixes) - { - if (prefixes.Length == 0) - { - throw new ArgumentOutOfRangeException(nameof(prefixes), "At least one prefix must be provided."); - } - - this.Prefixes = prefixes; - } - - public override Task ExecuteCheckAsync(CommandContext ctx, bool help) - => Task.FromResult((help && this.ShowInHelp) || this.Prefixes.Contains(ctx.Prefix, ctx.CommandsNext.GetStringComparer())); -} +using System; +using System.Linq; +using System.Threading.Tasks; + +namespace DSharpPlus.CommandsNext.Attributes; + +/// +/// Defines that usage of this command is only allowed with specific prefixes. +/// +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] +public sealed class RequirePrefixesAttribute : CheckBaseAttribute +{ + /// + /// Gets the array of prefixes with which execution of this command is allowed. + /// + public string[] Prefixes { get; } + + /// + /// Gets or sets default help behaviour for this check. When this is enabled, invoking help without matching prefix will show the commands. + /// Defaults to false. + /// + public bool ShowInHelp { get; set; } = false; + + /// + /// Defines that usage of this command is only allowed with specific prefixes. + /// + /// Prefixes with which the execution of this command is allowed. + public RequirePrefixesAttribute(params string[] prefixes) + { + if (prefixes.Length == 0) + { + throw new ArgumentOutOfRangeException(nameof(prefixes), "At least one prefix must be provided."); + } + + this.Prefixes = prefixes; + } + + public override Task ExecuteCheckAsync(CommandContext ctx, bool help) + => Task.FromResult((help && this.ShowInHelp) || this.Prefixes.Contains(ctx.Prefix, ctx.CommandsNext.GetStringComparer())); +} diff --git a/DSharpPlus.CommandsNext/Attributes/RequireReferencedMessageAttribute.cs b/DSharpPlus.CommandsNext/Attributes/RequireReferencedMessageAttribute.cs index d57c0ec2f5..925f3f0470 100644 --- a/DSharpPlus.CommandsNext/Attributes/RequireReferencedMessageAttribute.cs +++ b/DSharpPlus.CommandsNext/Attributes/RequireReferencedMessageAttribute.cs @@ -1,18 +1,18 @@ -using System.Threading.Tasks; - -namespace DSharpPlus.CommandsNext.Attributes; - -/// -/// Defines that a command is only usable when sent in reply. Command will appear in help regardless of this attribute. -/// -public sealed class RequireReferencedMessageAttribute : CheckBaseAttribute -{ - /// - /// Defines that a command is only usable when sent in reply. Command will appear in help regardless of this attribute. - /// - public RequireReferencedMessageAttribute() - { } - - public override Task ExecuteCheckAsync(CommandContext ctx, bool help) - => Task.FromResult(help || ctx.Message.ReferencedMessage != null); -} +using System.Threading.Tasks; + +namespace DSharpPlus.CommandsNext.Attributes; + +/// +/// Defines that a command is only usable when sent in reply. Command will appear in help regardless of this attribute. +/// +public sealed class RequireReferencedMessageAttribute : CheckBaseAttribute +{ + /// + /// Defines that a command is only usable when sent in reply. Command will appear in help regardless of this attribute. + /// + public RequireReferencedMessageAttribute() + { } + + public override Task ExecuteCheckAsync(CommandContext ctx, bool help) + => Task.FromResult(help || ctx.Message.ReferencedMessage != null); +} diff --git a/DSharpPlus.CommandsNext/Attributes/RequireRolesAttribute.cs b/DSharpPlus.CommandsNext/Attributes/RequireRolesAttribute.cs index b0988bd995..b26a254e2d 100644 --- a/DSharpPlus.CommandsNext/Attributes/RequireRolesAttribute.cs +++ b/DSharpPlus.CommandsNext/Attributes/RequireRolesAttribute.cs @@ -1,141 +1,141 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; -using System.Threading.Tasks; - -namespace DSharpPlus.CommandsNext.Attributes; - -/// -/// Defines that usage of this command is restricted to members with specified role. Note that it's much preferred to restrict access using . -/// -[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = false)] -public sealed class RequireRolesAttribute : CheckBaseAttribute -{ - /// - /// Gets the names of roles required to execute this command. - /// - public IReadOnlyList RoleNames { get; } - - /// - /// Gets the IDs of roles required to execute this command. - /// - public IReadOnlyList RoleIds { get; } - - /// - /// Gets the role checking mode. Refer to for more information. - /// - public RoleCheckMode CheckMode { get; } - - /// - /// Defines that usage of this command is restricted to members with specified role. Note that it's much preferred to restrict access using . - /// - /// Role checking mode. - /// Names of the role to be verified by this check. - public RequireRolesAttribute(RoleCheckMode checkMode, params string[] roleNames) - : this(checkMode, roleNames, []) - { } - - /// - /// Defines that usage of this command is restricted to members with the specified role. - /// Note that it is much preferred to restrict access using . - /// - /// Role checking mode. - /// IDs of the roles to be verified by this check. - public RequireRolesAttribute(RoleCheckMode checkMode, params ulong[] roleIds) - : this(checkMode, [], roleIds) - { } - - /// - /// Defines that usage of this command is restricted to members with the specified role. - /// Note that it is much preferred to restrict access using . - /// - /// Role checking mode. - /// Names of the role to be verified by this check. - /// IDs of the roles to be verified by this check. - public RequireRolesAttribute(RoleCheckMode checkMode, string[] roleNames, ulong[] roleIds) - { - this.CheckMode = checkMode; - this.RoleIds = new ReadOnlyCollection(roleIds); - this.RoleNames = new ReadOnlyCollection(roleNames); - } - - public override Task ExecuteCheckAsync(CommandContext ctx, bool help) - { - if (ctx.Guild == null || ctx.Member == null) - { - return Task.FromResult(false); - } - - if ((this.CheckMode.HasFlag(RoleCheckMode.MatchNames) && !this.CheckMode.HasFlag(RoleCheckMode.MatchIds)) || this.RoleIds.Count == 0) - { - return Task.FromResult(MatchRoles( - this.RoleNames, ctx.Member.Roles.Select(xm => xm.Name), ctx.CommandsNext.GetStringComparer())); - } - else if ((!this.CheckMode.HasFlag(RoleCheckMode.MatchNames) && this.CheckMode.HasFlag(RoleCheckMode.MatchIds)) || this.RoleNames.Count == 0) - { - return Task.FromResult(MatchRoles(this.RoleIds, ctx.Member.RoleIds)); - } - else // match both names and IDs - { - bool nameMatch = MatchRoles(this.RoleNames, ctx.Member.Roles.Select(xm => xm.Name), ctx.CommandsNext.GetStringComparer()), - idMatch = MatchRoles(this.RoleIds, ctx.Member.RoleIds); - - return Task.FromResult(this.CheckMode switch - { - RoleCheckMode.Any => nameMatch || idMatch, - _ => nameMatch && idMatch - }); - } - } - - private bool MatchRoles(IReadOnlyList present, IEnumerable passed, IEqualityComparer? comparer = null) - { - IEnumerable intersect = passed.Intersect(present, comparer ?? EqualityComparer.Default); - - return this.CheckMode switch - { - RoleCheckMode.All => present.Count == intersect.Count(), - RoleCheckMode.SpecifiedOnly => passed.Count() == intersect.Count(), - RoleCheckMode.None => !intersect.Any(), - _ => intersect.Any() - }; - } -} - -/// -/// Specifies how checks for roles. -/// -[Flags] -public enum RoleCheckMode -{ - /// - /// Member is required to have none of the specified roles. - /// - None = 0, - - /// - /// Member is required to have all of the specified roles. - /// - All = 1, - - /// - /// Member is required to have any of the specified roles. - /// - Any = 2, - - /// - /// Member is required to have exactly the same roles as specified; no extra roles may be present. - /// - SpecifiedOnly = 4, - - /// - /// Instructs the check to evaluate for matching role names. - /// - MatchNames = 8, - - /// - /// Instructs the check to evaluate for matching role IDs. - /// - MatchIds = 16 -} +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Threading.Tasks; + +namespace DSharpPlus.CommandsNext.Attributes; + +/// +/// Defines that usage of this command is restricted to members with specified role. Note that it's much preferred to restrict access using . +/// +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = false)] +public sealed class RequireRolesAttribute : CheckBaseAttribute +{ + /// + /// Gets the names of roles required to execute this command. + /// + public IReadOnlyList RoleNames { get; } + + /// + /// Gets the IDs of roles required to execute this command. + /// + public IReadOnlyList RoleIds { get; } + + /// + /// Gets the role checking mode. Refer to for more information. + /// + public RoleCheckMode CheckMode { get; } + + /// + /// Defines that usage of this command is restricted to members with specified role. Note that it's much preferred to restrict access using . + /// + /// Role checking mode. + /// Names of the role to be verified by this check. + public RequireRolesAttribute(RoleCheckMode checkMode, params string[] roleNames) + : this(checkMode, roleNames, []) + { } + + /// + /// Defines that usage of this command is restricted to members with the specified role. + /// Note that it is much preferred to restrict access using . + /// + /// Role checking mode. + /// IDs of the roles to be verified by this check. + public RequireRolesAttribute(RoleCheckMode checkMode, params ulong[] roleIds) + : this(checkMode, [], roleIds) + { } + + /// + /// Defines that usage of this command is restricted to members with the specified role. + /// Note that it is much preferred to restrict access using . + /// + /// Role checking mode. + /// Names of the role to be verified by this check. + /// IDs of the roles to be verified by this check. + public RequireRolesAttribute(RoleCheckMode checkMode, string[] roleNames, ulong[] roleIds) + { + this.CheckMode = checkMode; + this.RoleIds = new ReadOnlyCollection(roleIds); + this.RoleNames = new ReadOnlyCollection(roleNames); + } + + public override Task ExecuteCheckAsync(CommandContext ctx, bool help) + { + if (ctx.Guild == null || ctx.Member == null) + { + return Task.FromResult(false); + } + + if ((this.CheckMode.HasFlag(RoleCheckMode.MatchNames) && !this.CheckMode.HasFlag(RoleCheckMode.MatchIds)) || this.RoleIds.Count == 0) + { + return Task.FromResult(MatchRoles( + this.RoleNames, ctx.Member.Roles.Select(xm => xm.Name), ctx.CommandsNext.GetStringComparer())); + } + else if ((!this.CheckMode.HasFlag(RoleCheckMode.MatchNames) && this.CheckMode.HasFlag(RoleCheckMode.MatchIds)) || this.RoleNames.Count == 0) + { + return Task.FromResult(MatchRoles(this.RoleIds, ctx.Member.RoleIds)); + } + else // match both names and IDs + { + bool nameMatch = MatchRoles(this.RoleNames, ctx.Member.Roles.Select(xm => xm.Name), ctx.CommandsNext.GetStringComparer()), + idMatch = MatchRoles(this.RoleIds, ctx.Member.RoleIds); + + return Task.FromResult(this.CheckMode switch + { + RoleCheckMode.Any => nameMatch || idMatch, + _ => nameMatch && idMatch + }); + } + } + + private bool MatchRoles(IReadOnlyList present, IEnumerable passed, IEqualityComparer? comparer = null) + { + IEnumerable intersect = passed.Intersect(present, comparer ?? EqualityComparer.Default); + + return this.CheckMode switch + { + RoleCheckMode.All => present.Count == intersect.Count(), + RoleCheckMode.SpecifiedOnly => passed.Count() == intersect.Count(), + RoleCheckMode.None => !intersect.Any(), + _ => intersect.Any() + }; + } +} + +/// +/// Specifies how checks for roles. +/// +[Flags] +public enum RoleCheckMode +{ + /// + /// Member is required to have none of the specified roles. + /// + None = 0, + + /// + /// Member is required to have all of the specified roles. + /// + All = 1, + + /// + /// Member is required to have any of the specified roles. + /// + Any = 2, + + /// + /// Member is required to have exactly the same roles as specified; no extra roles may be present. + /// + SpecifiedOnly = 4, + + /// + /// Instructs the check to evaluate for matching role names. + /// + MatchNames = 8, + + /// + /// Instructs the check to evaluate for matching role IDs. + /// + MatchIds = 16 +} diff --git a/DSharpPlus.CommandsNext/Attributes/RequireUserPermissionsAttribute.cs b/DSharpPlus.CommandsNext/Attributes/RequireUserPermissionsAttribute.cs index 24d5368072..710af3cbbb 100644 --- a/DSharpPlus.CommandsNext/Attributes/RequireUserPermissionsAttribute.cs +++ b/DSharpPlus.CommandsNext/Attributes/RequireUserPermissionsAttribute.cs @@ -1,56 +1,56 @@ -using System; -using System.Threading.Tasks; -using DSharpPlus.Entities; - -namespace DSharpPlus.CommandsNext.Attributes; - -/// -/// Defines that usage of this command is restricted to members with specified permissions. -/// -[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = false)] -public sealed class RequireUserPermissionsAttribute : CheckBaseAttribute -{ - /// - /// Gets the permissions required by this attribute. - /// - public DiscordPermission[] Permissions { get; } - - /// - /// Gets this check's behaviour in DMs. True means the check will always pass in DMs, whereas false means that it will always fail. - /// - public bool IgnoreDms { get; } = true; - - /// - /// Defines that usage of this command is restricted to members with specified permissions. - /// - /// Permissions required to execute this command. - /// Sets this check's behaviour in DMs. True means the check will always pass in DMs, whereas false means that it will always fail. - public RequireUserPermissionsAttribute(bool ignoreDms = true, params DiscordPermission[] permissions) - { - this.Permissions = permissions; - this.IgnoreDms = ignoreDms; - } - - public override Task ExecuteCheckAsync(CommandContext ctx, bool help) - { - if (ctx.Guild == null) - { - return Task.FromResult(this.IgnoreDms); - } - - DiscordMember? usr = ctx.Member; - if (usr == null) - { - return Task.FromResult(false); - } - - if (usr.Id == ctx.Guild.OwnerId) - { - return Task.FromResult(true); - } - - DiscordPermissions pusr = ctx.Channel.PermissionsFor(usr); - - return Task.FromResult(pusr.HasAllPermissions(this.Permissions)); - } -} +using System; +using System.Threading.Tasks; +using DSharpPlus.Entities; + +namespace DSharpPlus.CommandsNext.Attributes; + +/// +/// Defines that usage of this command is restricted to members with specified permissions. +/// +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = false)] +public sealed class RequireUserPermissionsAttribute : CheckBaseAttribute +{ + /// + /// Gets the permissions required by this attribute. + /// + public DiscordPermission[] Permissions { get; } + + /// + /// Gets this check's behaviour in DMs. True means the check will always pass in DMs, whereas false means that it will always fail. + /// + public bool IgnoreDms { get; } = true; + + /// + /// Defines that usage of this command is restricted to members with specified permissions. + /// + /// Permissions required to execute this command. + /// Sets this check's behaviour in DMs. True means the check will always pass in DMs, whereas false means that it will always fail. + public RequireUserPermissionsAttribute(bool ignoreDms = true, params DiscordPermission[] permissions) + { + this.Permissions = permissions; + this.IgnoreDms = ignoreDms; + } + + public override Task ExecuteCheckAsync(CommandContext ctx, bool help) + { + if (ctx.Guild == null) + { + return Task.FromResult(this.IgnoreDms); + } + + DiscordMember? usr = ctx.Member; + if (usr == null) + { + return Task.FromResult(false); + } + + if (usr.Id == ctx.Guild.OwnerId) + { + return Task.FromResult(true); + } + + DiscordPermissions pusr = ctx.Channel.PermissionsFor(usr); + + return Task.FromResult(pusr.HasAllPermissions(this.Permissions)); + } +} diff --git a/DSharpPlus.CommandsNext/BaseCommandModule.cs b/DSharpPlus.CommandsNext/BaseCommandModule.cs index 422afb9552..8c255c5b7e 100644 --- a/DSharpPlus.CommandsNext/BaseCommandModule.cs +++ b/DSharpPlus.CommandsNext/BaseCommandModule.cs @@ -1,25 +1,25 @@ -using System.Threading.Tasks; - -namespace DSharpPlus.CommandsNext; - -/// -/// Represents a base class for all command modules. -/// -public abstract class BaseCommandModule -{ - /// - /// Called before a command in the implementing module is executed. - /// - /// Context in which the method is being executed. - /// - public virtual Task BeforeExecutionAsync(CommandContext ctx) - => Task.Delay(0); - - /// - /// Called after a command in the implementing module is successfully executed. - /// - /// Context in which the method is being executed. - /// - public virtual Task AfterExecutionAsync(CommandContext ctx) - => Task.Delay(0); -} +using System.Threading.Tasks; + +namespace DSharpPlus.CommandsNext; + +/// +/// Represents a base class for all command modules. +/// +public abstract class BaseCommandModule +{ + /// + /// Called before a command in the implementing module is executed. + /// + /// Context in which the method is being executed. + /// + public virtual Task BeforeExecutionAsync(CommandContext ctx) + => Task.Delay(0); + + /// + /// Called after a command in the implementing module is successfully executed. + /// + /// Context in which the method is being executed. + /// + public virtual Task AfterExecutionAsync(CommandContext ctx) + => Task.Delay(0); +} diff --git a/DSharpPlus.CommandsNext/CommandsNextConfiguration.cs b/DSharpPlus.CommandsNext/CommandsNextConfiguration.cs index 3c74d27628..8f7c39fff0 100644 --- a/DSharpPlus.CommandsNext/CommandsNextConfiguration.cs +++ b/DSharpPlus.CommandsNext/CommandsNextConfiguration.cs @@ -1,141 +1,141 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Threading.Tasks; -using DSharpPlus.CommandsNext.Attributes; -using DSharpPlus.CommandsNext.Converters; -using DSharpPlus.CommandsNext.Executors; -using DSharpPlus.Entities; - -namespace DSharpPlus.CommandsNext; - -/// -/// Represents a delegate for a function that takes a message, and returns the position of the start of command invocation in the message. It has to return -1 if prefix is not present. -/// -/// It is recommended that helper methods and -/// be used internally for checking. Their output can be passed through. -/// -/// -/// Message to check for prefix. -/// Position of the command invocation or -1 if not present. -public delegate Task PrefixResolverDelegate(DiscordMessage msg); - -/// -/// Represents a configuration for . -/// -public sealed class CommandsNextConfiguration -{ - /// - /// Sets the string prefixes used for commands. - /// Defaults to no value (disabled). - /// - public IEnumerable StringPrefixes { internal get; set; } = []; - - /// - /// Sets the custom prefix resolver used for commands. - /// Defaults to none (disabled). - /// - public PrefixResolverDelegate? PrefixResolver { internal get; set; } = null; - - /// - /// Sets whether to allow mentioning the bot to be used as command prefix. - /// Defaults to true. - /// - public bool EnableMentionPrefix { internal get; set; } = true; - - /// - /// Sets whether strings should be matched in a case-sensitive manner. - /// This switch affects the behaviour of default prefix resolver, command searching, and argument conversion. - /// Defaults to false. - /// - public bool CaseSensitive { internal get; set; } = false; - - /// - /// Sets whether to enable default help command. - /// Disabling this will allow you to make your own help command. - /// - /// Modifying default help can be achieved via custom help formatters (see and for more details). - /// It is recommended to use help formatter instead of disabling help. - /// - /// Defaults to true. - /// - public bool EnableDefaultHelp { internal get; set; } = true; - - /// - /// Controls whether the default help will be sent via DMs or not. - /// Enabling this will make the bot respond with help via direct messages. - /// Defaults to false. - /// - public bool DmHelp { internal get; set; } = false; - - /// - /// Sets the default pre-execution checks for the built-in help command. - /// Only applicable if default help is enabled. - /// Defaults to null. - /// - public IEnumerable DefaultHelpChecks { internal get; set; } = []; - - /// - /// Sets whether commands sent via direct messages should be processed. - /// Defaults to true. - /// - public bool EnableDms { internal get; set; } = true; - - /// - /// Gets whether any extra arguments passed to commands should be ignored or not. If this is set to false, extra arguments will throw, otherwise they will be ignored. - /// Defaults to false. - /// - public bool IgnoreExtraArguments { internal get; set; } = false; - - /// - /// Sets the quotation marks on parameters, used to interpret spaces as part of a single argument. - /// Defaults to a collection of ", «, », , , and . - /// - public IEnumerable QuotationMarks { internal get; set; } = new[] { '"', '«', '»', '‘', '“', '„', '‟' }; - - /// - /// Gets or sets whether to automatically enable handling commands. - /// If this is set to false, you will need to manually handle each incoming message and pass it to CommandsNext. - /// Defaults to true. - /// - public bool UseDefaultCommandHandler { internal get; set; } = true; - - /// - /// Gets or sets the default culture for parsers. - /// Defaults to invariant. - /// - public CultureInfo DefaultParserCulture { internal get; set; } = CultureInfo.InvariantCulture; - - /// - /// Gets or sets the default command executor. - /// This alters the behaviour, execution, and scheduling method of command execution. - /// - public ICommandExecutor CommandExecutor { internal get; set; } = new AsynchronousCommandExecutor(); - - /// - /// Creates a new instance of . - /// - public CommandsNextConfiguration() { } - - /// - /// Creates a new instance of , copying the properties of another configuration. - /// - /// Configuration the properties of which are to be copied. - public CommandsNextConfiguration(CommandsNextConfiguration other) - { - this.CaseSensitive = other.CaseSensitive; - this.PrefixResolver = other.PrefixResolver; - this.DefaultHelpChecks = other.DefaultHelpChecks; - this.EnableDefaultHelp = other.EnableDefaultHelp; - this.EnableDms = other.EnableDms; - this.EnableMentionPrefix = other.EnableMentionPrefix; - this.IgnoreExtraArguments = other.IgnoreExtraArguments; - this.QuotationMarks = other.QuotationMarks; - this.UseDefaultCommandHandler = other.UseDefaultCommandHandler; - this.StringPrefixes = other.StringPrefixes.ToArray(); - this.DmHelp = other.DmHelp; - this.DefaultParserCulture = other.DefaultParserCulture; - this.CommandExecutor = other.CommandExecutor; - } -} +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; +using DSharpPlus.CommandsNext.Attributes; +using DSharpPlus.CommandsNext.Converters; +using DSharpPlus.CommandsNext.Executors; +using DSharpPlus.Entities; + +namespace DSharpPlus.CommandsNext; + +/// +/// Represents a delegate for a function that takes a message, and returns the position of the start of command invocation in the message. It has to return -1 if prefix is not present. +/// +/// It is recommended that helper methods and +/// be used internally for checking. Their output can be passed through. +/// +/// +/// Message to check for prefix. +/// Position of the command invocation or -1 if not present. +public delegate Task PrefixResolverDelegate(DiscordMessage msg); + +/// +/// Represents a configuration for . +/// +public sealed class CommandsNextConfiguration +{ + /// + /// Sets the string prefixes used for commands. + /// Defaults to no value (disabled). + /// + public IEnumerable StringPrefixes { internal get; set; } = []; + + /// + /// Sets the custom prefix resolver used for commands. + /// Defaults to none (disabled). + /// + public PrefixResolverDelegate? PrefixResolver { internal get; set; } = null; + + /// + /// Sets whether to allow mentioning the bot to be used as command prefix. + /// Defaults to true. + /// + public bool EnableMentionPrefix { internal get; set; } = true; + + /// + /// Sets whether strings should be matched in a case-sensitive manner. + /// This switch affects the behaviour of default prefix resolver, command searching, and argument conversion. + /// Defaults to false. + /// + public bool CaseSensitive { internal get; set; } = false; + + /// + /// Sets whether to enable default help command. + /// Disabling this will allow you to make your own help command. + /// + /// Modifying default help can be achieved via custom help formatters (see and for more details). + /// It is recommended to use help formatter instead of disabling help. + /// + /// Defaults to true. + /// + public bool EnableDefaultHelp { internal get; set; } = true; + + /// + /// Controls whether the default help will be sent via DMs or not. + /// Enabling this will make the bot respond with help via direct messages. + /// Defaults to false. + /// + public bool DmHelp { internal get; set; } = false; + + /// + /// Sets the default pre-execution checks for the built-in help command. + /// Only applicable if default help is enabled. + /// Defaults to null. + /// + public IEnumerable DefaultHelpChecks { internal get; set; } = []; + + /// + /// Sets whether commands sent via direct messages should be processed. + /// Defaults to true. + /// + public bool EnableDms { internal get; set; } = true; + + /// + /// Gets whether any extra arguments passed to commands should be ignored or not. If this is set to false, extra arguments will throw, otherwise they will be ignored. + /// Defaults to false. + /// + public bool IgnoreExtraArguments { internal get; set; } = false; + + /// + /// Sets the quotation marks on parameters, used to interpret spaces as part of a single argument. + /// Defaults to a collection of ", «, », , , and . + /// + public IEnumerable QuotationMarks { internal get; set; } = new[] { '"', '«', '»', '‘', '“', '„', '‟' }; + + /// + /// Gets or sets whether to automatically enable handling commands. + /// If this is set to false, you will need to manually handle each incoming message and pass it to CommandsNext. + /// Defaults to true. + /// + public bool UseDefaultCommandHandler { internal get; set; } = true; + + /// + /// Gets or sets the default culture for parsers. + /// Defaults to invariant. + /// + public CultureInfo DefaultParserCulture { internal get; set; } = CultureInfo.InvariantCulture; + + /// + /// Gets or sets the default command executor. + /// This alters the behaviour, execution, and scheduling method of command execution. + /// + public ICommandExecutor CommandExecutor { internal get; set; } = new AsynchronousCommandExecutor(); + + /// + /// Creates a new instance of . + /// + public CommandsNextConfiguration() { } + + /// + /// Creates a new instance of , copying the properties of another configuration. + /// + /// Configuration the properties of which are to be copied. + public CommandsNextConfiguration(CommandsNextConfiguration other) + { + this.CaseSensitive = other.CaseSensitive; + this.PrefixResolver = other.PrefixResolver; + this.DefaultHelpChecks = other.DefaultHelpChecks; + this.EnableDefaultHelp = other.EnableDefaultHelp; + this.EnableDms = other.EnableDms; + this.EnableMentionPrefix = other.EnableMentionPrefix; + this.IgnoreExtraArguments = other.IgnoreExtraArguments; + this.QuotationMarks = other.QuotationMarks; + this.UseDefaultCommandHandler = other.UseDefaultCommandHandler; + this.StringPrefixes = other.StringPrefixes.ToArray(); + this.DmHelp = other.DmHelp; + this.DefaultParserCulture = other.DefaultParserCulture; + this.CommandExecutor = other.CommandExecutor; + } +} diff --git a/DSharpPlus.CommandsNext/CommandsNextEvents.cs b/DSharpPlus.CommandsNext/CommandsNextEvents.cs index 2daef22c2f..5e6c44fbee 100644 --- a/DSharpPlus.CommandsNext/CommandsNextEvents.cs +++ b/DSharpPlus.CommandsNext/CommandsNextEvents.cs @@ -1,19 +1,19 @@ -using Microsoft.Extensions.Logging; - -namespace DSharpPlus.CommandsNext; - -/// -/// Contains well-defined event IDs used by CommandsNext. -/// -public static class CommandsNextEvents -{ - /// - /// Miscellaneous events, that do not fit in any other category. - /// - internal static EventId Misc { get; } = new EventId(200, "CommandsNext"); - - /// - /// Events pertaining to Gateway Intents. Typically diagnostic information. - /// - internal static EventId Intents { get; } = new EventId(201, nameof(Intents)); -} +using Microsoft.Extensions.Logging; + +namespace DSharpPlus.CommandsNext; + +/// +/// Contains well-defined event IDs used by CommandsNext. +/// +public static class CommandsNextEvents +{ + /// + /// Miscellaneous events, that do not fit in any other category. + /// + internal static EventId Misc { get; } = new EventId(200, "CommandsNext"); + + /// + /// Events pertaining to Gateway Intents. Typically diagnostic information. + /// + internal static EventId Intents { get; } = new EventId(201, nameof(Intents)); +} diff --git a/DSharpPlus.CommandsNext/CommandsNextExtension.cs b/DSharpPlus.CommandsNext/CommandsNextExtension.cs index a707fc15be..9d6eb817e4 100644 --- a/DSharpPlus.CommandsNext/CommandsNextExtension.cs +++ b/DSharpPlus.CommandsNext/CommandsNextExtension.cs @@ -1,1219 +1,1219 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using System.Linq; -using System.Reflection; -using System.Threading.Tasks; - -using DSharpPlus.AsyncEvents; -using DSharpPlus.CommandsNext.Attributes; -using DSharpPlus.CommandsNext.Builders; -using DSharpPlus.CommandsNext.Converters; -using DSharpPlus.CommandsNext.Entities; -using DSharpPlus.CommandsNext.Exceptions; -using DSharpPlus.CommandsNext.Executors; -using DSharpPlus.Entities; -using DSharpPlus.EventArgs; - -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; - -namespace DSharpPlus.CommandsNext; - -/// -/// This is the class which handles command registration, management, and execution. -/// -public class CommandsNextExtension : IDisposable -{ - private CommandsNextConfiguration Config { get; } - private HelpFormatterFactory HelpFormatter { get; } - - private MethodInfo ConvertGeneric { get; } - private Dictionary UserFriendlyTypeNames { get; } - internal Dictionary ArgumentConverters { get; } - internal CultureInfo DefaultParserCulture - => this.Config.DefaultParserCulture; - - /// - /// Gets the service provider this CommandsNext module was configured with. - /// - public IServiceProvider Services - => this.Client.ServiceProvider; - - internal CommandsNextExtension(CommandsNextConfiguration cfg) - { - this.Config = new CommandsNextConfiguration(cfg); - 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(DiscordThreadChannel)] = new DiscordThreadChannelConverter(), - [typeof(DiscordGuild)] = new DiscordGuildConverter(), - [typeof(DiscordMessage)] = new DiscordMessageConverter(), - [typeof(DiscordEmoji)] = new DiscordEmojiConverter(), - [typeof(DiscordColor)] = new DiscordColorConverter() - }; - - 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(DiscordColor)] = "color" - }; - - Type ncvt = typeof(NullableConverter<>); - Type nt = typeof(Nullable<>); - Type[] cvts = [.. this.ArgumentConverters.Keys]; - foreach (Type? xt in cvts) - { - TypeInfo xti = xt.GetTypeInfo(); - if (!xti.IsValueType) - { - continue; - } - - Type xcvt = ncvt.MakeGenericType(xt); - Type xnt = nt.MakeGenericType(xt); - - if (this.ArgumentConverters.ContainsKey(xcvt) || Activator.CreateInstance(xcvt) is not IArgumentConverter xcv) - { - continue; - } - - this.ArgumentConverters[xnt] = xcv; - this.UserFriendlyTypeNames[xnt] = this.UserFriendlyTypeNames[xt]; - } - - Type t = typeof(CommandsNextExtension); - IEnumerable ms = t.GetTypeInfo().DeclaredMethods; - MethodInfo? m = ms.FirstOrDefault(xm => xm.Name == nameof(ConvertArgumentAsync) && xm.ContainsGenericParameters && !xm.IsStatic && xm.IsPublic); - this.ConvertGeneric = m; - } - - /// - /// Sets the help formatter to use with the default help command. - /// - /// Type of the formatter to use. - public void SetHelpFormatter() where T : BaseHelpFormatter => this.HelpFormatter.SetFormatterType(); - - /// - /// Disposes of this the resources used by CNext. - /// - public void Dispose() - { - this.Config.CommandExecutor.Dispose(); - - // Satisfy rule CA1816. Can be removed if this class is sealed. - GC.SuppressFinalize(this); - } - - #region DiscordClient Registration - /// - /// DO NOT USE THIS MANUALLY. - /// - /// DO NOT USE THIS MANUALLY. - /// - public void Setup(DiscordClient client) - { - this.Client = client; - - if (!Utilities.HasMessageIntents(client.Intents)) - { - client.Logger.LogCritical(CommandsNextEvents.Intents, "The CommandsNext extension is registered but there are no message intents enabled. It is highly recommended to enable them."); - } - - if (!client.Intents.HasIntent(DiscordIntents.Guilds)) - { - client.Logger.LogCritical(CommandsNextEvents.Intents, "The CommandsNext extension is registered but the guilds intent is not enabled. It is highly recommended to enable it."); - } - - DefaultClientErrorHandler errorHandler = new(client.Logger); - - this.executed = new AsyncEvent(errorHandler); - this.error = new AsyncEvent(errorHandler); - - if (this.Config.EnableDefaultHelp) - { - RegisterCommands(typeof(DefaultHelpModule), null, [], out List? tcmds); - - if (this.Config.DefaultHelpChecks.Any()) - { - CheckBaseAttribute[] checks = this.Config.DefaultHelpChecks.ToArray(); - - for (int i = 0; i < tcmds.Count; i++) - { - tcmds[i].WithExecutionChecks(checks); - } - } - - if (tcmds != null) - { - foreach (CommandBuilder xc in tcmds) - { - AddToCommandDictionary(xc.Build(null)); - } - } - } - - if (this.Config.CommandExecutor is ParallelQueuedCommandExecutor pqce) - { - this.Client.Logger.LogDebug(CommandsNextEvents.Misc, "Using parallel executor with degree {Parallelism}", pqce.Parallelism); - } - } - #endregion - - #region Command Handling - internal async Task HandleCommandsAsync(DiscordClient sender, MessageCreatedEventArgs e) - { - if (e.Author.IsBot) // bad bot - { - return; - } - - if (!this.Config.EnableDms && e.Channel.IsPrivate) - { - return; - } - - int mpos = -1; - if (this.Config.EnableMentionPrefix) - { - mpos = e.Message.GetMentionPrefixLength(this.Client.CurrentUser); - } - - if (this.Config.StringPrefixes.Any()) - { - foreach (string pfix in this.Config.StringPrefixes) - { - if (mpos == -1 && !string.IsNullOrWhiteSpace(pfix)) - { - mpos = e.Message.GetStringPrefixLength(pfix, this.Config.CaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase); - } - } - } - - if (mpos == -1 && this.Config.PrefixResolver != null) - { - mpos = await this.Config.PrefixResolver(e.Message); - } - - if (mpos == -1) - { - return; - } - - string pfx = e.Message.Content[..mpos]; - string cnt = e.Message.Content[mpos..]; - - int _ = 0; - string? fname = cnt.ExtractNextArgument(ref _, this.Config.QuotationMarks); - - Command? cmd = FindCommand(cnt, out string? args); - CommandContext ctx = CreateContext(e.Message, pfx, cmd, args); - - if (cmd is null) - { - await this.error.InvokeAsync(this, new CommandErrorEventArgs { Context = ctx, Exception = new CommandNotFoundException(fname ?? "UnknownCmd") }); - return; - } - - await this.Config.CommandExecutor.ExecuteAsync(ctx); - } - - /// - /// 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; - - bool ignoreCase = !this.Config.CaseSensitive; - int pos = 0; - string? next = commandString.ExtractNextArgument(ref pos, this.Config.QuotationMarks); - if (next is null) - { - return null; - } - - if (!this.RegisteredCommands.TryGetValue(next, out Command? cmd)) - { - if (!ignoreCase) - { - return null; - } - - KeyValuePair cmdKvp = this.RegisteredCommands.FirstOrDefault(x => x.Key.Equals(next, StringComparison.InvariantCultureIgnoreCase)); - if (cmdKvp.Value is null) - { - return null; - } - - cmd = cmdKvp.Value; - } - - if (cmd is not CommandGroup) - { - rawArguments = commandString[pos..].Trim(); - return cmd; - } - - while (cmd is CommandGroup) - { - CommandGroup? cm2 = cmd as CommandGroup; - int oldPos = pos; - next = commandString.ExtractNextArgument(ref pos, this.Config.QuotationMarks); - if (next is null) - { - break; - } - - StringComparison comparison = ignoreCase ? StringComparison.InvariantCultureIgnoreCase : StringComparison.InvariantCulture; - cmd = cm2?.Children.FirstOrDefault(x => x.Name.Equals(next, comparison) || x.Aliases.Any(xx => xx.Equals(next, comparison))); - - if (cmd is 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) - { - CommandContext ctx = new() - { - Client = this.Client, - Command = cmd, - Message = msg, - Config = this.Config, - RawArgumentString = rawArguments ?? "", - Prefix = prefix, - CommandsNext = this, - Services = this.Services - }; - - if (cmd is not null && (cmd.Module is TransientCommandModule || cmd.Module == null)) - { - IServiceScope 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 - { - Command? cmd = ctx.Command; - - if (cmd is null) - { - return; - } - - await RunAllChecksAsync(cmd, ctx); - - CommandResult res = await cmd.ExecuteAsync(ctx); - - if (res.IsSuccessful) - { - await this.executed.InvokeAsync(this, new CommandExecutionEventArgs { Context = res.Context }); - } - else - { - await this.error.InvokeAsync(this, new CommandErrorEventArgs { Context = res.Context, Exception = res.Exception }); - } - } - catch (Exception ex) - { - await this.error.InvokeAsync(this, new CommandErrorEventArgs { Context = ctx, Exception = ex }); - } - finally - { - if (ctx.ServiceScopeContext.IsInitialized) - { - ctx.ServiceScopeContext.Dispose(); - } - } - } - - private static async Task RunAllChecksAsync(Command cmd, CommandContext ctx) - { - if (cmd.Parent is not null) - { - await RunAllChecksAsync(cmd.Parent, ctx); - } - - IEnumerable fchecks = await cmd.RunChecksAsync(ctx, 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.TopLevelCommands; - - private Dictionary TopLevelCommands { get; set; } - public DiscordClient Client { get; private set; } - - /// - /// 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) - { - IEnumerable types = assembly.ExportedTypes.Where(xt => - { - TypeInfo xti = xt.GetTypeInfo(); - return xti.IsModuleCandidateType() && !xti.IsNested; - }); - foreach (Type? xt in types) - { - RegisterCommands(xt); - } - } - - /// - /// Registers all commands from a given command class. - /// - /// Class which holds commands to register. - public void RegisterCommands() where T : BaseCommandModule - { - Type t = typeof(T); - 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 is 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."); - } - - RegisterCommands(t, null, [], out List? tempCommands); - - if (tempCommands != null) - { - foreach (CommandBuilder command in tempCommands) - { - AddToCommandDictionary(command.Build(null)); - } - } - } - - private void RegisterCommands(Type t, CommandGroupBuilder? currentParent, IEnumerable inheritedChecks, out List foundCommands) - { - TypeInfo ti = t.GetTypeInfo(); - - ModuleLifespanAttribute? lifespan = ti.GetCustomAttribute(); - ModuleLifespan moduleLifespan = lifespan != null ? lifespan.Lifespan : ModuleLifespan.Singleton; - - ICommandModule 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 - CommandGroupBuilder? groupBuilder = new(module); - bool isModule = false; - IEnumerable moduleAttributes = ti.GetCustomAttributes(); - bool moduleHidden = false; - List moduleChecks = []; - - groupBuilder.WithCategory(ExtractCategoryAttribute(t)); - - foreach (Attribute xa in moduleAttributes) - { - switch (xa) - { - case GroupAttribute g: - isModule = true; - string? moduleName = g.Name; - if (moduleName is null) - { - moduleName = ti.Name; - - if (moduleName.EndsWith("Group") && moduleName != "Group") - { - moduleName = moduleName[..^5]; - } - else if (moduleName.EndsWith("Module") && moduleName != "Module") - { - moduleName = moduleName[..^6]; - } - else if (moduleName.EndsWith("Commands") && moduleName != "Commands") - { - moduleName = moduleName[..^8]; - } - } - - if (!this.Config.CaseSensitive) - { - moduleName = moduleName.ToLowerInvariant(); - } - - groupBuilder.WithName(moduleName); - - foreach (CheckBaseAttribute chk in inheritedChecks) - { - groupBuilder.WithExecutionCheck(chk); - } - - foreach (MethodInfo? mi in ti.DeclaredMethods.Where(x => x.IsCommandCandidate(out _) && x.GetCustomAttribute() != null)) - { - groupBuilder.WithOverload(new CommandOverloadBuilder(mi)); - } - - break; - - case AliasesAttribute a: - foreach (string xalias in a.Aliases) - { - groupBuilder.WithAlias(this.Config.CaseSensitive ? xalias : xalias.ToLowerInvariant()); - } - - break; - - case HiddenAttribute: - 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.Any()) - { - moduleChecks.AddRange(inheritedChecks); - } - } - - // candidate methods - IEnumerable methods = ti.DeclaredMethods; - List commands = []; - Dictionary commandBuilders = []; - foreach (MethodInfo m in methods) - { - if (!m.IsCommandCandidate(out _)) - { - continue; - } - - IEnumerable attrs = m.GetCustomAttributes(); - if (attrs.FirstOrDefault(xa => xa is CommandAttribute) is not CommandAttribute cattr) - { - continue; - } - - string? commandName = cattr.Name; - if (commandName is null) - { - commandName = m.Name; - if (commandName.EndsWith("Async") && commandName != "Async") - { - commandName = commandName[..^5]; - } - } - - if (!this.Config.CaseSensitive) - { - commandName = commandName.ToLowerInvariant(); - } - - if (!commandBuilders.TryGetValue(commandName, out CommandBuilder? 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.Count != 0) - { - foreach (CheckBaseAttribute chk in moduleChecks) - { - commandBuilder.WithExecutionCheck(chk); - } - } - - commandBuilder.WithCategory(ExtractCategoryAttribute(m)); - - foreach (Attribute xa in attrs) - { - switch (xa) - { - case AliasesAttribute a: - foreach (string xalias in a.Aliases) - { - commandBuilder.WithAlias(this.Config.CaseSensitive ? xalias : xalias.ToLowerInvariant()); - } - - break; - - case CheckBaseAttribute p: - commandBuilder.WithExecutionCheck(p); - break; - - case DescriptionAttribute d: - commandBuilder.WithDescription(d.Description); - break; - - case HiddenAttribute: - commandBuilder.WithHiddenStatus(true); - break; - - default: - commandBuilder.WithCustomAttribute(xa); - break; - } - } - - if (!isModule && moduleHidden) - { - commandBuilder.WithHiddenStatus(true); - } - } - - // candidate types - IEnumerable types = ti.DeclaredNestedTypes - .Where(xt => xt.IsModuleCandidateType() && xt.DeclaredConstructors.Any(xc => xc.IsPublic)); - foreach (TypeInfo? type in types) - { - RegisterCommands(type.AsType(), - groupBuilder, - !isModule ? moduleChecks : Enumerable.Empty(), - out List? tempCommands); - - if (isModule && groupBuilder is not null) - { - foreach (CheckBaseAttribute chk in moduleChecks) - { - groupBuilder.WithExecutionCheck(chk); - } - } - - if (isModule && tempCommands is not null && groupBuilder is not null) - { - foreach (CommandBuilder xtcmd in tempCommands) - { - groupBuilder.WithChild(xtcmd); - } - } - else if (tempCommands != null) - { - commands.AddRange(tempCommands); - } - } - - if (isModule && currentParent is null && groupBuilder is not null) - { - commands.Add(groupBuilder); - } - else if (isModule && currentParent is not null && groupBuilder is not null) - { - currentParent.WithChild(groupBuilder); - } - - foundCommands = commands; - } - - /// - /// Builds and registers all supplied commands. - /// - /// Commands to build and register. - public void RegisterCommands(params CommandBuilder[] cmds) - { - foreach (CommandBuilder cmd in cmds) - { - AddToCommandDictionary(cmd.Build(null)); - } - } - - /// - /// Unregisters specified commands from CommandsNext. - /// - /// Commands to unregister. - public void UnregisterCommands(params Command[] cmds) - { - if (cmds.Any(x => x.Parent is not null)) - { - throw new InvalidOperationException("Cannot unregister nested commands."); - } - - List keys = this.RegisteredCommands.Where(x => cmds.Contains(x.Value)).Select(x => x.Key).ToList(); - foreach (string? key in keys) - { - this.TopLevelCommands.Remove(key); - } - } - - private static string? ExtractCategoryAttribute(MethodInfo method) - { - CategoryAttribute attribute = method.GetCustomAttribute(); - - if (attribute is not null) - { - return attribute.Name; - } - - // extract from types - - return ExtractCategoryAttribute(method.DeclaringType); - } - - private static string? ExtractCategoryAttribute(Type type) - { - CategoryAttribute attribute; - - do - { - attribute = type.GetCustomAttribute(); - - if (attribute is not null) - { - return attribute.Name; - } - - type = type.DeclaringType; - - } while (type is not null); - - return null; - } - - private void AddToCommandDictionary(Command cmd) - { - if (cmd.Parent is not null) - { - return; - } - - if (this.TopLevelCommands.ContainsKey(cmd.Name) || cmd.Aliases.Any(xs => this.TopLevelCommands.ContainsKey(xs))) - { - throw new DuplicateCommandException(cmd.QualifiedName); - } - - this.TopLevelCommands[cmd.Name] = cmd; - - foreach (string xs in cmd.Aliases) - { - this.TopLevelCommands[xs] = cmd; - } - } - #endregion - - #region Default Help - [ModuleLifespan(ModuleLifespan.Transient)] - public class DefaultHelpModule : BaseCommandModule - { - [Command("help"), Description("Displays command help."), SuppressMessage("Quality Assurance", "CA1822:Mark members as static", Justification = "CommandsNext does not support static commands.")] - public async Task DefaultHelpAsync(CommandContext ctx, [Description("Command to provide help for.")] params string[] command) - { - IEnumerable topLevel = ctx.CommandsNext.TopLevelCommands.Values.Distinct(); - BaseHelpFormatter helpBuilder = ctx.CommandsNext.HelpFormatter.Create(ctx); - - if (command != null && command.Length != 0) - { - Command? cmd = null; - IEnumerable? searchIn = topLevel; - foreach (string c in command) - { - if (searchIn is null) - { - cmd = null; - break; - } - - (StringComparison comparison, StringComparer comparer) = ctx.Config.CaseSensitive switch - { - true => (StringComparison.InvariantCulture, StringComparer.InvariantCulture), - false => (StringComparison.InvariantCultureIgnoreCase, StringComparer.InvariantCultureIgnoreCase) - }; - cmd = searchIn.FirstOrDefault(xc => xc.Name.Equals(c, comparison) || xc.Aliases.Contains(c, comparer)); - - if (cmd is null) - { - break; - } - - IEnumerable failedChecks = await cmd.RunChecksAsync(ctx, true); - if (failedChecks.Any()) - { - throw new ChecksFailedException(cmd, ctx, failedChecks); - } - - searchIn = cmd is CommandGroup cmdGroup ? cmdGroup.Children : null; - } - - if (cmd is null) - { - throw new CommandNotFoundException(string.Join(" ", command)); - } - - helpBuilder.WithCommand(cmd); - - if (cmd is CommandGroup group) - { - IEnumerable commandsToSearch = group.Children.Where(xc => !xc.IsHidden); - List eligibleCommands = []; - foreach (Command? candidateCommand in commandsToSearch) - { - if (candidateCommand.ExecutionChecks == null || !candidateCommand.ExecutionChecks.Any()) - { - eligibleCommands.Add(candidateCommand); - continue; - } - - IEnumerable candidateFailedChecks = await candidateCommand.RunChecksAsync(ctx, true); - if (!candidateFailedChecks.Any()) - { - eligibleCommands.Add(candidateCommand); - } - } - - if (eligibleCommands.Count != 0) - { - helpBuilder.WithSubcommands(eligibleCommands.OrderBy(xc => xc.Name)); - } - } - } - else - { - IEnumerable commandsToSearch = topLevel.Where(xc => !xc.IsHidden); - List eligibleCommands = []; - foreach (Command? sc in commandsToSearch) - { - if (sc.ExecutionChecks == null || !sc.ExecutionChecks.Any()) - { - eligibleCommands.Add(sc); - continue; - } - - IEnumerable candidateFailedChecks = await sc.RunChecksAsync(ctx, true); - if (!candidateFailedChecks.Any()) - { - eligibleCommands.Add(sc); - } - } - - if (eligibleCommands.Count != 0) - { - helpBuilder.WithSubcommands(eligibleCommands.OrderBy(xc => xc.Name)); - } - } - - CommandHelpMessage helpMessage = helpBuilder.Build(); - - DiscordMessageBuilder builder = new DiscordMessageBuilder().WithContent(helpMessage.Content).AddEmbed(helpMessage.Embed); - - if (!ctx.Config.DmHelp || ctx.Channel is DiscordDmChannel || ctx.Guild is null || ctx.Member is null) - { - await ctx.RespondAsync(builder); - } - else - { - await ctx.Member.SendMessageAsync(builder); - } - } - } - #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) - { - DateTimeOffset epoch = new(2015, 1, 1, 0, 0, 0, TimeSpan.Zero); - DateTimeOffset now = DateTimeOffset.UtcNow; - ulong timeSpan = (ulong)(now - epoch).TotalMilliseconds; - - // create fake message - DiscordMessage msg = new() - { - Discord = this.Client, - Author = actor, - Channel = channel, - ChannelId = channel.Id, - Content = messageContents, - Id = timeSpan << 22, - Pinned = false, - MentionEveryone = messageContents.Contains("@everyone"), - IsTTS = false, - attachments = [], - embeds = [], - Timestamp = now, - reactions = [] - }; - - List mentionedUsers = []; - List? mentionedRoles = msg.Channel.Guild != null ? [] : null; - List? mentionedChannels = msg.Channel.Guild != null ? [] : null; - - if (!string.IsNullOrWhiteSpace(msg.Content)) - { - if (msg.Channel.Guild != null) - { - mentionedUsers = Utilities.GetUserMentions(msg).Select(xid => msg.Channel.Guild.members.TryGetValue(xid, out DiscordMember? member) ? member : null).Cast().ToList(); - mentionedRoles = Utilities.GetRoleMentions(msg).Select(xid => msg.Channel.Guild.roles.GetValueOrDefault(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.mentionedUsers = mentionedUsers; - msg.mentionedRoles = mentionedRoles; - msg.mentionedChannels = mentionedChannels; - - CommandContext ctx = new() - { - Client = this.Client, - Command = cmd, - Message = msg, - Config = this.Config, - RawArgumentString = rawArguments ?? "", - Prefix = prefix, - CommandsNext = this, - Services = this.Services - }; - - if (cmd is not null && (cmd.Module is TransientCommandModule || cmd.Module is null)) - { - IServiceScope 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 ConvertArgumentAsync(string value, CommandContext ctx) - { - Type t = typeof(T); - if (!this.ArgumentConverters.TryGetValue(t, out IArgumentConverter argumentConverter)) - { - throw new ArgumentException("There is no converter specified for given type.", nameof(T)); - } - - if (argumentConverter is not IArgumentConverter cv) - { - throw new ArgumentException("Invalid converter registered for this type.", nameof(T)); - } - - Optional cvr = await cv.ConvertAsync(value, ctx); - 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 ConvertArgumentAsync(string? value, CommandContext ctx, Type type) - { - MethodInfo m = this.ConvertGeneric.MakeGenericMethod(type); - try - { - return await (Task)m.Invoke(this, [value, ctx]); - } - catch (Exception ex) when (ex is TargetInvocationException or InvalidCastException) - { - 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 is null) - { - throw new ArgumentNullException(nameof(converter), "Converter cannot be null."); - } - - Type t = typeof(T); - TypeInfo ti = t.GetTypeInfo(); - this.ArgumentConverters[t] = converter; - - if (!ti.IsValueType) - { - return; - } - - Type nullableConverterType = typeof(NullableConverter<>).MakeGenericType(t); - Type nullableType = typeof(Nullable<>).MakeGenericType(t); - if (this.ArgumentConverters.ContainsKey(nullableType)) - { - return; - } - - IArgumentConverter? nullableConverter = Activator.CreateInstance(nullableConverterType) as IArgumentConverter; - - if (nullableConverter is not null) - { - this.ArgumentConverters[nullableType] = nullableConverter; - } - } - - /// - /// Unregisters an argument converter for specified type. - /// - /// Type for which to unregister the converter. - public void UnregisterConverter() - { - Type t = typeof(T); - TypeInfo ti = t.GetTypeInfo(); - this.ArgumentConverters.Remove(t); - this.UserFriendlyTypeNames.Remove(t); - - if (!ti.IsValueType) - { - return; - } - - Type nullableType = typeof(Nullable<>).MakeGenericType(t); - if (!this.ArgumentConverters.ContainsKey(nullableType)) - { - return; - } - - this.ArgumentConverters.Remove(nullableType); - this.UserFriendlyTypeNames.Remove(nullableType); - } - - /// - /// Registers a user-friendly type name. - /// - /// Type to register the name for. - /// Name to register. - public void RegisterUserFriendlyTypeName(string value) - { - if (string.IsNullOrWhiteSpace(value)) - { - throw new ArgumentNullException(nameof(value), "Name cannot be null or empty."); - } - - Type t = typeof(T); - TypeInfo ti = t.GetTypeInfo(); - if (!this.ArgumentConverters.ContainsKey(t)) - { - throw new InvalidOperationException("Cannot register a friendly name for a type which has no associated converter."); - } - - this.UserFriendlyTypeNames[t] = value; - - if (!ti.IsValueType) - { - return; - } - - Type nullableType = typeof(Nullable<>).MakeGenericType(t); - this.UserFriendlyTypeNames[nullableType] = value; - } - - /// - /// Converts a type into user-friendly type name. - /// - /// Type to convert. - /// User-friendly type name. - public string GetUserFriendlyTypeName(Type t) - { - if (this.UserFriendlyTypeNames.TryGetValue(t, out string value)) - { - return value; - } - - TypeInfo ti = t.GetTypeInfo(); - if (ti.IsGenericTypeDefinition && t.GetGenericTypeDefinition() == typeof(Nullable<>)) - { - Type tn = ti.GenericTypeArguments[0]; - return this.UserFriendlyTypeNames.TryGetValue(tn, out value) ? value : tn.Name; - } - - return t.Name; - } - #endregion - - #region Helpers - /// - /// Gets the configuration-specific string comparer. This returns or , - /// depending on whether is set to or . - /// - /// A string comparer. - internal IEqualityComparer GetStringComparer() - => this.Config.CaseSensitive - ? StringComparer.Ordinal - : StringComparer.OrdinalIgnoreCase; - #endregion - - #region Events - /// - /// Triggered whenever a command executes successfully. - /// - public event AsyncEventHandler CommandExecuted - { - add => this.executed.Register(value); - remove => this.executed.Unregister(value); - } - private AsyncEvent executed = null!; - - /// - /// 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 = null!; - - private Task OnCommandExecuted(CommandExecutionEventArgs e) - => this.executed.InvokeAsync(this, e); - - private Task OnCommandErrored(CommandErrorEventArgs e) - => this.error.InvokeAsync(this, e); - #endregion -} +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; + +using DSharpPlus.AsyncEvents; +using DSharpPlus.CommandsNext.Attributes; +using DSharpPlus.CommandsNext.Builders; +using DSharpPlus.CommandsNext.Converters; +using DSharpPlus.CommandsNext.Entities; +using DSharpPlus.CommandsNext.Exceptions; +using DSharpPlus.CommandsNext.Executors; +using DSharpPlus.Entities; +using DSharpPlus.EventArgs; + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace DSharpPlus.CommandsNext; + +/// +/// This is the class which handles command registration, management, and execution. +/// +public class CommandsNextExtension : IDisposable +{ + private CommandsNextConfiguration Config { get; } + private HelpFormatterFactory HelpFormatter { get; } + + private MethodInfo ConvertGeneric { get; } + private Dictionary UserFriendlyTypeNames { get; } + internal Dictionary ArgumentConverters { get; } + internal CultureInfo DefaultParserCulture + => this.Config.DefaultParserCulture; + + /// + /// Gets the service provider this CommandsNext module was configured with. + /// + public IServiceProvider Services + => this.Client.ServiceProvider; + + internal CommandsNextExtension(CommandsNextConfiguration cfg) + { + this.Config = new CommandsNextConfiguration(cfg); + 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(DiscordThreadChannel)] = new DiscordThreadChannelConverter(), + [typeof(DiscordGuild)] = new DiscordGuildConverter(), + [typeof(DiscordMessage)] = new DiscordMessageConverter(), + [typeof(DiscordEmoji)] = new DiscordEmojiConverter(), + [typeof(DiscordColor)] = new DiscordColorConverter() + }; + + 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(DiscordColor)] = "color" + }; + + Type ncvt = typeof(NullableConverter<>); + Type nt = typeof(Nullable<>); + Type[] cvts = [.. this.ArgumentConverters.Keys]; + foreach (Type? xt in cvts) + { + TypeInfo xti = xt.GetTypeInfo(); + if (!xti.IsValueType) + { + continue; + } + + Type xcvt = ncvt.MakeGenericType(xt); + Type xnt = nt.MakeGenericType(xt); + + if (this.ArgumentConverters.ContainsKey(xcvt) || Activator.CreateInstance(xcvt) is not IArgumentConverter xcv) + { + continue; + } + + this.ArgumentConverters[xnt] = xcv; + this.UserFriendlyTypeNames[xnt] = this.UserFriendlyTypeNames[xt]; + } + + Type t = typeof(CommandsNextExtension); + IEnumerable ms = t.GetTypeInfo().DeclaredMethods; + MethodInfo? m = ms.FirstOrDefault(xm => xm.Name == nameof(ConvertArgumentAsync) && xm.ContainsGenericParameters && !xm.IsStatic && xm.IsPublic); + this.ConvertGeneric = m; + } + + /// + /// Sets the help formatter to use with the default help command. + /// + /// Type of the formatter to use. + public void SetHelpFormatter() where T : BaseHelpFormatter => this.HelpFormatter.SetFormatterType(); + + /// + /// Disposes of this the resources used by CNext. + /// + public void Dispose() + { + this.Config.CommandExecutor.Dispose(); + + // Satisfy rule CA1816. Can be removed if this class is sealed. + GC.SuppressFinalize(this); + } + + #region DiscordClient Registration + /// + /// DO NOT USE THIS MANUALLY. + /// + /// DO NOT USE THIS MANUALLY. + /// + public void Setup(DiscordClient client) + { + this.Client = client; + + if (!Utilities.HasMessageIntents(client.Intents)) + { + client.Logger.LogCritical(CommandsNextEvents.Intents, "The CommandsNext extension is registered but there are no message intents enabled. It is highly recommended to enable them."); + } + + if (!client.Intents.HasIntent(DiscordIntents.Guilds)) + { + client.Logger.LogCritical(CommandsNextEvents.Intents, "The CommandsNext extension is registered but the guilds intent is not enabled. It is highly recommended to enable it."); + } + + DefaultClientErrorHandler errorHandler = new(client.Logger); + + this.executed = new AsyncEvent(errorHandler); + this.error = new AsyncEvent(errorHandler); + + if (this.Config.EnableDefaultHelp) + { + RegisterCommands(typeof(DefaultHelpModule), null, [], out List? tcmds); + + if (this.Config.DefaultHelpChecks.Any()) + { + CheckBaseAttribute[] checks = this.Config.DefaultHelpChecks.ToArray(); + + for (int i = 0; i < tcmds.Count; i++) + { + tcmds[i].WithExecutionChecks(checks); + } + } + + if (tcmds != null) + { + foreach (CommandBuilder xc in tcmds) + { + AddToCommandDictionary(xc.Build(null)); + } + } + } + + if (this.Config.CommandExecutor is ParallelQueuedCommandExecutor pqce) + { + this.Client.Logger.LogDebug(CommandsNextEvents.Misc, "Using parallel executor with degree {Parallelism}", pqce.Parallelism); + } + } + #endregion + + #region Command Handling + internal async Task HandleCommandsAsync(DiscordClient sender, MessageCreatedEventArgs e) + { + if (e.Author.IsBot) // bad bot + { + return; + } + + if (!this.Config.EnableDms && e.Channel.IsPrivate) + { + return; + } + + int mpos = -1; + if (this.Config.EnableMentionPrefix) + { + mpos = e.Message.GetMentionPrefixLength(this.Client.CurrentUser); + } + + if (this.Config.StringPrefixes.Any()) + { + foreach (string pfix in this.Config.StringPrefixes) + { + if (mpos == -1 && !string.IsNullOrWhiteSpace(pfix)) + { + mpos = e.Message.GetStringPrefixLength(pfix, this.Config.CaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase); + } + } + } + + if (mpos == -1 && this.Config.PrefixResolver != null) + { + mpos = await this.Config.PrefixResolver(e.Message); + } + + if (mpos == -1) + { + return; + } + + string pfx = e.Message.Content[..mpos]; + string cnt = e.Message.Content[mpos..]; + + int _ = 0; + string? fname = cnt.ExtractNextArgument(ref _, this.Config.QuotationMarks); + + Command? cmd = FindCommand(cnt, out string? args); + CommandContext ctx = CreateContext(e.Message, pfx, cmd, args); + + if (cmd is null) + { + await this.error.InvokeAsync(this, new CommandErrorEventArgs { Context = ctx, Exception = new CommandNotFoundException(fname ?? "UnknownCmd") }); + return; + } + + await this.Config.CommandExecutor.ExecuteAsync(ctx); + } + + /// + /// 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; + + bool ignoreCase = !this.Config.CaseSensitive; + int pos = 0; + string? next = commandString.ExtractNextArgument(ref pos, this.Config.QuotationMarks); + if (next is null) + { + return null; + } + + if (!this.RegisteredCommands.TryGetValue(next, out Command? cmd)) + { + if (!ignoreCase) + { + return null; + } + + KeyValuePair cmdKvp = this.RegisteredCommands.FirstOrDefault(x => x.Key.Equals(next, StringComparison.InvariantCultureIgnoreCase)); + if (cmdKvp.Value is null) + { + return null; + } + + cmd = cmdKvp.Value; + } + + if (cmd is not CommandGroup) + { + rawArguments = commandString[pos..].Trim(); + return cmd; + } + + while (cmd is CommandGroup) + { + CommandGroup? cm2 = cmd as CommandGroup; + int oldPos = pos; + next = commandString.ExtractNextArgument(ref pos, this.Config.QuotationMarks); + if (next is null) + { + break; + } + + StringComparison comparison = ignoreCase ? StringComparison.InvariantCultureIgnoreCase : StringComparison.InvariantCulture; + cmd = cm2?.Children.FirstOrDefault(x => x.Name.Equals(next, comparison) || x.Aliases.Any(xx => xx.Equals(next, comparison))); + + if (cmd is 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) + { + CommandContext ctx = new() + { + Client = this.Client, + Command = cmd, + Message = msg, + Config = this.Config, + RawArgumentString = rawArguments ?? "", + Prefix = prefix, + CommandsNext = this, + Services = this.Services + }; + + if (cmd is not null && (cmd.Module is TransientCommandModule || cmd.Module == null)) + { + IServiceScope 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 + { + Command? cmd = ctx.Command; + + if (cmd is null) + { + return; + } + + await RunAllChecksAsync(cmd, ctx); + + CommandResult res = await cmd.ExecuteAsync(ctx); + + if (res.IsSuccessful) + { + await this.executed.InvokeAsync(this, new CommandExecutionEventArgs { Context = res.Context }); + } + else + { + await this.error.InvokeAsync(this, new CommandErrorEventArgs { Context = res.Context, Exception = res.Exception }); + } + } + catch (Exception ex) + { + await this.error.InvokeAsync(this, new CommandErrorEventArgs { Context = ctx, Exception = ex }); + } + finally + { + if (ctx.ServiceScopeContext.IsInitialized) + { + ctx.ServiceScopeContext.Dispose(); + } + } + } + + private static async Task RunAllChecksAsync(Command cmd, CommandContext ctx) + { + if (cmd.Parent is not null) + { + await RunAllChecksAsync(cmd.Parent, ctx); + } + + IEnumerable fchecks = await cmd.RunChecksAsync(ctx, 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.TopLevelCommands; + + private Dictionary TopLevelCommands { get; set; } + public DiscordClient Client { get; private set; } + + /// + /// 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) + { + IEnumerable types = assembly.ExportedTypes.Where(xt => + { + TypeInfo xti = xt.GetTypeInfo(); + return xti.IsModuleCandidateType() && !xti.IsNested; + }); + foreach (Type? xt in types) + { + RegisterCommands(xt); + } + } + + /// + /// Registers all commands from a given command class. + /// + /// Class which holds commands to register. + public void RegisterCommands() where T : BaseCommandModule + { + Type t = typeof(T); + 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 is 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."); + } + + RegisterCommands(t, null, [], out List? tempCommands); + + if (tempCommands != null) + { + foreach (CommandBuilder command in tempCommands) + { + AddToCommandDictionary(command.Build(null)); + } + } + } + + private void RegisterCommands(Type t, CommandGroupBuilder? currentParent, IEnumerable inheritedChecks, out List foundCommands) + { + TypeInfo ti = t.GetTypeInfo(); + + ModuleLifespanAttribute? lifespan = ti.GetCustomAttribute(); + ModuleLifespan moduleLifespan = lifespan != null ? lifespan.Lifespan : ModuleLifespan.Singleton; + + ICommandModule 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 + CommandGroupBuilder? groupBuilder = new(module); + bool isModule = false; + IEnumerable moduleAttributes = ti.GetCustomAttributes(); + bool moduleHidden = false; + List moduleChecks = []; + + groupBuilder.WithCategory(ExtractCategoryAttribute(t)); + + foreach (Attribute xa in moduleAttributes) + { + switch (xa) + { + case GroupAttribute g: + isModule = true; + string? moduleName = g.Name; + if (moduleName is null) + { + moduleName = ti.Name; + + if (moduleName.EndsWith("Group") && moduleName != "Group") + { + moduleName = moduleName[..^5]; + } + else if (moduleName.EndsWith("Module") && moduleName != "Module") + { + moduleName = moduleName[..^6]; + } + else if (moduleName.EndsWith("Commands") && moduleName != "Commands") + { + moduleName = moduleName[..^8]; + } + } + + if (!this.Config.CaseSensitive) + { + moduleName = moduleName.ToLowerInvariant(); + } + + groupBuilder.WithName(moduleName); + + foreach (CheckBaseAttribute chk in inheritedChecks) + { + groupBuilder.WithExecutionCheck(chk); + } + + foreach (MethodInfo? mi in ti.DeclaredMethods.Where(x => x.IsCommandCandidate(out _) && x.GetCustomAttribute() != null)) + { + groupBuilder.WithOverload(new CommandOverloadBuilder(mi)); + } + + break; + + case AliasesAttribute a: + foreach (string xalias in a.Aliases) + { + groupBuilder.WithAlias(this.Config.CaseSensitive ? xalias : xalias.ToLowerInvariant()); + } + + break; + + case HiddenAttribute: + 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.Any()) + { + moduleChecks.AddRange(inheritedChecks); + } + } + + // candidate methods + IEnumerable methods = ti.DeclaredMethods; + List commands = []; + Dictionary commandBuilders = []; + foreach (MethodInfo m in methods) + { + if (!m.IsCommandCandidate(out _)) + { + continue; + } + + IEnumerable attrs = m.GetCustomAttributes(); + if (attrs.FirstOrDefault(xa => xa is CommandAttribute) is not CommandAttribute cattr) + { + continue; + } + + string? commandName = cattr.Name; + if (commandName is null) + { + commandName = m.Name; + if (commandName.EndsWith("Async") && commandName != "Async") + { + commandName = commandName[..^5]; + } + } + + if (!this.Config.CaseSensitive) + { + commandName = commandName.ToLowerInvariant(); + } + + if (!commandBuilders.TryGetValue(commandName, out CommandBuilder? 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.Count != 0) + { + foreach (CheckBaseAttribute chk in moduleChecks) + { + commandBuilder.WithExecutionCheck(chk); + } + } + + commandBuilder.WithCategory(ExtractCategoryAttribute(m)); + + foreach (Attribute xa in attrs) + { + switch (xa) + { + case AliasesAttribute a: + foreach (string xalias in a.Aliases) + { + commandBuilder.WithAlias(this.Config.CaseSensitive ? xalias : xalias.ToLowerInvariant()); + } + + break; + + case CheckBaseAttribute p: + commandBuilder.WithExecutionCheck(p); + break; + + case DescriptionAttribute d: + commandBuilder.WithDescription(d.Description); + break; + + case HiddenAttribute: + commandBuilder.WithHiddenStatus(true); + break; + + default: + commandBuilder.WithCustomAttribute(xa); + break; + } + } + + if (!isModule && moduleHidden) + { + commandBuilder.WithHiddenStatus(true); + } + } + + // candidate types + IEnumerable types = ti.DeclaredNestedTypes + .Where(xt => xt.IsModuleCandidateType() && xt.DeclaredConstructors.Any(xc => xc.IsPublic)); + foreach (TypeInfo? type in types) + { + RegisterCommands(type.AsType(), + groupBuilder, + !isModule ? moduleChecks : Enumerable.Empty(), + out List? tempCommands); + + if (isModule && groupBuilder is not null) + { + foreach (CheckBaseAttribute chk in moduleChecks) + { + groupBuilder.WithExecutionCheck(chk); + } + } + + if (isModule && tempCommands is not null && groupBuilder is not null) + { + foreach (CommandBuilder xtcmd in tempCommands) + { + groupBuilder.WithChild(xtcmd); + } + } + else if (tempCommands != null) + { + commands.AddRange(tempCommands); + } + } + + if (isModule && currentParent is null && groupBuilder is not null) + { + commands.Add(groupBuilder); + } + else if (isModule && currentParent is not null && groupBuilder is not null) + { + currentParent.WithChild(groupBuilder); + } + + foundCommands = commands; + } + + /// + /// Builds and registers all supplied commands. + /// + /// Commands to build and register. + public void RegisterCommands(params CommandBuilder[] cmds) + { + foreach (CommandBuilder cmd in cmds) + { + AddToCommandDictionary(cmd.Build(null)); + } + } + + /// + /// Unregisters specified commands from CommandsNext. + /// + /// Commands to unregister. + public void UnregisterCommands(params Command[] cmds) + { + if (cmds.Any(x => x.Parent is not null)) + { + throw new InvalidOperationException("Cannot unregister nested commands."); + } + + List keys = this.RegisteredCommands.Where(x => cmds.Contains(x.Value)).Select(x => x.Key).ToList(); + foreach (string? key in keys) + { + this.TopLevelCommands.Remove(key); + } + } + + private static string? ExtractCategoryAttribute(MethodInfo method) + { + CategoryAttribute attribute = method.GetCustomAttribute(); + + if (attribute is not null) + { + return attribute.Name; + } + + // extract from types + + return ExtractCategoryAttribute(method.DeclaringType); + } + + private static string? ExtractCategoryAttribute(Type type) + { + CategoryAttribute attribute; + + do + { + attribute = type.GetCustomAttribute(); + + if (attribute is not null) + { + return attribute.Name; + } + + type = type.DeclaringType; + + } while (type is not null); + + return null; + } + + private void AddToCommandDictionary(Command cmd) + { + if (cmd.Parent is not null) + { + return; + } + + if (this.TopLevelCommands.ContainsKey(cmd.Name) || cmd.Aliases.Any(xs => this.TopLevelCommands.ContainsKey(xs))) + { + throw new DuplicateCommandException(cmd.QualifiedName); + } + + this.TopLevelCommands[cmd.Name] = cmd; + + foreach (string xs in cmd.Aliases) + { + this.TopLevelCommands[xs] = cmd; + } + } + #endregion + + #region Default Help + [ModuleLifespan(ModuleLifespan.Transient)] + public class DefaultHelpModule : BaseCommandModule + { + [Command("help"), Description("Displays command help."), SuppressMessage("Quality Assurance", "CA1822:Mark members as static", Justification = "CommandsNext does not support static commands.")] + public async Task DefaultHelpAsync(CommandContext ctx, [Description("Command to provide help for.")] params string[] command) + { + IEnumerable topLevel = ctx.CommandsNext.TopLevelCommands.Values.Distinct(); + BaseHelpFormatter helpBuilder = ctx.CommandsNext.HelpFormatter.Create(ctx); + + if (command != null && command.Length != 0) + { + Command? cmd = null; + IEnumerable? searchIn = topLevel; + foreach (string c in command) + { + if (searchIn is null) + { + cmd = null; + break; + } + + (StringComparison comparison, StringComparer comparer) = ctx.Config.CaseSensitive switch + { + true => (StringComparison.InvariantCulture, StringComparer.InvariantCulture), + false => (StringComparison.InvariantCultureIgnoreCase, StringComparer.InvariantCultureIgnoreCase) + }; + cmd = searchIn.FirstOrDefault(xc => xc.Name.Equals(c, comparison) || xc.Aliases.Contains(c, comparer)); + + if (cmd is null) + { + break; + } + + IEnumerable failedChecks = await cmd.RunChecksAsync(ctx, true); + if (failedChecks.Any()) + { + throw new ChecksFailedException(cmd, ctx, failedChecks); + } + + searchIn = cmd is CommandGroup cmdGroup ? cmdGroup.Children : null; + } + + if (cmd is null) + { + throw new CommandNotFoundException(string.Join(" ", command)); + } + + helpBuilder.WithCommand(cmd); + + if (cmd is CommandGroup group) + { + IEnumerable commandsToSearch = group.Children.Where(xc => !xc.IsHidden); + List eligibleCommands = []; + foreach (Command? candidateCommand in commandsToSearch) + { + if (candidateCommand.ExecutionChecks == null || !candidateCommand.ExecutionChecks.Any()) + { + eligibleCommands.Add(candidateCommand); + continue; + } + + IEnumerable candidateFailedChecks = await candidateCommand.RunChecksAsync(ctx, true); + if (!candidateFailedChecks.Any()) + { + eligibleCommands.Add(candidateCommand); + } + } + + if (eligibleCommands.Count != 0) + { + helpBuilder.WithSubcommands(eligibleCommands.OrderBy(xc => xc.Name)); + } + } + } + else + { + IEnumerable commandsToSearch = topLevel.Where(xc => !xc.IsHidden); + List eligibleCommands = []; + foreach (Command? sc in commandsToSearch) + { + if (sc.ExecutionChecks == null || !sc.ExecutionChecks.Any()) + { + eligibleCommands.Add(sc); + continue; + } + + IEnumerable candidateFailedChecks = await sc.RunChecksAsync(ctx, true); + if (!candidateFailedChecks.Any()) + { + eligibleCommands.Add(sc); + } + } + + if (eligibleCommands.Count != 0) + { + helpBuilder.WithSubcommands(eligibleCommands.OrderBy(xc => xc.Name)); + } + } + + CommandHelpMessage helpMessage = helpBuilder.Build(); + + DiscordMessageBuilder builder = new DiscordMessageBuilder().WithContent(helpMessage.Content).AddEmbed(helpMessage.Embed); + + if (!ctx.Config.DmHelp || ctx.Channel is DiscordDmChannel || ctx.Guild is null || ctx.Member is null) + { + await ctx.RespondAsync(builder); + } + else + { + await ctx.Member.SendMessageAsync(builder); + } + } + } + #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) + { + DateTimeOffset epoch = new(2015, 1, 1, 0, 0, 0, TimeSpan.Zero); + DateTimeOffset now = DateTimeOffset.UtcNow; + ulong timeSpan = (ulong)(now - epoch).TotalMilliseconds; + + // create fake message + DiscordMessage msg = new() + { + Discord = this.Client, + Author = actor, + Channel = channel, + ChannelId = channel.Id, + Content = messageContents, + Id = timeSpan << 22, + Pinned = false, + MentionEveryone = messageContents.Contains("@everyone"), + IsTTS = false, + attachments = [], + embeds = [], + Timestamp = now, + reactions = [] + }; + + List mentionedUsers = []; + List? mentionedRoles = msg.Channel.Guild != null ? [] : null; + List? mentionedChannels = msg.Channel.Guild != null ? [] : null; + + if (!string.IsNullOrWhiteSpace(msg.Content)) + { + if (msg.Channel.Guild != null) + { + mentionedUsers = Utilities.GetUserMentions(msg).Select(xid => msg.Channel.Guild.members.TryGetValue(xid, out DiscordMember? member) ? member : null).Cast().ToList(); + mentionedRoles = Utilities.GetRoleMentions(msg).Select(xid => msg.Channel.Guild.roles.GetValueOrDefault(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.mentionedUsers = mentionedUsers; + msg.mentionedRoles = mentionedRoles; + msg.mentionedChannels = mentionedChannels; + + CommandContext ctx = new() + { + Client = this.Client, + Command = cmd, + Message = msg, + Config = this.Config, + RawArgumentString = rawArguments ?? "", + Prefix = prefix, + CommandsNext = this, + Services = this.Services + }; + + if (cmd is not null && (cmd.Module is TransientCommandModule || cmd.Module is null)) + { + IServiceScope 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 ConvertArgumentAsync(string value, CommandContext ctx) + { + Type t = typeof(T); + if (!this.ArgumentConverters.TryGetValue(t, out IArgumentConverter argumentConverter)) + { + throw new ArgumentException("There is no converter specified for given type.", nameof(T)); + } + + if (argumentConverter is not IArgumentConverter cv) + { + throw new ArgumentException("Invalid converter registered for this type.", nameof(T)); + } + + Optional cvr = await cv.ConvertAsync(value, ctx); + 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 ConvertArgumentAsync(string? value, CommandContext ctx, Type type) + { + MethodInfo m = this.ConvertGeneric.MakeGenericMethod(type); + try + { + return await (Task)m.Invoke(this, [value, ctx]); + } + catch (Exception ex) when (ex is TargetInvocationException or InvalidCastException) + { + 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 is null) + { + throw new ArgumentNullException(nameof(converter), "Converter cannot be null."); + } + + Type t = typeof(T); + TypeInfo ti = t.GetTypeInfo(); + this.ArgumentConverters[t] = converter; + + if (!ti.IsValueType) + { + return; + } + + Type nullableConverterType = typeof(NullableConverter<>).MakeGenericType(t); + Type nullableType = typeof(Nullable<>).MakeGenericType(t); + if (this.ArgumentConverters.ContainsKey(nullableType)) + { + return; + } + + IArgumentConverter? nullableConverter = Activator.CreateInstance(nullableConverterType) as IArgumentConverter; + + if (nullableConverter is not null) + { + this.ArgumentConverters[nullableType] = nullableConverter; + } + } + + /// + /// Unregisters an argument converter for specified type. + /// + /// Type for which to unregister the converter. + public void UnregisterConverter() + { + Type t = typeof(T); + TypeInfo ti = t.GetTypeInfo(); + this.ArgumentConverters.Remove(t); + this.UserFriendlyTypeNames.Remove(t); + + if (!ti.IsValueType) + { + return; + } + + Type nullableType = typeof(Nullable<>).MakeGenericType(t); + if (!this.ArgumentConverters.ContainsKey(nullableType)) + { + return; + } + + this.ArgumentConverters.Remove(nullableType); + this.UserFriendlyTypeNames.Remove(nullableType); + } + + /// + /// Registers a user-friendly type name. + /// + /// Type to register the name for. + /// Name to register. + public void RegisterUserFriendlyTypeName(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + throw new ArgumentNullException(nameof(value), "Name cannot be null or empty."); + } + + Type t = typeof(T); + TypeInfo ti = t.GetTypeInfo(); + if (!this.ArgumentConverters.ContainsKey(t)) + { + throw new InvalidOperationException("Cannot register a friendly name for a type which has no associated converter."); + } + + this.UserFriendlyTypeNames[t] = value; + + if (!ti.IsValueType) + { + return; + } + + Type nullableType = typeof(Nullable<>).MakeGenericType(t); + this.UserFriendlyTypeNames[nullableType] = value; + } + + /// + /// Converts a type into user-friendly type name. + /// + /// Type to convert. + /// User-friendly type name. + public string GetUserFriendlyTypeName(Type t) + { + if (this.UserFriendlyTypeNames.TryGetValue(t, out string value)) + { + return value; + } + + TypeInfo ti = t.GetTypeInfo(); + if (ti.IsGenericTypeDefinition && t.GetGenericTypeDefinition() == typeof(Nullable<>)) + { + Type tn = ti.GenericTypeArguments[0]; + return this.UserFriendlyTypeNames.TryGetValue(tn, out value) ? value : tn.Name; + } + + return t.Name; + } + #endregion + + #region Helpers + /// + /// Gets the configuration-specific string comparer. This returns or , + /// depending on whether is set to or . + /// + /// A string comparer. + internal IEqualityComparer GetStringComparer() + => this.Config.CaseSensitive + ? StringComparer.Ordinal + : StringComparer.OrdinalIgnoreCase; + #endregion + + #region Events + /// + /// Triggered whenever a command executes successfully. + /// + public event AsyncEventHandler CommandExecuted + { + add => this.executed.Register(value); + remove => this.executed.Unregister(value); + } + private AsyncEvent executed = null!; + + /// + /// 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 = null!; + + private Task OnCommandExecuted(CommandExecutionEventArgs e) + => this.executed.InvokeAsync(this, e); + + private Task OnCommandErrored(CommandErrorEventArgs e) + => this.error.InvokeAsync(this, e); + #endregion +} diff --git a/DSharpPlus.CommandsNext/CommandsNextUtilities.cs b/DSharpPlus.CommandsNext/CommandsNextUtilities.cs index 6076752d20..5ac5c25ba1 100644 --- a/DSharpPlus.CommandsNext/CommandsNextUtilities.cs +++ b/DSharpPlus.CommandsNext/CommandsNextUtilities.cs @@ -1,439 +1,439 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using DSharpPlus.CommandsNext.Attributes; -using DSharpPlus.CommandsNext.Converters; -using DSharpPlus.Entities; -using Microsoft.Extensions.DependencyInjection; - -namespace DSharpPlus.CommandsNext; - -/// -/// Various CommandsNext-related utilities. -/// -public static partial class CommandsNextUtilities -{ - /// - /// Checks whether the message has a specified string prefix. - /// - /// Message to check. - /// String to check for. - /// Method of string comparison for the purposes of finding prefixes. - /// Positive number if the prefix is present, -1 otherwise. - public static int GetStringPrefixLength(this DiscordMessage msg, string str, StringComparison comparisonType = StringComparison.Ordinal) - { - string content = msg.Content; - return str.Length >= content.Length ? -1 : !content.StartsWith(str, comparisonType) ? -1 : str.Length; - } - - /// - /// Checks whether the message contains a specified mention prefix. - /// - /// Message to check. - /// User to check for. - /// Positive number if the prefix is present, -1 otherwise. - public static int GetMentionPrefixLength(this DiscordMessage msg, DiscordUser user) - { - string content = msg.Content; - if (!content.StartsWith("<@")) - { - return -1; - } - - int cni = content.IndexOf('>'); - if (cni == -1 || content.Length <= cni + 2) - { - return -1; - } - - string cnp = content[..(cni + 2)]; - Match m = GetUserRegex().Match(cnp); - if (!m.Success) - { - return -1; - } - - ulong userId = ulong.Parse(m.Groups[1].Value, CultureInfo.InvariantCulture); - return user.Id != userId ? -1 : m.Value.Length; - } - - //internal static string ExtractNextArgument(string str, out string remainder) - internal static string? ExtractNextArgument(this string str, ref int startPos, IEnumerable quoteChars) - { - if (string.IsNullOrWhiteSpace(str)) - { - return null; - } - - bool inBacktick = false; - bool inTripleBacktick = false; - bool inQuote = false; - bool inEscape = false; - List removeIndices = new(str.Length - startPos); - - int i = startPos; - for (; i < str.Length; i++) - { - if (!char.IsWhiteSpace(str[i])) - { - break; - } - } - - startPos = i; - - int endPosition = -1; - int startPosition = startPos; - for (i = startPosition; i < str.Length; i++) - { - if (char.IsWhiteSpace(str[i]) && !inQuote && !inTripleBacktick && !inBacktick && !inEscape) - { - endPosition = i; - } - - if (str[i] == '\\' && str.Length > i + 1) - { - if (!inEscape && !inBacktick && !inTripleBacktick) - { - inEscape = true; - if (str.IndexOf("\\`", i) == i || quoteChars.Any(c => str.IndexOf($"\\{c}", i) == i) || str.IndexOf("\\\\", i) == i || (str.Length >= i && char.IsWhiteSpace(str[i + 1]))) - { - removeIndices.Add(i - startPosition); - } - - i++; - } - else if ((inBacktick || inTripleBacktick) && str.IndexOf("\\`", i) == i) - { - inEscape = true; - removeIndices.Add(i - startPosition); - i++; - } - } - - if (str[i] == '`' && !inEscape) - { - bool tripleBacktick = str.IndexOf("```", i) == i; - if (inTripleBacktick && tripleBacktick) - { - inTripleBacktick = false; - i += 2; - } - else if (!inBacktick && tripleBacktick) - { - inTripleBacktick = true; - i += 2; - } - - if (inBacktick && !tripleBacktick) - { - inBacktick = false; - } - else if (!inTripleBacktick && tripleBacktick) - { - inBacktick = true; - } - } - - if (quoteChars.Contains(str[i]) && !inEscape && !inBacktick && !inTripleBacktick) - { - removeIndices.Add(i - startPosition); - - inQuote = !inQuote; - } - - if (inEscape) - { - inEscape = false; - } - - if (endPosition != -1) - { - startPos = endPosition; - return startPosition != endPosition ? str[startPosition..endPosition].CleanupString(removeIndices) : null; - } - } - - startPos = str.Length; - return startPos != startPosition ? str[startPosition..].CleanupString(removeIndices) : null; - } - - internal static string CleanupString(this string s, IList indices) - { - if (!indices.Any()) - { - return s; - } - - int li = indices.Last(); - int ll = 1; - for (int x = indices.Count - 2; x >= 0; x--) - { - if (li - indices[x] == ll) - { - ll++; - continue; - } - - s = s.Remove(li - ll + 1, ll); - li = indices[x]; - ll = 1; - } - - return s.Remove(li - ll + 1, ll); - } - - internal static async Task BindArgumentsAsync(CommandContext ctx, bool ignoreSurplus) - { - CommandOverload overload = ctx.Overload; - - object?[] args = new object?[overload.Arguments.Count + 2]; - args[1] = ctx; - List rawArgumentList = new(overload.Arguments.Count); - string? argString = ctx.RawArgumentString; - int foundAt = 0; - - for (int i = 0; i < overload.Arguments.Count; i++) - { - CommandArgument arg = overload.Arguments[i]; - string? argValue = string.Empty; - if (arg.IsCatchAll) - { - if (arg.isArray) - { - while (true) - { - argValue = ExtractNextArgument(argString, ref foundAt, ctx.Config.QuotationMarks); - if (argValue == null) - { - break; - } - - rawArgumentList.Add(argValue); - } - - break; - } - else - { - if (argString == null) - { - break; - } - - argValue = argString[foundAt..].Trim(); - argValue = argValue == "" ? null : argValue; - foundAt = argString.Length; - - rawArgumentList.Add(argValue); - break; - } - } - else - { - argValue = ExtractNextArgument(argString, ref foundAt, ctx.Config.QuotationMarks); - rawArgumentList.Add(argValue); - } - - if (argValue == null && !arg.IsOptional && !arg.IsCatchAll) - { - return new ArgumentBindingResult(new ArgumentException("Not enough arguments supplied to the command.")); - } - else if (argValue == null) - { - rawArgumentList.Add(null); - } - } - - if (!ignoreSurplus && foundAt < (argString?.Length ?? 0)) - { - return new ArgumentBindingResult(new ArgumentException("Too many arguments were supplied to this command.")); - } - - for (int i = 0; i < overload.Arguments.Count; i++) - { - CommandArgument arg = overload.Arguments[i]; - if (arg.IsCatchAll && arg.isArray) - { - Array array = Array.CreateInstance(arg.Type, rawArgumentList.Count - i); - int start = i; - while (i < rawArgumentList.Count) - { - try - { - array.SetValue(await ctx.CommandsNext.ConvertArgumentAsync(rawArgumentList[i], ctx, arg.Type), i - start); - } - catch (Exception ex) - { - return new ArgumentBindingResult(ex); - } - i++; - } - - args[start + 2] = array; - break; - } - else - { - try - { - args[i + 2] = rawArgumentList[i] != null ? await ctx.CommandsNext.ConvertArgumentAsync(rawArgumentList[i], ctx, arg.Type) : arg.DefaultValue; - } - catch (Exception ex) - { - return new ArgumentBindingResult(ex); - } - } - } - - return new ArgumentBindingResult(args, rawArgumentList.Where(x => x is not null).OfType().ToArray()); - } - - internal static bool IsModuleCandidateType(this Type type) - => type.GetTypeInfo().IsModuleCandidateType(); - - internal static bool IsModuleCandidateType(this TypeInfo ti) - { - // check if compiler-generated - if (ti.GetCustomAttribute(false) != null) - { - return false; - } - - // check if derives from the required base class - Type tmodule = typeof(BaseCommandModule); - TypeInfo timodule = tmodule.GetTypeInfo(); - if (!timodule.IsAssignableFrom(ti)) - { - return false; - } - - // check if anonymous - if (ti.IsGenericType && ti.Name.Contains("AnonymousType") && (ti.Name.StartsWith("<>") || ti.Name.StartsWith("VB$")) && (ti.Attributes & TypeAttributes.NotPublic) == TypeAttributes.NotPublic) - { - return false; - } - - // check if abstract, static, or not a class - if (!ti.IsClass || ti.IsAbstract) - { - return false; - } - - // check if delegate type - TypeInfo tdelegate = typeof(Delegate).GetTypeInfo(); - if (tdelegate.IsAssignableFrom(ti)) - { - return false; - } - - // qualifies if any method or type qualifies - return ti.DeclaredMethods.Any(xmi => xmi.IsCommandCandidate(out _)) || ti.DeclaredNestedTypes.Any(xti => xti.IsModuleCandidateType()); - } - - internal static bool IsCommandCandidate(this MethodInfo method, out ParameterInfo[] parameters) - { - parameters = []; - - // check if exists - if (method == null) - { - return false; - } - - // check if static, non-public, abstract, a constructor, or a special name - if (method.IsStatic || method.IsAbstract || method.IsConstructor || method.IsSpecialName) - { - return false; - } - - // check if appropriate return and arguments - parameters = method.GetParameters(); - if (parameters.Length < 1 || parameters[0].ParameterType != typeof(CommandContext) || method.ReturnType != typeof(Task)) - { - return false; - } - - // qualifies - return true; - } - - internal static object CreateInstance(this Type t, IServiceProvider services) - { - TypeInfo ti = t.GetTypeInfo(); - ConstructorInfo[] 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."); - } - - ConstructorInfo constructor = constructors[0]; - ParameterInfo[] constructorArgs = constructor.GetParameters(); - object[] 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 (int i = 0; i < args.Length; i++) - { - args[i] = services.GetRequiredService(constructorArgs[i].ParameterType); - } - } - - object? moduleInstance = Activator.CreateInstance(t, args); - - // inject into properties - IEnumerable props = t.GetRuntimeProperties().Where(xp => xp.CanWrite && xp.SetMethod != null && !xp.SetMethod.IsStatic && xp.SetMethod.IsPublic); - foreach (PropertyInfo? prop in props) - { - if (prop.GetCustomAttribute() != null) - { - continue; - } - - object? service = services.GetService(prop.PropertyType); - if (service == null) - { - continue; - } - - prop.SetValue(moduleInstance, service); - } - - // inject into fields - IEnumerable fields = t.GetRuntimeFields().Where(xf => !xf.IsInitOnly && !xf.IsStatic && xf.IsPublic); - foreach (FieldInfo? field in fields) - { - if (field.GetCustomAttribute() != null) - { - continue; - } - - object? service = services.GetService(field.FieldType); - if (service == null) - { - continue; - } - - field.SetValue(moduleInstance, service); - } - - return moduleInstance; - } - - [GeneratedRegex(@"<@\!?(\d+?)> ", RegexOptions.ECMAScript)] - private static partial Regex GetUserRegex(); -} +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using DSharpPlus.CommandsNext.Attributes; +using DSharpPlus.CommandsNext.Converters; +using DSharpPlus.Entities; +using Microsoft.Extensions.DependencyInjection; + +namespace DSharpPlus.CommandsNext; + +/// +/// Various CommandsNext-related utilities. +/// +public static partial class CommandsNextUtilities +{ + /// + /// Checks whether the message has a specified string prefix. + /// + /// Message to check. + /// String to check for. + /// Method of string comparison for the purposes of finding prefixes. + /// Positive number if the prefix is present, -1 otherwise. + public static int GetStringPrefixLength(this DiscordMessage msg, string str, StringComparison comparisonType = StringComparison.Ordinal) + { + string content = msg.Content; + return str.Length >= content.Length ? -1 : !content.StartsWith(str, comparisonType) ? -1 : str.Length; + } + + /// + /// Checks whether the message contains a specified mention prefix. + /// + /// Message to check. + /// User to check for. + /// Positive number if the prefix is present, -1 otherwise. + public static int GetMentionPrefixLength(this DiscordMessage msg, DiscordUser user) + { + string content = msg.Content; + if (!content.StartsWith("<@")) + { + return -1; + } + + int cni = content.IndexOf('>'); + if (cni == -1 || content.Length <= cni + 2) + { + return -1; + } + + string cnp = content[..(cni + 2)]; + Match m = GetUserRegex().Match(cnp); + if (!m.Success) + { + return -1; + } + + ulong userId = ulong.Parse(m.Groups[1].Value, CultureInfo.InvariantCulture); + return user.Id != userId ? -1 : m.Value.Length; + } + + //internal static string ExtractNextArgument(string str, out string remainder) + internal static string? ExtractNextArgument(this string str, ref int startPos, IEnumerable quoteChars) + { + if (string.IsNullOrWhiteSpace(str)) + { + return null; + } + + bool inBacktick = false; + bool inTripleBacktick = false; + bool inQuote = false; + bool inEscape = false; + List removeIndices = new(str.Length - startPos); + + int i = startPos; + for (; i < str.Length; i++) + { + if (!char.IsWhiteSpace(str[i])) + { + break; + } + } + + startPos = i; + + int endPosition = -1; + int startPosition = startPos; + for (i = startPosition; i < str.Length; i++) + { + if (char.IsWhiteSpace(str[i]) && !inQuote && !inTripleBacktick && !inBacktick && !inEscape) + { + endPosition = i; + } + + if (str[i] == '\\' && str.Length > i + 1) + { + if (!inEscape && !inBacktick && !inTripleBacktick) + { + inEscape = true; + if (str.IndexOf("\\`", i) == i || quoteChars.Any(c => str.IndexOf($"\\{c}", i) == i) || str.IndexOf("\\\\", i) == i || (str.Length >= i && char.IsWhiteSpace(str[i + 1]))) + { + removeIndices.Add(i - startPosition); + } + + i++; + } + else if ((inBacktick || inTripleBacktick) && str.IndexOf("\\`", i) == i) + { + inEscape = true; + removeIndices.Add(i - startPosition); + i++; + } + } + + if (str[i] == '`' && !inEscape) + { + bool tripleBacktick = str.IndexOf("```", i) == i; + if (inTripleBacktick && tripleBacktick) + { + inTripleBacktick = false; + i += 2; + } + else if (!inBacktick && tripleBacktick) + { + inTripleBacktick = true; + i += 2; + } + + if (inBacktick && !tripleBacktick) + { + inBacktick = false; + } + else if (!inTripleBacktick && tripleBacktick) + { + inBacktick = true; + } + } + + if (quoteChars.Contains(str[i]) && !inEscape && !inBacktick && !inTripleBacktick) + { + removeIndices.Add(i - startPosition); + + inQuote = !inQuote; + } + + if (inEscape) + { + inEscape = false; + } + + if (endPosition != -1) + { + startPos = endPosition; + return startPosition != endPosition ? str[startPosition..endPosition].CleanupString(removeIndices) : null; + } + } + + startPos = str.Length; + return startPos != startPosition ? str[startPosition..].CleanupString(removeIndices) : null; + } + + internal static string CleanupString(this string s, IList indices) + { + if (!indices.Any()) + { + return s; + } + + int li = indices.Last(); + int ll = 1; + for (int x = indices.Count - 2; x >= 0; x--) + { + if (li - indices[x] == ll) + { + ll++; + continue; + } + + s = s.Remove(li - ll + 1, ll); + li = indices[x]; + ll = 1; + } + + return s.Remove(li - ll + 1, ll); + } + + internal static async Task BindArgumentsAsync(CommandContext ctx, bool ignoreSurplus) + { + CommandOverload overload = ctx.Overload; + + object?[] args = new object?[overload.Arguments.Count + 2]; + args[1] = ctx; + List rawArgumentList = new(overload.Arguments.Count); + string? argString = ctx.RawArgumentString; + int foundAt = 0; + + for (int i = 0; i < overload.Arguments.Count; i++) + { + CommandArgument arg = overload.Arguments[i]; + string? argValue = string.Empty; + if (arg.IsCatchAll) + { + if (arg.isArray) + { + while (true) + { + argValue = ExtractNextArgument(argString, ref foundAt, ctx.Config.QuotationMarks); + if (argValue == null) + { + break; + } + + rawArgumentList.Add(argValue); + } + + break; + } + else + { + if (argString == null) + { + break; + } + + argValue = argString[foundAt..].Trim(); + argValue = argValue == "" ? null : argValue; + foundAt = argString.Length; + + rawArgumentList.Add(argValue); + break; + } + } + else + { + argValue = ExtractNextArgument(argString, ref foundAt, ctx.Config.QuotationMarks); + rawArgumentList.Add(argValue); + } + + if (argValue == null && !arg.IsOptional && !arg.IsCatchAll) + { + return new ArgumentBindingResult(new ArgumentException("Not enough arguments supplied to the command.")); + } + else if (argValue == null) + { + rawArgumentList.Add(null); + } + } + + if (!ignoreSurplus && foundAt < (argString?.Length ?? 0)) + { + return new ArgumentBindingResult(new ArgumentException("Too many arguments were supplied to this command.")); + } + + for (int i = 0; i < overload.Arguments.Count; i++) + { + CommandArgument arg = overload.Arguments[i]; + if (arg.IsCatchAll && arg.isArray) + { + Array array = Array.CreateInstance(arg.Type, rawArgumentList.Count - i); + int start = i; + while (i < rawArgumentList.Count) + { + try + { + array.SetValue(await ctx.CommandsNext.ConvertArgumentAsync(rawArgumentList[i], ctx, arg.Type), i - start); + } + catch (Exception ex) + { + return new ArgumentBindingResult(ex); + } + i++; + } + + args[start + 2] = array; + break; + } + else + { + try + { + args[i + 2] = rawArgumentList[i] != null ? await ctx.CommandsNext.ConvertArgumentAsync(rawArgumentList[i], ctx, arg.Type) : arg.DefaultValue; + } + catch (Exception ex) + { + return new ArgumentBindingResult(ex); + } + } + } + + return new ArgumentBindingResult(args, rawArgumentList.Where(x => x is not null).OfType().ToArray()); + } + + internal static bool IsModuleCandidateType(this Type type) + => type.GetTypeInfo().IsModuleCandidateType(); + + internal static bool IsModuleCandidateType(this TypeInfo ti) + { + // check if compiler-generated + if (ti.GetCustomAttribute(false) != null) + { + return false; + } + + // check if derives from the required base class + Type tmodule = typeof(BaseCommandModule); + TypeInfo timodule = tmodule.GetTypeInfo(); + if (!timodule.IsAssignableFrom(ti)) + { + return false; + } + + // check if anonymous + if (ti.IsGenericType && ti.Name.Contains("AnonymousType") && (ti.Name.StartsWith("<>") || ti.Name.StartsWith("VB$")) && (ti.Attributes & TypeAttributes.NotPublic) == TypeAttributes.NotPublic) + { + return false; + } + + // check if abstract, static, or not a class + if (!ti.IsClass || ti.IsAbstract) + { + return false; + } + + // check if delegate type + TypeInfo tdelegate = typeof(Delegate).GetTypeInfo(); + if (tdelegate.IsAssignableFrom(ti)) + { + return false; + } + + // qualifies if any method or type qualifies + return ti.DeclaredMethods.Any(xmi => xmi.IsCommandCandidate(out _)) || ti.DeclaredNestedTypes.Any(xti => xti.IsModuleCandidateType()); + } + + internal static bool IsCommandCandidate(this MethodInfo method, out ParameterInfo[] parameters) + { + parameters = []; + + // check if exists + if (method == null) + { + return false; + } + + // check if static, non-public, abstract, a constructor, or a special name + if (method.IsStatic || method.IsAbstract || method.IsConstructor || method.IsSpecialName) + { + return false; + } + + // check if appropriate return and arguments + parameters = method.GetParameters(); + if (parameters.Length < 1 || parameters[0].ParameterType != typeof(CommandContext) || method.ReturnType != typeof(Task)) + { + return false; + } + + // qualifies + return true; + } + + internal static object CreateInstance(this Type t, IServiceProvider services) + { + TypeInfo ti = t.GetTypeInfo(); + ConstructorInfo[] 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."); + } + + ConstructorInfo constructor = constructors[0]; + ParameterInfo[] constructorArgs = constructor.GetParameters(); + object[] 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 (int i = 0; i < args.Length; i++) + { + args[i] = services.GetRequiredService(constructorArgs[i].ParameterType); + } + } + + object? moduleInstance = Activator.CreateInstance(t, args); + + // inject into properties + IEnumerable props = t.GetRuntimeProperties().Where(xp => xp.CanWrite && xp.SetMethod != null && !xp.SetMethod.IsStatic && xp.SetMethod.IsPublic); + foreach (PropertyInfo? prop in props) + { + if (prop.GetCustomAttribute() != null) + { + continue; + } + + object? service = services.GetService(prop.PropertyType); + if (service == null) + { + continue; + } + + prop.SetValue(moduleInstance, service); + } + + // inject into fields + IEnumerable fields = t.GetRuntimeFields().Where(xf => !xf.IsInitOnly && !xf.IsStatic && xf.IsPublic); + foreach (FieldInfo? field in fields) + { + if (field.GetCustomAttribute() != null) + { + continue; + } + + object? service = services.GetService(field.FieldType); + if (service == null) + { + continue; + } + + field.SetValue(moduleInstance, service); + } + + return moduleInstance; + } + + [GeneratedRegex(@"<@\!?(\d+?)> ", RegexOptions.ECMAScript)] + private static partial Regex GetUserRegex(); +} diff --git a/DSharpPlus.CommandsNext/Converters/ArgumentBindingResult.cs b/DSharpPlus.CommandsNext/Converters/ArgumentBindingResult.cs index d3656febae..e8c7c16be7 100644 --- a/DSharpPlus.CommandsNext/Converters/ArgumentBindingResult.cs +++ b/DSharpPlus.CommandsNext/Converters/ArgumentBindingResult.cs @@ -1,28 +1,28 @@ -using System; -using System.Collections.Generic; - -namespace DSharpPlus.CommandsNext.Converters; - -public readonly struct ArgumentBindingResult -{ - public bool IsSuccessful { get; } - public object?[] Converted { get; } - public IReadOnlyList Raw { get; } - public Exception? Reason { get; } - - public ArgumentBindingResult(object?[] converted, IReadOnlyList raw) - { - this.IsSuccessful = true; - this.Reason = null; - this.Converted = converted; - this.Raw = raw; - } - - public ArgumentBindingResult(Exception ex) - { - this.IsSuccessful = false; - this.Reason = ex; - this.Converted = []; - this.Raw = Array.Empty(); - } -} +using System; +using System.Collections.Generic; + +namespace DSharpPlus.CommandsNext.Converters; + +public readonly struct ArgumentBindingResult +{ + public bool IsSuccessful { get; } + public object?[] Converted { get; } + public IReadOnlyList Raw { get; } + public Exception? Reason { get; } + + public ArgumentBindingResult(object?[] converted, IReadOnlyList raw) + { + this.IsSuccessful = true; + this.Reason = null; + this.Converted = converted; + this.Raw = raw; + } + + public ArgumentBindingResult(Exception ex) + { + this.IsSuccessful = false; + this.Reason = ex; + this.Converted = []; + this.Raw = Array.Empty(); + } +} diff --git a/DSharpPlus.CommandsNext/Converters/BaseHelpFormatter.cs b/DSharpPlus.CommandsNext/Converters/BaseHelpFormatter.cs index d500bfd674..e489c63c57 100644 --- a/DSharpPlus.CommandsNext/Converters/BaseHelpFormatter.cs +++ b/DSharpPlus.CommandsNext/Converters/BaseHelpFormatter.cs @@ -1,46 +1,46 @@ -using System.Collections.Generic; -using DSharpPlus.CommandsNext.Entities; - -namespace DSharpPlus.CommandsNext.Converters; - -/// -/// Represents a base class for all default help formatters. -/// -public abstract class BaseHelpFormatter -{ - /// - /// Gets the context in which this formatter is being invoked. - /// - protected CommandContext Context { get; } - - /// - /// Gets the CommandsNext extension which constructed this help formatter. - /// - protected CommandsNextExtension CommandsNext => this.Context.CommandsNext; - - /// - /// Creates a new help formatter for specified CommandsNext extension instance. - /// - /// Context in which this formatter is being invoked. - public BaseHelpFormatter(CommandContext ctx) => this.Context = ctx; - - /// - /// Sets the command this help message will be for. - /// - /// Command for which the help message is being produced. - /// This help formatter. - public abstract BaseHelpFormatter WithCommand(Command command); - - /// - /// 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 abstract BaseHelpFormatter WithSubcommands(IEnumerable subcommands); - - /// - /// Constructs the help message. - /// - /// Data for the help message. - public abstract CommandHelpMessage Build(); -} +using System.Collections.Generic; +using DSharpPlus.CommandsNext.Entities; + +namespace DSharpPlus.CommandsNext.Converters; + +/// +/// Represents a base class for all default help formatters. +/// +public abstract class BaseHelpFormatter +{ + /// + /// Gets the context in which this formatter is being invoked. + /// + protected CommandContext Context { get; } + + /// + /// Gets the CommandsNext extension which constructed this help formatter. + /// + protected CommandsNextExtension CommandsNext => this.Context.CommandsNext; + + /// + /// Creates a new help formatter for specified CommandsNext extension instance. + /// + /// Context in which this formatter is being invoked. + public BaseHelpFormatter(CommandContext ctx) => this.Context = ctx; + + /// + /// Sets the command this help message will be for. + /// + /// Command for which the help message is being produced. + /// This help formatter. + public abstract BaseHelpFormatter WithCommand(Command command); + + /// + /// 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 abstract BaseHelpFormatter WithSubcommands(IEnumerable subcommands); + + /// + /// Constructs the help message. + /// + /// Data for the help message. + public abstract CommandHelpMessage Build(); +} diff --git a/DSharpPlus.CommandsNext/Converters/DefaultHelpFormatter.cs b/DSharpPlus.CommandsNext/Converters/DefaultHelpFormatter.cs index b8d28c3e16..3e435cf62f 100644 --- a/DSharpPlus.CommandsNext/Converters/DefaultHelpFormatter.cs +++ b/DSharpPlus.CommandsNext/Converters/DefaultHelpFormatter.cs @@ -1,115 +1,115 @@ -using System.Collections.Generic; -using System.Linq; -using System.Text; -using DSharpPlus.CommandsNext.Entities; -using DSharpPlus.Entities; - -namespace DSharpPlus.CommandsNext.Converters; - -/// -/// Default CommandsNext help formatter. -/// -public class DefaultHelpFormatter : BaseHelpFormatter -{ - public DiscordEmbedBuilder EmbedBuilder { get; } - private Command? Command { get; set; } - - /// - /// 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.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.Count > 0) - { - this.EmbedBuilder.AddField("Aliases", string.Join(", ", command.Aliases.Select(Formatter.InlineCode)), false); - } - - if (command.Overloads.Count > 0) - { - StringBuilder sb = new(); - - foreach (CommandOverload? ovl in command.Overloads.OrderByDescending(x => x.Priority)) - { - sb.Append('`').Append(command.QualifiedName); - - foreach (CommandArgument 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 (CommandArgument 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) - { - - IOrderedEnumerable> categories = subcommands.GroupBy(xm => xm.Category).OrderBy(xm => xm.Key == null).ThenBy(xm => xm.Key); - - // no known categories, proceed without categorization - if (categories.Count() == 1 && categories.Single().Key == null) - { - this.EmbedBuilder.AddField(this.Command is not null ? "Subcommands" : "Commands", string.Join(", ", subcommands.Select(x => Formatter.InlineCode(x.Name))), false); - - return this; - } - - foreach (IGrouping? category in categories) - { - this.EmbedBuilder.AddField(category.Key ?? "Uncategorized commands", string.Join(", ", category.Select(xm => Formatter.InlineCode(xm.Name))), false); - } - - return this; - } - - /// - /// Construct the help message. - /// - /// Data for the help message. - public override CommandHelpMessage Build() - { - if (this.Command is 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()); - } -} +using System.Collections.Generic; +using System.Linq; +using System.Text; +using DSharpPlus.CommandsNext.Entities; +using DSharpPlus.Entities; + +namespace DSharpPlus.CommandsNext.Converters; + +/// +/// Default CommandsNext help formatter. +/// +public class DefaultHelpFormatter : BaseHelpFormatter +{ + public DiscordEmbedBuilder EmbedBuilder { get; } + private Command? Command { get; set; } + + /// + /// 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.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.Count > 0) + { + this.EmbedBuilder.AddField("Aliases", string.Join(", ", command.Aliases.Select(Formatter.InlineCode)), false); + } + + if (command.Overloads.Count > 0) + { + StringBuilder sb = new(); + + foreach (CommandOverload? ovl in command.Overloads.OrderByDescending(x => x.Priority)) + { + sb.Append('`').Append(command.QualifiedName); + + foreach (CommandArgument 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 (CommandArgument 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) + { + + IOrderedEnumerable> categories = subcommands.GroupBy(xm => xm.Category).OrderBy(xm => xm.Key == null).ThenBy(xm => xm.Key); + + // no known categories, proceed without categorization + if (categories.Count() == 1 && categories.Single().Key == null) + { + this.EmbedBuilder.AddField(this.Command is not null ? "Subcommands" : "Commands", string.Join(", ", subcommands.Select(x => Formatter.InlineCode(x.Name))), false); + + return this; + } + + foreach (IGrouping? category in categories) + { + this.EmbedBuilder.AddField(category.Key ?? "Uncategorized commands", string.Join(", ", category.Select(xm => Formatter.InlineCode(xm.Name))), false); + } + + return this; + } + + /// + /// Construct the help message. + /// + /// Data for the help message. + public override CommandHelpMessage Build() + { + if (this.Command is 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/DSharpPlus.CommandsNext/Converters/EntityConverters.cs b/DSharpPlus.CommandsNext/Converters/EntityConverters.cs index ed847da5ba..020a496fbf 100644 --- a/DSharpPlus.CommandsNext/Converters/EntityConverters.cs +++ b/DSharpPlus.CommandsNext/Converters/EntityConverters.cs @@ -1,329 +1,329 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using DSharpPlus.Entities; - -namespace DSharpPlus.CommandsNext.Converters; - -public partial class DiscordUserConverter : IArgumentConverter -{ - async Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) - { - if (ulong.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out ulong uid)) - { - DiscordUser result = await ctx.Client.GetUserAsync(uid); - Optional ret = result != null ? Optional.FromValue(result) : Optional.FromNoValue(); - return ret; - } - - Match m = GetUserRegex().Match(value); - if (m.Success && ulong.TryParse(m.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out uid)) - { - DiscordUser result = await ctx.Client.GetUserAsync(uid); - Optional ret = result != null ? Optional.FromValue(result) : Optional.FromNoValue(); - return ret; - } - - bool cs = ctx.Config.CaseSensitive; - - int di = value.IndexOf('#'); - string un = di != -1 ? value[..di] : value; - string? dv = di != -1 ? value[(di + 1)..] : null; - - IEnumerable us = ctx.Client.Guilds.Values - .SelectMany(xkvp => xkvp.Members.Values).Where(xm => - xm.Username.Equals(un, cs ? StringComparison.InvariantCulture : StringComparison.InvariantCultureIgnoreCase) && - ((dv != null && xm.Discriminator == dv) || dv == null)); - - DiscordMember? usr = us.FirstOrDefault(); - return usr != null ? Optional.FromValue(usr) : Optional.FromNoValue(); - } - - [GeneratedRegex(@"^<@\!?(\d+?)>$", RegexOptions.Compiled | RegexOptions.ECMAScript)] - private static partial Regex GetUserRegex(); -} - -public partial class DiscordMemberConverter : IArgumentConverter -{ - async Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) - { - if (ctx.Guild == null) - { - return Optional.FromNoValue(); - } - - if (ulong.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out ulong uid)) - { - DiscordMember result = await ctx.Guild.GetMemberAsync(uid); - Optional ret = result != null ? Optional.FromValue(result) : Optional.FromNoValue(); - return ret; - } - - Match m = GetUserRegex().Match(value); - if (m.Success && ulong.TryParse(m.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out uid)) - { - DiscordMember result = await ctx.Guild.GetMemberAsync(uid); - Optional ret = result != null ? Optional.FromValue(result) : Optional.FromNoValue(); - return ret; - } - - IReadOnlyList searchResult = await ctx.Guild.SearchMembersAsync(value); - if (searchResult.Any()) - { - return Optional.FromValue(searchResult[0]); - } - - bool cs = ctx.Config.CaseSensitive; - - int di = value.IndexOf('#'); - string un = di != -1 ? value[..di] : value; - string? dv = di != -1 ? value[(di + 1)..] : null; - - StringComparison comparison = cs ? StringComparison.InvariantCulture : StringComparison.InvariantCultureIgnoreCase; - IEnumerable us = ctx.Guild.Members.Values.Where(xm => - (xm.Username.Equals(un, comparison) && - ((dv != null && xm.Discriminator == dv) || dv == null)) || value.Equals(xm.Nickname, comparison)); - - DiscordMember? mbr = us.FirstOrDefault(); - return mbr != null ? Optional.FromValue(mbr) : Optional.FromNoValue(); - } - - [GeneratedRegex(@"^<@\!?(\d+?)>$", RegexOptions.Compiled | RegexOptions.ECMAScript)] - private static partial Regex GetUserRegex(); -} - -public partial class DiscordChannelConverter : IArgumentConverter -{ - async Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) - { - if (ulong.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out ulong cid)) - { - DiscordChannel result = await ctx.Client.GetChannelAsync(cid); - return result != null ? Optional.FromValue(result) : Optional.FromNoValue(); - } - - Match m = GetChannelRegex().Match(value); - if (m.Success && ulong.TryParse(m.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out cid)) - { - DiscordChannel result = await ctx.Client.GetChannelAsync(cid); - return result != null ? Optional.FromValue(result) : Optional.FromNoValue(); - } - - bool cs = ctx.Config.CaseSensitive; - - StringComparison comparison = cs ? StringComparison.InvariantCulture : StringComparison.InvariantCultureIgnoreCase; - DiscordChannel? chn = ctx.Guild?.Channels.Values.FirstOrDefault(xc => xc.Name.Equals(value, comparison)) ?? - ctx.Guild?.Threads.Values.FirstOrDefault(xThread => xThread.Name.Equals(value, comparison)); - - return chn != null ? Optional.FromValue(chn) : Optional.FromNoValue(); - } - - [GeneratedRegex(@"^<#(\d+)>$", RegexOptions.Compiled | RegexOptions.ECMAScript)] - private static partial Regex GetChannelRegex(); -} - -public partial class DiscordThreadChannelConverter : IArgumentConverter -{ - Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) - { - if (ulong.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out ulong threadId)) - { - DiscordThreadChannel result = ctx.Client.InternalGetCachedThread(threadId); - return Task.FromResult(result != null ? Optional.FromValue(result) : Optional.FromNoValue()); - } - - Match m = GetThreadRegex().Match(value); - if (m.Success && ulong.TryParse(m.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out threadId)) - { - DiscordThreadChannel result = ctx.Client.InternalGetCachedThread(threadId); - return Task.FromResult(result != null ? Optional.FromValue(result) : Optional.FromNoValue()); - } - - bool cs = ctx.Config.CaseSensitive; - - DiscordThreadChannel? thread = ctx.Guild?.Threads.Values.FirstOrDefault(xt => - xt.Name.Equals(value, cs ? StringComparison.InvariantCulture : StringComparison.InvariantCultureIgnoreCase)); - - return Task.FromResult(thread != null ? Optional.FromValue(thread) : Optional.FromNoValue()); - } - - [GeneratedRegex(@"^<#(\d+)>$", RegexOptions.Compiled | RegexOptions.ECMAScript)] - private static partial Regex GetThreadRegex(); -} - -public partial class DiscordRoleConverter : IArgumentConverter -{ - Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) - { - if (ctx.Guild == null) - { - return Task.FromResult(Optional.FromNoValue()); - } - - if (ulong.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out ulong rid)) - { - DiscordRole? result = ctx.Guild.Roles.GetValueOrDefault(rid); - Optional ret = result != null ? Optional.FromValue(result) : Optional.FromNoValue(); - return Task.FromResult(ret); - } - - Match m = GetRoleRegex().Match(value); - if (m.Success && ulong.TryParse(m.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out rid)) - { - DiscordRole? result = ctx.Guild.Roles.GetValueOrDefault(rid); - Optional ret = result != null ? Optional.FromValue(result) : Optional.FromNoValue(); - return Task.FromResult(ret); - } - - bool cs = ctx.Config.CaseSensitive; - - DiscordRole? rol = ctx.Guild.Roles.Values.FirstOrDefault(xr => - xr.Name.Equals(value, cs ? StringComparison.InvariantCulture : StringComparison.InvariantCultureIgnoreCase)); - return Task.FromResult(rol != null ? Optional.FromValue(rol) : Optional.FromNoValue()); - } - - [GeneratedRegex(@"^<@&(\d+?)>$", RegexOptions.Compiled | RegexOptions.ECMAScript)] - private static partial Regex GetRoleRegex(); -} - -public class DiscordGuildConverter : IArgumentConverter -{ - Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) - { - if (ulong.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out ulong gid)) - { - return ctx.Client.Guilds.TryGetValue(gid, out DiscordGuild? result) - ? Task.FromResult(Optional.FromValue(result)) - : Task.FromResult(Optional.FromNoValue()); - } - - bool cs = ctx.Config.CaseSensitive; - - DiscordGuild? gld = ctx.Client.Guilds.Values.FirstOrDefault(xg => - xg.Name.Equals(value, cs ? StringComparison.InvariantCulture : StringComparison.InvariantCultureIgnoreCase)); - return Task.FromResult(gld != null ? Optional.FromValue(gld) : Optional.FromNoValue()); - } -} - -public partial class DiscordMessageConverter : IArgumentConverter -{ - async Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) - { - if (string.IsNullOrWhiteSpace(value)) - { - return Optional.FromNoValue(); - } - - string msguri = value.StartsWith('<') && value.EndsWith('>') ? value[1..^1] : value; - ulong mid; - if (Uri.TryCreate(msguri, UriKind.Absolute, out Uri? uri)) - { - if (uri.Host != "discordapp.com" && uri.Host != "discord.com" && !uri.Host.EndsWith(".discordapp.com") && !uri.Host.EndsWith(".discord.com")) - { - return Optional.FromNoValue(); - } - - Match uripath = GetMessagePathRegex().Match(uri.AbsolutePath); - if (!uripath.Success - || !ulong.TryParse(uripath.Groups["channel"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out ulong cid) - || !ulong.TryParse(uripath.Groups["message"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out mid)) - { - return Optional.FromNoValue(); - } - - DiscordChannel chn = await ctx.Client.GetChannelAsync(cid); - if (chn == null) - { - return Optional.FromNoValue(); - } - - DiscordMessage msg = await chn.GetMessageAsync(mid); - return msg != null ? Optional.FromValue(msg) : Optional.FromNoValue(); - } - - if (ulong.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out mid)) - { - DiscordMessage result = await ctx.Channel.GetMessageAsync(mid); - return result != null ? Optional.FromValue(result) : Optional.FromNoValue(); - } - - return Optional.FromNoValue(); - } - - [GeneratedRegex(@"^\/channels\/(?(?:\d+|@me))\/(?\d+)\/(?\d+)\/?$", RegexOptions.Compiled | RegexOptions.ECMAScript)] - private static partial Regex GetMessagePathRegex(); -} - -public partial class DiscordEmojiConverter : IArgumentConverter -{ - Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) - { - if (DiscordEmoji.TryFromUnicode(ctx.Client, value, out DiscordEmoji? emoji)) - { - DiscordEmoji result = emoji; - Optional ret = Optional.FromValue(result); - return Task.FromResult(ret); - } - - Match m = GetEmoteRegex().Match(value); - if (m.Success) - { - string sid = m.Groups["id"].Value; - string name = m.Groups["name"].Value; - bool anim = m.Groups["animated"].Success; - - return !ulong.TryParse(sid, NumberStyles.Integer, CultureInfo.InvariantCulture, out ulong id) - ? Task.FromResult(Optional.FromNoValue()) - : DiscordEmoji.TryFromGuildEmote(ctx.Client, id, out emoji) - ? Task.FromResult(Optional.FromValue(emoji)) - : Task.FromResult(Optional.FromValue(new DiscordEmoji - { - Discord = ctx.Client, - Id = id, - Name = name, - IsAnimated = anim, - RequiresColons = true, - IsManaged = false - })); - } - - return Task.FromResult(Optional.FromNoValue()); - } - - [GeneratedRegex(@"^<(?a)?:(?[a-zA-Z0-9_]+?):(?\d+?)>$", RegexOptions.Compiled | RegexOptions.ECMAScript)] - private static partial Regex GetEmoteRegex(); -} - -public partial class DiscordColorConverter : IArgumentConverter -{ - Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) - { - Match m = GetHexRegex().Match(value); - if (m.Success && int.TryParse(m.Groups[1].Value, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out int clr)) - { - return Task.FromResult(Optional.FromValue(clr)); - } - - m = GetRgbRegex().Match(value); - if (m.Success) - { - bool p1 = byte.TryParse(m.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out byte r); - bool p2 = byte.TryParse(m.Groups[2].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out byte g); - bool p3 = byte.TryParse(m.Groups[3].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out byte b); - - return !(p1 && p2 && p3) - ? Task.FromResult(Optional.FromNoValue()) - : Task.FromResult(Optional.FromValue(new DiscordColor(r, g, b))); - } - - return Task.FromResult(Optional.FromNoValue()); - } - - [GeneratedRegex(@"^#?([a-fA-F0-9]{6})$", RegexOptions.Compiled | RegexOptions.ECMAScript)] - private static partial Regex GetHexRegex(); - [GeneratedRegex(@"^(\d{1,3})\s*?,\s*?(\d{1,3}),\s*?(\d{1,3})$", RegexOptions.Compiled | RegexOptions.ECMAScript)] - private static partial Regex GetRgbRegex(); -} +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using DSharpPlus.Entities; + +namespace DSharpPlus.CommandsNext.Converters; + +public partial class DiscordUserConverter : IArgumentConverter +{ + async Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) + { + if (ulong.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out ulong uid)) + { + DiscordUser result = await ctx.Client.GetUserAsync(uid); + Optional ret = result != null ? Optional.FromValue(result) : Optional.FromNoValue(); + return ret; + } + + Match m = GetUserRegex().Match(value); + if (m.Success && ulong.TryParse(m.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out uid)) + { + DiscordUser result = await ctx.Client.GetUserAsync(uid); + Optional ret = result != null ? Optional.FromValue(result) : Optional.FromNoValue(); + return ret; + } + + bool cs = ctx.Config.CaseSensitive; + + int di = value.IndexOf('#'); + string un = di != -1 ? value[..di] : value; + string? dv = di != -1 ? value[(di + 1)..] : null; + + IEnumerable us = ctx.Client.Guilds.Values + .SelectMany(xkvp => xkvp.Members.Values).Where(xm => + xm.Username.Equals(un, cs ? StringComparison.InvariantCulture : StringComparison.InvariantCultureIgnoreCase) && + ((dv != null && xm.Discriminator == dv) || dv == null)); + + DiscordMember? usr = us.FirstOrDefault(); + return usr != null ? Optional.FromValue(usr) : Optional.FromNoValue(); + } + + [GeneratedRegex(@"^<@\!?(\d+?)>$", RegexOptions.Compiled | RegexOptions.ECMAScript)] + private static partial Regex GetUserRegex(); +} + +public partial class DiscordMemberConverter : IArgumentConverter +{ + async Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) + { + if (ctx.Guild == null) + { + return Optional.FromNoValue(); + } + + if (ulong.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out ulong uid)) + { + DiscordMember result = await ctx.Guild.GetMemberAsync(uid); + Optional ret = result != null ? Optional.FromValue(result) : Optional.FromNoValue(); + return ret; + } + + Match m = GetUserRegex().Match(value); + if (m.Success && ulong.TryParse(m.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out uid)) + { + DiscordMember result = await ctx.Guild.GetMemberAsync(uid); + Optional ret = result != null ? Optional.FromValue(result) : Optional.FromNoValue(); + return ret; + } + + IReadOnlyList searchResult = await ctx.Guild.SearchMembersAsync(value); + if (searchResult.Any()) + { + return Optional.FromValue(searchResult[0]); + } + + bool cs = ctx.Config.CaseSensitive; + + int di = value.IndexOf('#'); + string un = di != -1 ? value[..di] : value; + string? dv = di != -1 ? value[(di + 1)..] : null; + + StringComparison comparison = cs ? StringComparison.InvariantCulture : StringComparison.InvariantCultureIgnoreCase; + IEnumerable us = ctx.Guild.Members.Values.Where(xm => + (xm.Username.Equals(un, comparison) && + ((dv != null && xm.Discriminator == dv) || dv == null)) || value.Equals(xm.Nickname, comparison)); + + DiscordMember? mbr = us.FirstOrDefault(); + return mbr != null ? Optional.FromValue(mbr) : Optional.FromNoValue(); + } + + [GeneratedRegex(@"^<@\!?(\d+?)>$", RegexOptions.Compiled | RegexOptions.ECMAScript)] + private static partial Regex GetUserRegex(); +} + +public partial class DiscordChannelConverter : IArgumentConverter +{ + async Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) + { + if (ulong.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out ulong cid)) + { + DiscordChannel result = await ctx.Client.GetChannelAsync(cid); + return result != null ? Optional.FromValue(result) : Optional.FromNoValue(); + } + + Match m = GetChannelRegex().Match(value); + if (m.Success && ulong.TryParse(m.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out cid)) + { + DiscordChannel result = await ctx.Client.GetChannelAsync(cid); + return result != null ? Optional.FromValue(result) : Optional.FromNoValue(); + } + + bool cs = ctx.Config.CaseSensitive; + + StringComparison comparison = cs ? StringComparison.InvariantCulture : StringComparison.InvariantCultureIgnoreCase; + DiscordChannel? chn = ctx.Guild?.Channels.Values.FirstOrDefault(xc => xc.Name.Equals(value, comparison)) ?? + ctx.Guild?.Threads.Values.FirstOrDefault(xThread => xThread.Name.Equals(value, comparison)); + + return chn != null ? Optional.FromValue(chn) : Optional.FromNoValue(); + } + + [GeneratedRegex(@"^<#(\d+)>$", RegexOptions.Compiled | RegexOptions.ECMAScript)] + private static partial Regex GetChannelRegex(); +} + +public partial class DiscordThreadChannelConverter : IArgumentConverter +{ + Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) + { + if (ulong.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out ulong threadId)) + { + DiscordThreadChannel result = ctx.Client.InternalGetCachedThread(threadId); + return Task.FromResult(result != null ? Optional.FromValue(result) : Optional.FromNoValue()); + } + + Match m = GetThreadRegex().Match(value); + if (m.Success && ulong.TryParse(m.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out threadId)) + { + DiscordThreadChannel result = ctx.Client.InternalGetCachedThread(threadId); + return Task.FromResult(result != null ? Optional.FromValue(result) : Optional.FromNoValue()); + } + + bool cs = ctx.Config.CaseSensitive; + + DiscordThreadChannel? thread = ctx.Guild?.Threads.Values.FirstOrDefault(xt => + xt.Name.Equals(value, cs ? StringComparison.InvariantCulture : StringComparison.InvariantCultureIgnoreCase)); + + return Task.FromResult(thread != null ? Optional.FromValue(thread) : Optional.FromNoValue()); + } + + [GeneratedRegex(@"^<#(\d+)>$", RegexOptions.Compiled | RegexOptions.ECMAScript)] + private static partial Regex GetThreadRegex(); +} + +public partial class DiscordRoleConverter : IArgumentConverter +{ + Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) + { + if (ctx.Guild == null) + { + return Task.FromResult(Optional.FromNoValue()); + } + + if (ulong.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out ulong rid)) + { + DiscordRole? result = ctx.Guild.Roles.GetValueOrDefault(rid); + Optional ret = result != null ? Optional.FromValue(result) : Optional.FromNoValue(); + return Task.FromResult(ret); + } + + Match m = GetRoleRegex().Match(value); + if (m.Success && ulong.TryParse(m.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out rid)) + { + DiscordRole? result = ctx.Guild.Roles.GetValueOrDefault(rid); + Optional ret = result != null ? Optional.FromValue(result) : Optional.FromNoValue(); + return Task.FromResult(ret); + } + + bool cs = ctx.Config.CaseSensitive; + + DiscordRole? rol = ctx.Guild.Roles.Values.FirstOrDefault(xr => + xr.Name.Equals(value, cs ? StringComparison.InvariantCulture : StringComparison.InvariantCultureIgnoreCase)); + return Task.FromResult(rol != null ? Optional.FromValue(rol) : Optional.FromNoValue()); + } + + [GeneratedRegex(@"^<@&(\d+?)>$", RegexOptions.Compiled | RegexOptions.ECMAScript)] + private static partial Regex GetRoleRegex(); +} + +public class DiscordGuildConverter : IArgumentConverter +{ + Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) + { + if (ulong.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out ulong gid)) + { + return ctx.Client.Guilds.TryGetValue(gid, out DiscordGuild? result) + ? Task.FromResult(Optional.FromValue(result)) + : Task.FromResult(Optional.FromNoValue()); + } + + bool cs = ctx.Config.CaseSensitive; + + DiscordGuild? gld = ctx.Client.Guilds.Values.FirstOrDefault(xg => + xg.Name.Equals(value, cs ? StringComparison.InvariantCulture : StringComparison.InvariantCultureIgnoreCase)); + return Task.FromResult(gld != null ? Optional.FromValue(gld) : Optional.FromNoValue()); + } +} + +public partial class DiscordMessageConverter : IArgumentConverter +{ + async Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) + { + if (string.IsNullOrWhiteSpace(value)) + { + return Optional.FromNoValue(); + } + + string msguri = value.StartsWith('<') && value.EndsWith('>') ? value[1..^1] : value; + ulong mid; + if (Uri.TryCreate(msguri, UriKind.Absolute, out Uri? uri)) + { + if (uri.Host != "discordapp.com" && uri.Host != "discord.com" && !uri.Host.EndsWith(".discordapp.com") && !uri.Host.EndsWith(".discord.com")) + { + return Optional.FromNoValue(); + } + + Match uripath = GetMessagePathRegex().Match(uri.AbsolutePath); + if (!uripath.Success + || !ulong.TryParse(uripath.Groups["channel"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out ulong cid) + || !ulong.TryParse(uripath.Groups["message"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out mid)) + { + return Optional.FromNoValue(); + } + + DiscordChannel chn = await ctx.Client.GetChannelAsync(cid); + if (chn == null) + { + return Optional.FromNoValue(); + } + + DiscordMessage msg = await chn.GetMessageAsync(mid); + return msg != null ? Optional.FromValue(msg) : Optional.FromNoValue(); + } + + if (ulong.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out mid)) + { + DiscordMessage result = await ctx.Channel.GetMessageAsync(mid); + return result != null ? Optional.FromValue(result) : Optional.FromNoValue(); + } + + return Optional.FromNoValue(); + } + + [GeneratedRegex(@"^\/channels\/(?(?:\d+|@me))\/(?\d+)\/(?\d+)\/?$", RegexOptions.Compiled | RegexOptions.ECMAScript)] + private static partial Regex GetMessagePathRegex(); +} + +public partial class DiscordEmojiConverter : IArgumentConverter +{ + Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) + { + if (DiscordEmoji.TryFromUnicode(ctx.Client, value, out DiscordEmoji? emoji)) + { + DiscordEmoji result = emoji; + Optional ret = Optional.FromValue(result); + return Task.FromResult(ret); + } + + Match m = GetEmoteRegex().Match(value); + if (m.Success) + { + string sid = m.Groups["id"].Value; + string name = m.Groups["name"].Value; + bool anim = m.Groups["animated"].Success; + + return !ulong.TryParse(sid, NumberStyles.Integer, CultureInfo.InvariantCulture, out ulong id) + ? Task.FromResult(Optional.FromNoValue()) + : DiscordEmoji.TryFromGuildEmote(ctx.Client, id, out emoji) + ? Task.FromResult(Optional.FromValue(emoji)) + : Task.FromResult(Optional.FromValue(new DiscordEmoji + { + Discord = ctx.Client, + Id = id, + Name = name, + IsAnimated = anim, + RequiresColons = true, + IsManaged = false + })); + } + + return Task.FromResult(Optional.FromNoValue()); + } + + [GeneratedRegex(@"^<(?a)?:(?[a-zA-Z0-9_]+?):(?\d+?)>$", RegexOptions.Compiled | RegexOptions.ECMAScript)] + private static partial Regex GetEmoteRegex(); +} + +public partial class DiscordColorConverter : IArgumentConverter +{ + Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) + { + Match m = GetHexRegex().Match(value); + if (m.Success && int.TryParse(m.Groups[1].Value, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out int clr)) + { + return Task.FromResult(Optional.FromValue(clr)); + } + + m = GetRgbRegex().Match(value); + if (m.Success) + { + bool p1 = byte.TryParse(m.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out byte r); + bool p2 = byte.TryParse(m.Groups[2].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out byte g); + bool p3 = byte.TryParse(m.Groups[3].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out byte b); + + return !(p1 && p2 && p3) + ? Task.FromResult(Optional.FromNoValue()) + : Task.FromResult(Optional.FromValue(new DiscordColor(r, g, b))); + } + + return Task.FromResult(Optional.FromNoValue()); + } + + [GeneratedRegex(@"^#?([a-fA-F0-9]{6})$", RegexOptions.Compiled | RegexOptions.ECMAScript)] + private static partial Regex GetHexRegex(); + [GeneratedRegex(@"^(\d{1,3})\s*?,\s*?(\d{1,3}),\s*?(\d{1,3})$", RegexOptions.Compiled | RegexOptions.ECMAScript)] + private static partial Regex GetRgbRegex(); +} diff --git a/DSharpPlus.CommandsNext/Converters/EnumConverter.cs b/DSharpPlus.CommandsNext/Converters/EnumConverter.cs index b813312ce0..7eeb515007 100644 --- a/DSharpPlus.CommandsNext/Converters/EnumConverter.cs +++ b/DSharpPlus.CommandsNext/Converters/EnumConverter.cs @@ -1,24 +1,24 @@ -using System; -using System.Reflection; -using System.Threading.Tasks; -using DSharpPlus.Entities; - -namespace DSharpPlus.CommandsNext.Converters; - -/// -/// Converts a string to an enum type. -/// -/// Type of enum to convert. -public class EnumConverter : IArgumentConverter where T : struct, IComparable, IConvertible, IFormattable -{ - Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) - { - Type t = typeof(T); - TypeInfo ti = t.GetTypeInfo(); - return !ti.IsEnum - ? throw new InvalidOperationException("Cannot convert non-enum value to an enum.") - : Enum.TryParse(value, !ctx.Config.CaseSensitive, out T ev) - ? Task.FromResult(Optional.FromValue(ev)) - : Task.FromResult(Optional.FromNoValue()); - } -} +using System; +using System.Reflection; +using System.Threading.Tasks; +using DSharpPlus.Entities; + +namespace DSharpPlus.CommandsNext.Converters; + +/// +/// Converts a string to an enum type. +/// +/// Type of enum to convert. +public class EnumConverter : IArgumentConverter where T : struct, IComparable, IConvertible, IFormattable +{ + Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) + { + Type t = typeof(T); + TypeInfo ti = t.GetTypeInfo(); + return !ti.IsEnum + ? throw new InvalidOperationException("Cannot convert non-enum value to an enum.") + : Enum.TryParse(value, !ctx.Config.CaseSensitive, out T ev) + ? Task.FromResult(Optional.FromValue(ev)) + : Task.FromResult(Optional.FromNoValue()); + } +} diff --git a/DSharpPlus.CommandsNext/Converters/HelpFormatterFactory.cs b/DSharpPlus.CommandsNext/Converters/HelpFormatterFactory.cs index 7c383b84be..bf50e6ca26 100644 --- a/DSharpPlus.CommandsNext/Converters/HelpFormatterFactory.cs +++ b/DSharpPlus.CommandsNext/Converters/HelpFormatterFactory.cs @@ -1,18 +1,18 @@ -using System; -using Microsoft.Extensions.DependencyInjection; - -namespace DSharpPlus.CommandsNext.Converters; - -internal class HelpFormatterFactory -{ - private ObjectFactory Factory { get; set; } = null!; - - public HelpFormatterFactory() { } - - public void SetFormatterType() where T : BaseHelpFormatter => this.Factory = ActivatorUtilities.CreateFactory(typeof(T), [typeof(CommandContext)]); - - public BaseHelpFormatter Create(CommandContext ctx) - => this.Factory is null - ? throw new InvalidOperationException($"A formatter type must be set with the {nameof(SetFormatterType)} method.") - : (BaseHelpFormatter)this.Factory(ctx.Services, [ctx]); -} +using System; +using Microsoft.Extensions.DependencyInjection; + +namespace DSharpPlus.CommandsNext.Converters; + +internal class HelpFormatterFactory +{ + private ObjectFactory Factory { get; set; } = null!; + + public HelpFormatterFactory() { } + + public void SetFormatterType() where T : BaseHelpFormatter => this.Factory = ActivatorUtilities.CreateFactory(typeof(T), [typeof(CommandContext)]); + + public BaseHelpFormatter Create(CommandContext ctx) + => this.Factory is null + ? throw new InvalidOperationException($"A formatter type must be set with the {nameof(SetFormatterType)} method.") + : (BaseHelpFormatter)this.Factory(ctx.Services, [ctx]); +} diff --git a/DSharpPlus.CommandsNext/Converters/IArgumentConverter.cs b/DSharpPlus.CommandsNext/Converters/IArgumentConverter.cs index 7472c4239f..3f30ec866a 100644 --- a/DSharpPlus.CommandsNext/Converters/IArgumentConverter.cs +++ b/DSharpPlus.CommandsNext/Converters/IArgumentConverter.cs @@ -1,25 +1,25 @@ -using System.Threading.Tasks; -using DSharpPlus.Entities; - -namespace DSharpPlus.CommandsNext.Converters; - -/// -/// Argument converter abstract. -/// -public interface IArgumentConverter -{ } - -/// -/// Represents a converter for specific argument type. -/// -/// Type for which the converter is to be registered. -public interface IArgumentConverter : IArgumentConverter -{ - /// - /// Converts the raw value into the specified type. - /// - /// Value to convert. - /// Context in which the value will be converted. - /// A structure containing information whether the value was converted, and, if so, the converted value. - public Task> ConvertAsync(string value, CommandContext ctx); -} +using System.Threading.Tasks; +using DSharpPlus.Entities; + +namespace DSharpPlus.CommandsNext.Converters; + +/// +/// Argument converter abstract. +/// +public interface IArgumentConverter +{ } + +/// +/// Represents a converter for specific argument type. +/// +/// Type for which the converter is to be registered. +public interface IArgumentConverter : IArgumentConverter +{ + /// + /// Converts the raw value into the specified type. + /// + /// Value to convert. + /// Context in which the value will be converted. + /// A structure containing information whether the value was converted, and, if so, the converted value. + public Task> ConvertAsync(string value, CommandContext ctx); +} diff --git a/DSharpPlus.CommandsNext/Converters/NullableConverter.cs b/DSharpPlus.CommandsNext/Converters/NullableConverter.cs index 7f79a55ad6..6c4e0a053f 100644 --- a/DSharpPlus.CommandsNext/Converters/NullableConverter.cs +++ b/DSharpPlus.CommandsNext/Converters/NullableConverter.cs @@ -1,30 +1,30 @@ -using System; -using System.Threading.Tasks; -using DSharpPlus.Entities; - -namespace DSharpPlus.CommandsNext.Converters; - -public class NullableConverter : IArgumentConverter> where T : struct -{ - async Task>> IArgumentConverter>.ConvertAsync(string value, CommandContext ctx) - { - if (!ctx.Config.CaseSensitive) - { - value = value.ToLowerInvariant(); - } - - if (value == "null") - { - return Optional.FromValue>(null); - } - - if (ctx.CommandsNext.ArgumentConverters.TryGetValue(typeof(T), out IArgumentConverter? cv)) - { - IArgumentConverter cvx = (IArgumentConverter)cv; - Optional val = await cvx.ConvertAsync(value, ctx); - return val.HasValue ? Optional.FromValue>(val.Value) : Optional.FromNoValue>(); - } - - return Optional.FromNoValue>(); - } -} +using System; +using System.Threading.Tasks; +using DSharpPlus.Entities; + +namespace DSharpPlus.CommandsNext.Converters; + +public class NullableConverter : IArgumentConverter> where T : struct +{ + async Task>> IArgumentConverter>.ConvertAsync(string value, CommandContext ctx) + { + if (!ctx.Config.CaseSensitive) + { + value = value.ToLowerInvariant(); + } + + if (value == "null") + { + return Optional.FromValue>(null); + } + + if (ctx.CommandsNext.ArgumentConverters.TryGetValue(typeof(T), out IArgumentConverter? cv)) + { + IArgumentConverter cvx = (IArgumentConverter)cv; + Optional val = await cvx.ConvertAsync(value, ctx); + return val.HasValue ? Optional.FromValue>(val.Value) : Optional.FromNoValue>(); + } + + return Optional.FromNoValue>(); + } +} diff --git a/DSharpPlus.CommandsNext/Converters/NumericConverters.cs b/DSharpPlus.CommandsNext/Converters/NumericConverters.cs index 28a35549cc..cd7e9ce5ba 100644 --- a/DSharpPlus.CommandsNext/Converters/NumericConverters.cs +++ b/DSharpPlus.CommandsNext/Converters/NumericConverters.cs @@ -1,89 +1,89 @@ -using System.Globalization; -using System.Threading.Tasks; -using DSharpPlus.Entities; - -namespace DSharpPlus.CommandsNext.Converters; - -public class BoolConverter : IArgumentConverter -{ - Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) => bool.TryParse(value, out bool result) - ? Task.FromResult(Optional.FromValue(result)) - : Task.FromResult(Optional.FromNoValue()); -} - -public class Int8Converter : IArgumentConverter -{ - Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) => sbyte.TryParse(value, NumberStyles.Integer, ctx.CommandsNext.DefaultParserCulture, out sbyte result) - ? Task.FromResult(Optional.FromValue(result)) - : Task.FromResult(Optional.FromNoValue()); -} - -public class Uint8Converter : IArgumentConverter -{ - Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) => byte.TryParse(value, NumberStyles.Integer, ctx.CommandsNext.DefaultParserCulture, out byte result) - ? Task.FromResult(Optional.FromValue(result)) - : Task.FromResult(Optional.FromNoValue()); -} - -public class Int16Converter : IArgumentConverter -{ - Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) => short.TryParse(value, NumberStyles.Integer, ctx.CommandsNext.DefaultParserCulture, out short result) - ? Task.FromResult(Optional.FromValue(result)) - : Task.FromResult(Optional.FromNoValue()); -} - -public class Uint16Converter : IArgumentConverter -{ - Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) => ushort.TryParse(value, NumberStyles.Integer, ctx.CommandsNext.DefaultParserCulture, out ushort result) - ? Task.FromResult(Optional.FromValue(result)) - : Task.FromResult(Optional.FromNoValue()); -} - -public class Int32Converter : IArgumentConverter -{ - Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) => int.TryParse(value, NumberStyles.Integer, ctx.CommandsNext.DefaultParserCulture, out int result) - ? Task.FromResult(Optional.FromValue(result)) - : Task.FromResult(Optional.FromNoValue()); -} - -public class Uint32Converter : IArgumentConverter -{ - Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) => uint.TryParse(value, NumberStyles.Integer, ctx.CommandsNext.DefaultParserCulture, out uint result) - ? Task.FromResult(Optional.FromValue(result)) - : Task.FromResult(Optional.FromNoValue()); -} - -public class Int64Converter : IArgumentConverter -{ - Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) => long.TryParse(value, NumberStyles.Integer, ctx.CommandsNext.DefaultParserCulture, out long result) - ? Task.FromResult(Optional.FromValue(result)) - : Task.FromResult(Optional.FromNoValue()); -} - -public class Uint64Converter : IArgumentConverter -{ - Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) => ulong.TryParse(value, NumberStyles.Integer, ctx.CommandsNext.DefaultParserCulture, out ulong result) - ? Task.FromResult(Optional.FromValue(result)) - : Task.FromResult(Optional.FromNoValue()); -} - -public class Float32Converter : IArgumentConverter -{ - Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) => float.TryParse(value, NumberStyles.Number, ctx.CommandsNext.DefaultParserCulture, out float result) - ? Task.FromResult(Optional.FromValue(result)) - : Task.FromResult(Optional.FromNoValue()); -} - -public class Float64Converter : IArgumentConverter -{ - Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) => double.TryParse(value, NumberStyles.Number, ctx.CommandsNext.DefaultParserCulture, out double result) - ? Task.FromResult(Optional.FromValue(result)) - : Task.FromResult(Optional.FromNoValue()); -} - -public class Float128Converter : IArgumentConverter -{ - Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) => decimal.TryParse(value, NumberStyles.Number, ctx.CommandsNext.DefaultParserCulture, out decimal result) - ? Task.FromResult(Optional.FromValue(result)) - : Task.FromResult(Optional.FromNoValue()); -} +using System.Globalization; +using System.Threading.Tasks; +using DSharpPlus.Entities; + +namespace DSharpPlus.CommandsNext.Converters; + +public class BoolConverter : IArgumentConverter +{ + Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) => bool.TryParse(value, out bool result) + ? Task.FromResult(Optional.FromValue(result)) + : Task.FromResult(Optional.FromNoValue()); +} + +public class Int8Converter : IArgumentConverter +{ + Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) => sbyte.TryParse(value, NumberStyles.Integer, ctx.CommandsNext.DefaultParserCulture, out sbyte result) + ? Task.FromResult(Optional.FromValue(result)) + : Task.FromResult(Optional.FromNoValue()); +} + +public class Uint8Converter : IArgumentConverter +{ + Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) => byte.TryParse(value, NumberStyles.Integer, ctx.CommandsNext.DefaultParserCulture, out byte result) + ? Task.FromResult(Optional.FromValue(result)) + : Task.FromResult(Optional.FromNoValue()); +} + +public class Int16Converter : IArgumentConverter +{ + Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) => short.TryParse(value, NumberStyles.Integer, ctx.CommandsNext.DefaultParserCulture, out short result) + ? Task.FromResult(Optional.FromValue(result)) + : Task.FromResult(Optional.FromNoValue()); +} + +public class Uint16Converter : IArgumentConverter +{ + Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) => ushort.TryParse(value, NumberStyles.Integer, ctx.CommandsNext.DefaultParserCulture, out ushort result) + ? Task.FromResult(Optional.FromValue(result)) + : Task.FromResult(Optional.FromNoValue()); +} + +public class Int32Converter : IArgumentConverter +{ + Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) => int.TryParse(value, NumberStyles.Integer, ctx.CommandsNext.DefaultParserCulture, out int result) + ? Task.FromResult(Optional.FromValue(result)) + : Task.FromResult(Optional.FromNoValue()); +} + +public class Uint32Converter : IArgumentConverter +{ + Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) => uint.TryParse(value, NumberStyles.Integer, ctx.CommandsNext.DefaultParserCulture, out uint result) + ? Task.FromResult(Optional.FromValue(result)) + : Task.FromResult(Optional.FromNoValue()); +} + +public class Int64Converter : IArgumentConverter +{ + Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) => long.TryParse(value, NumberStyles.Integer, ctx.CommandsNext.DefaultParserCulture, out long result) + ? Task.FromResult(Optional.FromValue(result)) + : Task.FromResult(Optional.FromNoValue()); +} + +public class Uint64Converter : IArgumentConverter +{ + Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) => ulong.TryParse(value, NumberStyles.Integer, ctx.CommandsNext.DefaultParserCulture, out ulong result) + ? Task.FromResult(Optional.FromValue(result)) + : Task.FromResult(Optional.FromNoValue()); +} + +public class Float32Converter : IArgumentConverter +{ + Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) => float.TryParse(value, NumberStyles.Number, ctx.CommandsNext.DefaultParserCulture, out float result) + ? Task.FromResult(Optional.FromValue(result)) + : Task.FromResult(Optional.FromNoValue()); +} + +public class Float64Converter : IArgumentConverter +{ + Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) => double.TryParse(value, NumberStyles.Number, ctx.CommandsNext.DefaultParserCulture, out double result) + ? Task.FromResult(Optional.FromValue(result)) + : Task.FromResult(Optional.FromNoValue()); +} + +public class Float128Converter : IArgumentConverter +{ + Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) => decimal.TryParse(value, NumberStyles.Number, ctx.CommandsNext.DefaultParserCulture, out decimal result) + ? Task.FromResult(Optional.FromValue(result)) + : Task.FromResult(Optional.FromNoValue()); +} diff --git a/DSharpPlus.CommandsNext/Converters/StringConverter.cs b/DSharpPlus.CommandsNext/Converters/StringConverter.cs index 66246570a9..7fdd38fe95 100644 --- a/DSharpPlus.CommandsNext/Converters/StringConverter.cs +++ b/DSharpPlus.CommandsNext/Converters/StringConverter.cs @@ -1,31 +1,31 @@ -using System; -using System.Threading.Tasks; -using DSharpPlus.Entities; - -namespace DSharpPlus.CommandsNext.Converters; - -public class StringConverter : IArgumentConverter -{ - Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) - => Task.FromResult(Optional.FromValue(value)); -} - -public class UriConverter : IArgumentConverter -{ - Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) - { - try - { - if (value.StartsWith('<') && value.EndsWith('>')) - { - value = value[1..^1]; - } - - return Task.FromResult(Optional.FromValue(new Uri(value))); - } - catch - { - return Task.FromResult(Optional.FromNoValue()); - } - } -} +using System; +using System.Threading.Tasks; +using DSharpPlus.Entities; + +namespace DSharpPlus.CommandsNext.Converters; + +public class StringConverter : IArgumentConverter +{ + Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) + => Task.FromResult(Optional.FromValue(value)); +} + +public class UriConverter : IArgumentConverter +{ + Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) + { + try + { + if (value.StartsWith('<') && value.EndsWith('>')) + { + value = value[1..^1]; + } + + return Task.FromResult(Optional.FromValue(new Uri(value))); + } + catch + { + return Task.FromResult(Optional.FromNoValue()); + } + } +} diff --git a/DSharpPlus.CommandsNext/Converters/TimeConverters.cs b/DSharpPlus.CommandsNext/Converters/TimeConverters.cs index c38f1f49f4..91ca2db236 100644 --- a/DSharpPlus.CommandsNext/Converters/TimeConverters.cs +++ b/DSharpPlus.CommandsNext/Converters/TimeConverters.cs @@ -1,60 +1,60 @@ -using System; -using System.Globalization; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using DSharpPlus.Entities; - -namespace DSharpPlus.CommandsNext.Converters; - -public class DateTimeConverter : IArgumentConverter -{ - Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) => DateTime.TryParse(value, ctx.CommandsNext.DefaultParserCulture, DateTimeStyles.None, out DateTime result) - ? Task.FromResult(new Optional(result)) - : Task.FromResult(Optional.FromNoValue()); -} - -public class DateTimeOffsetConverter : IArgumentConverter -{ - Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) => DateTimeOffset.TryParse(value, ctx.CommandsNext.DefaultParserCulture, DateTimeStyles.None, out DateTimeOffset result) - ? Task.FromResult(Optional.FromValue(result)) - : Task.FromResult(Optional.FromNoValue()); -} - -public partial class TimeSpanConverter : IArgumentConverter -{ - Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) - { - if (value == "0") - { - return Task.FromResult(Optional.FromValue(TimeSpan.Zero)); - } - - if (int.TryParse(value, NumberStyles.Number, ctx.CommandsNext.DefaultParserCulture, out _)) - { - return Task.FromResult(Optional.FromNoValue()); - } - - if (!ctx.Config.CaseSensitive) - { - value = value.ToLowerInvariant(); - } - - if (TimeSpan.TryParse(value, ctx.CommandsNext.DefaultParserCulture, out TimeSpan result)) - { - return Task.FromResult(Optional.FromValue(result)); - } - - Match m = GetTimeSpanRegex().Match(value); - - int ds = m.Groups["days"].Success ? int.Parse(m.Groups["days"].Value) : 0; - int hs = m.Groups["hours"].Success ? int.Parse(m.Groups["hours"].Value) : 0; - int ms = m.Groups["minutes"].Success ? int.Parse(m.Groups["minutes"].Value) : 0; - int ss = m.Groups["seconds"].Success ? int.Parse(m.Groups["seconds"].Value) : 0; - - result = TimeSpan.FromSeconds((ds * 24 * 60 * 60) + (hs * 60 * 60) + (ms * 60) + ss); - return result.TotalSeconds < 1 ? Task.FromResult(Optional.FromNoValue()) : Task.FromResult(Optional.FromValue(result)); - } - - [GeneratedRegex(@"^((?\d+)d\s*)?((?\d+)h\s*)?((?\d+)m\s*)?((?\d+)s\s*)?$", RegexOptions.ExplicitCapture | RegexOptions.Compiled | RegexOptions.RightToLeft | RegexOptions.CultureInvariant)] - private static partial Regex GetTimeSpanRegex(); -} +using System; +using System.Globalization; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using DSharpPlus.Entities; + +namespace DSharpPlus.CommandsNext.Converters; + +public class DateTimeConverter : IArgumentConverter +{ + Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) => DateTime.TryParse(value, ctx.CommandsNext.DefaultParserCulture, DateTimeStyles.None, out DateTime result) + ? Task.FromResult(new Optional(result)) + : Task.FromResult(Optional.FromNoValue()); +} + +public class DateTimeOffsetConverter : IArgumentConverter +{ + Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) => DateTimeOffset.TryParse(value, ctx.CommandsNext.DefaultParserCulture, DateTimeStyles.None, out DateTimeOffset result) + ? Task.FromResult(Optional.FromValue(result)) + : Task.FromResult(Optional.FromNoValue()); +} + +public partial class TimeSpanConverter : IArgumentConverter +{ + Task> IArgumentConverter.ConvertAsync(string value, CommandContext ctx) + { + if (value == "0") + { + return Task.FromResult(Optional.FromValue(TimeSpan.Zero)); + } + + if (int.TryParse(value, NumberStyles.Number, ctx.CommandsNext.DefaultParserCulture, out _)) + { + return Task.FromResult(Optional.FromNoValue()); + } + + if (!ctx.Config.CaseSensitive) + { + value = value.ToLowerInvariant(); + } + + if (TimeSpan.TryParse(value, ctx.CommandsNext.DefaultParserCulture, out TimeSpan result)) + { + return Task.FromResult(Optional.FromValue(result)); + } + + Match m = GetTimeSpanRegex().Match(value); + + int ds = m.Groups["days"].Success ? int.Parse(m.Groups["days"].Value) : 0; + int hs = m.Groups["hours"].Success ? int.Parse(m.Groups["hours"].Value) : 0; + int ms = m.Groups["minutes"].Success ? int.Parse(m.Groups["minutes"].Value) : 0; + int ss = m.Groups["seconds"].Success ? int.Parse(m.Groups["seconds"].Value) : 0; + + result = TimeSpan.FromSeconds((ds * 24 * 60 * 60) + (hs * 60 * 60) + (ms * 60) + ss); + return result.TotalSeconds < 1 ? Task.FromResult(Optional.FromNoValue()) : Task.FromResult(Optional.FromValue(result)); + } + + [GeneratedRegex(@"^((?\d+)d\s*)?((?\d+)h\s*)?((?\d+)m\s*)?((?\d+)s\s*)?$", RegexOptions.ExplicitCapture | RegexOptions.Compiled | RegexOptions.RightToLeft | RegexOptions.CultureInvariant)] + private static partial Regex GetTimeSpanRegex(); +} diff --git a/DSharpPlus.CommandsNext/Entities/Builders/CommandBuilder.cs b/DSharpPlus.CommandsNext/Entities/Builders/CommandBuilder.cs index 2da61c0ab2..213d48f3fb 100644 --- a/DSharpPlus.CommandsNext/Entities/Builders/CommandBuilder.cs +++ b/DSharpPlus.CommandsNext/Entities/Builders/CommandBuilder.cs @@ -1,297 +1,297 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; -using DSharpPlus.CommandsNext.Attributes; -using DSharpPlus.CommandsNext.Entities; -using DSharpPlus.CommandsNext.Exceptions; - -namespace DSharpPlus.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; } = null!; - - /// - /// Gets the category set for this command. - /// - public string? Category { get; private set; } - - /// - /// Gets the aliases set for this command. - /// - public IReadOnlyList Aliases { get; } - private List aliasList { get; } - - /// - /// 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; } - private List executionCheckList { get; } - - /// - /// Gets the collection of this command's overloads. - /// - public IReadOnlyList Overloads { get; } - private List overloadList { get; } - private HashSet overloadArgumentSets { get; } - - /// - /// 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; } - private List customAttributeList { get; } - - /// - /// 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 = []; - this.Aliases = new ReadOnlyCollection(this.aliasList); - - this.executionCheckList = []; - this.ExecutionChecks = new ReadOnlyCollection(this.executionCheckList); - - this.overloadArgumentSets = []; - this.overloadList = []; - this.Overloads = new ReadOnlyCollection(this.overloadList); - - this.Module = module; - - this.customAttributeList = []; - 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)); - } - else if (this.Name != null) - { - throw new InvalidOperationException("This command already has a name."); - } - else if (this.aliasList.Contains(name)) - { - throw new ArgumentException("Command name cannot be one of its aliases.", nameof(name)); - } - - this.Name = name; - return this; - } - - /// - /// Sets the category for this command. - /// - /// Category for this command. May be . - /// This builder. - public CommandBuilder WithCategory(string? category) - { - this.Category = category; - 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.Length == 0) - { - throw new ArgumentException("You need to pass at least one alias.", nameof(aliases)); - } - - foreach (string alias in aliases) - { - 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)) - { - throw new ArgumentException("Aliases cannot contain the command name, and cannot be duplicate.", nameof(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)); - 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); - } - - 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 (CommandOverloadBuilder overload in overloads) - { - 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)) - { - throw new DuplicateOverloadException(this.Name, overload.Arguments.Select(x => x.Type).ToList(), overload.argumentSet); - } - - 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); - 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 (Attribute attr in attributes) - { - WithCustomAttribute(attr); - } - - return this; - } - - internal virtual Command Build(CommandGroup? parent) - { - Command cmd = new() - { - Name = string.IsNullOrWhiteSpace(this.Name) - ? throw new InvalidOperationException($"Cannot build a command with an invalid name. Use the method {nameof(WithName)} to set a valid name.") - : this.Name, - - Category = this.Category, - 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; - } -} +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using DSharpPlus.CommandsNext.Attributes; +using DSharpPlus.CommandsNext.Entities; +using DSharpPlus.CommandsNext.Exceptions; + +namespace DSharpPlus.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; } = null!; + + /// + /// Gets the category set for this command. + /// + public string? Category { get; private set; } + + /// + /// Gets the aliases set for this command. + /// + public IReadOnlyList Aliases { get; } + private List aliasList { get; } + + /// + /// 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; } + private List executionCheckList { get; } + + /// + /// Gets the collection of this command's overloads. + /// + public IReadOnlyList Overloads { get; } + private List overloadList { get; } + private HashSet overloadArgumentSets { get; } + + /// + /// 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; } + private List customAttributeList { get; } + + /// + /// 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 = []; + this.Aliases = new ReadOnlyCollection(this.aliasList); + + this.executionCheckList = []; + this.ExecutionChecks = new ReadOnlyCollection(this.executionCheckList); + + this.overloadArgumentSets = []; + this.overloadList = []; + this.Overloads = new ReadOnlyCollection(this.overloadList); + + this.Module = module; + + this.customAttributeList = []; + 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)); + } + else if (this.Name != null) + { + throw new InvalidOperationException("This command already has a name."); + } + else if (this.aliasList.Contains(name)) + { + throw new ArgumentException("Command name cannot be one of its aliases.", nameof(name)); + } + + this.Name = name; + return this; + } + + /// + /// Sets the category for this command. + /// + /// Category for this command. May be . + /// This builder. + public CommandBuilder WithCategory(string? category) + { + this.Category = category; + 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.Length == 0) + { + throw new ArgumentException("You need to pass at least one alias.", nameof(aliases)); + } + + foreach (string alias in aliases) + { + 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)) + { + throw new ArgumentException("Aliases cannot contain the command name, and cannot be duplicate.", nameof(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)); + 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); + } + + 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 (CommandOverloadBuilder overload in overloads) + { + 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)) + { + throw new DuplicateOverloadException(this.Name, overload.Arguments.Select(x => x.Type).ToList(), overload.argumentSet); + } + + 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); + 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 (Attribute attr in attributes) + { + WithCustomAttribute(attr); + } + + return this; + } + + internal virtual Command Build(CommandGroup? parent) + { + Command cmd = new() + { + Name = string.IsNullOrWhiteSpace(this.Name) + ? throw new InvalidOperationException($"Cannot build a command with an invalid name. Use the method {nameof(WithName)} to set a valid name.") + : this.Name, + + Category = this.Category, + 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/DSharpPlus.CommandsNext/Entities/Builders/CommandGroupBuilder.cs b/DSharpPlus.CommandsNext/Entities/Builders/CommandGroupBuilder.cs index 24c9cd798d..26bf05d085 100644 --- a/DSharpPlus.CommandsNext/Entities/Builders/CommandGroupBuilder.cs +++ b/DSharpPlus.CommandsNext/Entities/Builders/CommandGroupBuilder.cs @@ -1,70 +1,70 @@ -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; -using DSharpPlus.CommandsNext.Entities; - -namespace DSharpPlus.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; } - private List childrenList { get; } - - /// - /// 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 = []; - 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); - return this; - } - - internal override Command Build(CommandGroup? parent) - { - CommandGroup cmd = new() - { - 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, - Category = this.Category - }; - - List cs = []; - foreach (CommandBuilder xc in this.Children) - { - cs.Add(xc.Build(cmd)); - } - - cmd.Children = new ReadOnlyCollection(cs); - return cmd; - } -} +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using DSharpPlus.CommandsNext.Entities; + +namespace DSharpPlus.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; } + private List childrenList { get; } + + /// + /// 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 = []; + 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); + return this; + } + + internal override Command Build(CommandGroup? parent) + { + CommandGroup cmd = new() + { + 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, + Category = this.Category + }; + + List cs = []; + foreach (CommandBuilder xc in this.Children) + { + cs.Add(xc.Build(cmd)); + } + + cmd.Children = new ReadOnlyCollection(cs); + return cmd; + } +} diff --git a/DSharpPlus.CommandsNext/Entities/Builders/CommandModuleBuilder.cs b/DSharpPlus.CommandsNext/Entities/Builders/CommandModuleBuilder.cs index 05a3efd82b..56adb9fdb0 100644 --- a/DSharpPlus.CommandsNext/Entities/Builders/CommandModuleBuilder.cs +++ b/DSharpPlus.CommandsNext/Entities/Builders/CommandModuleBuilder.cs @@ -1,62 +1,62 @@ -using System; -using DSharpPlus.CommandsNext.Attributes; -using DSharpPlus.CommandsNext.Entities; - -namespace DSharpPlus.CommandsNext.Builders; - -/// -/// Represents an interface to build a command module. -/// -public sealed class CommandModuleBuilder -{ - /// - /// Gets the type this build will construct a module out of. - /// - public Type Type { get; private set; } = null!; - - /// - /// Gets the lifespan for the built module. - /// - public ModuleLifespan Lifespan { get; private set; } - - /// - /// Creates a new command module builder. - /// - public CommandModuleBuilder() { } - - /// - /// Sets the type this builder will construct a module out of. - /// - /// Type to build a module out of. It has to derive from . - /// This builder. - public CommandModuleBuilder WithType(Type t) - { - if (!t.IsModuleCandidateType()) - { - throw new ArgumentException("Specified type is not a valid module type.", nameof(t)); - } - - this.Type = t; - return this; - } - - /// - /// Lifespan to give this module. - /// - /// Lifespan for this module. - /// This builder. - public CommandModuleBuilder WithLifespan(ModuleLifespan lifespan) - { - this.Lifespan = lifespan; - return this; - } - - internal ICommandModule Build(IServiceProvider services) => this.Type is null - ? throw new InvalidOperationException($"A command module cannot be built without a module type, please use the {nameof(WithType)} method to set a type.") - : this.Lifespan switch - { - ModuleLifespan.Singleton => new SingletonCommandModule(this.Type, services), - ModuleLifespan.Transient => new TransientCommandModule(this.Type), - _ => throw new NotSupportedException("Module lifespans other than transient and singleton are not supported."), - }; -} +using System; +using DSharpPlus.CommandsNext.Attributes; +using DSharpPlus.CommandsNext.Entities; + +namespace DSharpPlus.CommandsNext.Builders; + +/// +/// Represents an interface to build a command module. +/// +public sealed class CommandModuleBuilder +{ + /// + /// Gets the type this build will construct a module out of. + /// + public Type Type { get; private set; } = null!; + + /// + /// Gets the lifespan for the built module. + /// + public ModuleLifespan Lifespan { get; private set; } + + /// + /// Creates a new command module builder. + /// + public CommandModuleBuilder() { } + + /// + /// Sets the type this builder will construct a module out of. + /// + /// Type to build a module out of. It has to derive from . + /// This builder. + public CommandModuleBuilder WithType(Type t) + { + if (!t.IsModuleCandidateType()) + { + throw new ArgumentException("Specified type is not a valid module type.", nameof(t)); + } + + this.Type = t; + return this; + } + + /// + /// Lifespan to give this module. + /// + /// Lifespan for this module. + /// This builder. + public CommandModuleBuilder WithLifespan(ModuleLifespan lifespan) + { + this.Lifespan = lifespan; + return this; + } + + internal ICommandModule Build(IServiceProvider services) => this.Type is null + ? throw new InvalidOperationException($"A command module cannot be built without a module type, please use the {nameof(WithType)} method to set a type.") + : this.Lifespan switch + { + ModuleLifespan.Singleton => new SingletonCommandModule(this.Type, services), + ModuleLifespan.Transient => new TransientCommandModule(this.Type), + _ => throw new NotSupportedException("Module lifespans other than transient and singleton are not supported."), + }; +} diff --git a/DSharpPlus.CommandsNext/Entities/Builders/CommandOverloadBuilder.cs b/DSharpPlus.CommandsNext/Entities/Builders/CommandOverloadBuilder.cs index d35f62531a..ec8602bf3b 100644 --- a/DSharpPlus.CommandsNext/Entities/Builders/CommandOverloadBuilder.cs +++ b/DSharpPlus.CommandsNext/Entities/Builders/CommandOverloadBuilder.cs @@ -1,162 +1,162 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; -using System.Text; -using DSharpPlus.CommandsNext.Attributes; -using DSharpPlus.CommandsNext.Exceptions; - -namespace DSharpPlus.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; } = Array.Empty(); - - /// - /// 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; } - - private object? invocationTarget { get; } - - /// - /// 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) { } - - private CommandOverloadBuilder(MethodInfo method, object? target) - { - if (!method.IsCommandCandidate(out ParameterInfo[]? prms)) - { - throw new ArgumentException("Specified method is not suitable for a command.", nameof(method)); - } - - this.invocationTarget = target; - - // create the argument array - ParameterExpression[] ea = new ParameterExpression[prms.Length + 1]; - ParameterExpression iep = Expression.Parameter(target?.GetType() ?? method.DeclaringType, "instance"); - ea[0] = iep; - ea[1] = Expression.Parameter(typeof(CommandContext), "ctx"); - - PriorityAttribute? pri = method.GetCustomAttribute(); - if (pri != null) - { - this.Priority = pri.Priority; - } - - int i = 2; - List args = new(prms.Length - 1); - StringBuilder setb = new(); - foreach (ParameterInfo? arg in prms.Skip(1)) - { - setb.Append(arg.ParameterType).Append(';'); - CommandArgument ca = new() - { - Name = arg.Name, - Type = arg.ParameterType, - IsOptional = arg.IsOptional, - DefaultValue = arg.IsOptional ? arg.DefaultValue : null - }; - - List attrsCustom = []; - IEnumerable attrs = arg.GetCustomAttributes(); - bool isParams = false; - foreach (Attribute xa in attrs) - { - switch (xa) - { - case DescriptionAttribute d: - ca.Description = d.Description; - break; - - case RemainingTextAttribute: - ca.IsCatchAll = true; - break; - - case ParamArrayAttribute: - 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)); - MethodCallExpression ec = Expression.Call(iep, method, ea.Skip(1)); - LambdaExpression 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; - } - - internal CommandOverload Build() - { - CommandOverload ovl = new() - { - Arguments = this.Arguments, - Priority = this.Priority, - callable = this.Callable, - invocationTarget = this.invocationTarget - }; - - return ovl; - } -} +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Text; +using DSharpPlus.CommandsNext.Attributes; +using DSharpPlus.CommandsNext.Exceptions; + +namespace DSharpPlus.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; } = Array.Empty(); + + /// + /// 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; } + + private object? invocationTarget { get; } + + /// + /// 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) { } + + private CommandOverloadBuilder(MethodInfo method, object? target) + { + if (!method.IsCommandCandidate(out ParameterInfo[]? prms)) + { + throw new ArgumentException("Specified method is not suitable for a command.", nameof(method)); + } + + this.invocationTarget = target; + + // create the argument array + ParameterExpression[] ea = new ParameterExpression[prms.Length + 1]; + ParameterExpression iep = Expression.Parameter(target?.GetType() ?? method.DeclaringType, "instance"); + ea[0] = iep; + ea[1] = Expression.Parameter(typeof(CommandContext), "ctx"); + + PriorityAttribute? pri = method.GetCustomAttribute(); + if (pri != null) + { + this.Priority = pri.Priority; + } + + int i = 2; + List args = new(prms.Length - 1); + StringBuilder setb = new(); + foreach (ParameterInfo? arg in prms.Skip(1)) + { + setb.Append(arg.ParameterType).Append(';'); + CommandArgument ca = new() + { + Name = arg.Name, + Type = arg.ParameterType, + IsOptional = arg.IsOptional, + DefaultValue = arg.IsOptional ? arg.DefaultValue : null + }; + + List attrsCustom = []; + IEnumerable attrs = arg.GetCustomAttributes(); + bool isParams = false; + foreach (Attribute xa in attrs) + { + switch (xa) + { + case DescriptionAttribute d: + ca.Description = d.Description; + break; + + case RemainingTextAttribute: + ca.IsCatchAll = true; + break; + + case ParamArrayAttribute: + 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)); + MethodCallExpression ec = Expression.Call(iep, method, ea.Skip(1)); + LambdaExpression 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; + } + + internal CommandOverload Build() + { + CommandOverload ovl = new() + { + Arguments = this.Arguments, + Priority = this.Priority, + callable = this.Callable, + invocationTarget = this.invocationTarget + }; + + return ovl; + } +} diff --git a/DSharpPlus.CommandsNext/Entities/Command.cs b/DSharpPlus.CommandsNext/Entities/Command.cs index f67ca29360..019ce53419 100644 --- a/DSharpPlus.CommandsNext/Entities/Command.cs +++ b/DSharpPlus.CommandsNext/Entities/Command.cs @@ -1,207 +1,207 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Runtime.ExceptionServices; -using System.Threading.Tasks; -using DSharpPlus.CommandsNext.Attributes; -using DSharpPlus.CommandsNext.Converters; -using DSharpPlus.CommandsNext.Entities; - -namespace DSharpPlus.CommandsNext; - -/// -/// Represents a command. -/// -public class Command -{ - /// - /// Gets this command's name. - /// - public string Name { get; internal set; } = string.Empty; - - /// - /// Gets the category this command belongs to. - /// - public string? Category { get; internal set; } = null; - - /// - /// Gets this command's qualified name (i.e. one that includes all module names). - /// - public string QualifiedName => this.Parent is not null ? string.Concat(this.Parent.QualifiedName, " ", this.Name) : this.Name; - - /// - /// Gets this command's aliases. - /// - public IReadOnlyList Aliases { get; internal set; } = Array.Empty(); - - /// - /// Gets this command's parent module, if any. - /// - public CommandGroup? Parent { get; internal set; } - - /// - /// Gets this command's description. - /// - public string? Description { get; internal set; } - - /// - /// Gets whether this command is hidden. - /// - public bool IsHidden { get; internal set; } - - /// - /// Gets a collection of pre-execution checks for this command. - /// - public IReadOnlyList ExecutionChecks { get; internal set; } = Array.Empty(); - - /// - /// Gets a collection of this command's overloads. - /// - public IReadOnlyList Overloads { get; internal set; } = Array.Empty(); - - /// - /// Gets the module in which this command is defined. - /// - public ICommandModule? Module { get; internal set; } - - /// - /// Gets the custom attributes defined on this command. - /// - public IReadOnlyList CustomAttributes { get; internal set; } = Array.Empty(); - - internal Command() { } - - /// - /// Executes this command with specified context. - /// - /// Context to execute the command in. - /// Command's execution results. - public virtual async Task ExecuteAsync(CommandContext ctx) - { - try - { - foreach (CommandOverload? overload in this.Overloads.OrderByDescending(x => x.Priority)) - { - ctx.Overload = overload; - - // Attempt to match the arguments to the overload - ArgumentBindingResult args = await CommandsNextUtilities.BindArgumentsAsync(ctx, ctx.Config.IgnoreExtraArguments); - if (!args.IsSuccessful) - { - continue; - } - - ctx.RawArguments = args.Raw; - - // From... what I can gather, this seems to be support for executing commands that don't inherit from BaseCommandModule. - // But, that can never be the case since all Commands must inherit from BaseCommandModule. - // Regardless, I'm not removing this legacy code in case if it's actually used and I'm just not seeing it. - BaseCommandModule? commandModule = this.Module?.GetInstance(ctx.Services); - if (commandModule is not null) - { - await commandModule.BeforeExecutionAsync(ctx); - } - - args.Converted[0] = overload.invocationTarget ?? commandModule; - await (Task)overload.callable.DynamicInvoke(args.Converted)!; - - if (commandModule is not null) - { - await commandModule.AfterExecutionAsync(ctx); - } - - return new CommandResult - { - IsSuccessful = true, - Context = ctx - }; - } - - throw new ArgumentException("Could not find a suitable overload for the command."); - } - catch (Exception error) - { - if (error is TargetInvocationException targetInvocationError) - { - error = ExceptionDispatchInfo.Capture(targetInvocationError.InnerException!).SourceException; - } - - return new CommandResult - { - IsSuccessful = false, - Exception = error, - Context = ctx - }; - } - } - - /// - /// Runs pre-execution checks for this command and returns any that fail for given context. - /// - /// Context in which the command is executed. - /// Whether this check is being executed from help or not. This can be used to probe whether command can be run without setting off certain fail conditions (such as cooldowns). - /// Pre-execution checks that fail for given context. - public async Task> RunChecksAsync(CommandContext ctx, bool help) - { - List fchecks = []; - if (this.ExecutionChecks.Any()) - { - foreach (CheckBaseAttribute ec in this.ExecutionChecks) - { - if (!await ec.ExecuteCheckAsync(ctx, help)) - { - fchecks.Add(ec); - } - } - } - - return fchecks; - } - - /// - /// Checks whether this command is equal to another one. - /// - /// Command to compare to. - /// Command to compare. - /// Whether the two commands are equal. - public static bool operator ==(Command? cmd1, Command? cmd2) - { - object? o1 = cmd1; - object? o2 = cmd2; - return (o1 != null || o2 == null) && (o1 == null || o2 != null) && ((o1 == null && o2 == null) || cmd1!.QualifiedName == cmd2!.QualifiedName); - } - - /// - /// Checks whether this command is not equal to another one. - /// - /// Command to compare to. - /// Command to compare. - /// Whether the two commands are not equal. - public static bool operator !=(Command? cmd1, Command? cmd2) => !(cmd1 == cmd2); - - /// - /// Checks whether this command equals another object. - /// - /// Object to compare to. - /// Whether this command is equal to another object. - public override bool Equals(object? obj) - { - object? o2 = this; - return (obj != null || o2 == null) && (obj == null || o2 != null) && ((obj == null && o2 == null) || (obj is Command cmd && cmd.QualifiedName == this.QualifiedName)); - } - - /// - /// Gets this command's hash code. - /// - /// This command's hash code. - public override int GetHashCode() => this.QualifiedName.GetHashCode(); - - /// - /// Returns a string representation of this command. - /// - /// String representation of this command. - public override string ToString() => this is CommandGroup g - ? $"Command Group: {this.QualifiedName}, {g.Children.Count} top-level children" - : $"Command: {this.QualifiedName}"; -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.ExceptionServices; +using System.Threading.Tasks; +using DSharpPlus.CommandsNext.Attributes; +using DSharpPlus.CommandsNext.Converters; +using DSharpPlus.CommandsNext.Entities; + +namespace DSharpPlus.CommandsNext; + +/// +/// Represents a command. +/// +public class Command +{ + /// + /// Gets this command's name. + /// + public string Name { get; internal set; } = string.Empty; + + /// + /// Gets the category this command belongs to. + /// + public string? Category { get; internal set; } = null; + + /// + /// Gets this command's qualified name (i.e. one that includes all module names). + /// + public string QualifiedName => this.Parent is not null ? string.Concat(this.Parent.QualifiedName, " ", this.Name) : this.Name; + + /// + /// Gets this command's aliases. + /// + public IReadOnlyList Aliases { get; internal set; } = Array.Empty(); + + /// + /// Gets this command's parent module, if any. + /// + public CommandGroup? Parent { get; internal set; } + + /// + /// Gets this command's description. + /// + public string? Description { get; internal set; } + + /// + /// Gets whether this command is hidden. + /// + public bool IsHidden { get; internal set; } + + /// + /// Gets a collection of pre-execution checks for this command. + /// + public IReadOnlyList ExecutionChecks { get; internal set; } = Array.Empty(); + + /// + /// Gets a collection of this command's overloads. + /// + public IReadOnlyList Overloads { get; internal set; } = Array.Empty(); + + /// + /// Gets the module in which this command is defined. + /// + public ICommandModule? Module { get; internal set; } + + /// + /// Gets the custom attributes defined on this command. + /// + public IReadOnlyList CustomAttributes { get; internal set; } = Array.Empty(); + + internal Command() { } + + /// + /// Executes this command with specified context. + /// + /// Context to execute the command in. + /// Command's execution results. + public virtual async Task ExecuteAsync(CommandContext ctx) + { + try + { + foreach (CommandOverload? overload in this.Overloads.OrderByDescending(x => x.Priority)) + { + ctx.Overload = overload; + + // Attempt to match the arguments to the overload + ArgumentBindingResult args = await CommandsNextUtilities.BindArgumentsAsync(ctx, ctx.Config.IgnoreExtraArguments); + if (!args.IsSuccessful) + { + continue; + } + + ctx.RawArguments = args.Raw; + + // From... what I can gather, this seems to be support for executing commands that don't inherit from BaseCommandModule. + // But, that can never be the case since all Commands must inherit from BaseCommandModule. + // Regardless, I'm not removing this legacy code in case if it's actually used and I'm just not seeing it. + BaseCommandModule? commandModule = this.Module?.GetInstance(ctx.Services); + if (commandModule is not null) + { + await commandModule.BeforeExecutionAsync(ctx); + } + + args.Converted[0] = overload.invocationTarget ?? commandModule; + await (Task)overload.callable.DynamicInvoke(args.Converted)!; + + if (commandModule is not null) + { + await commandModule.AfterExecutionAsync(ctx); + } + + return new CommandResult + { + IsSuccessful = true, + Context = ctx + }; + } + + throw new ArgumentException("Could not find a suitable overload for the command."); + } + catch (Exception error) + { + if (error is TargetInvocationException targetInvocationError) + { + error = ExceptionDispatchInfo.Capture(targetInvocationError.InnerException!).SourceException; + } + + return new CommandResult + { + IsSuccessful = false, + Exception = error, + Context = ctx + }; + } + } + + /// + /// Runs pre-execution checks for this command and returns any that fail for given context. + /// + /// Context in which the command is executed. + /// Whether this check is being executed from help or not. This can be used to probe whether command can be run without setting off certain fail conditions (such as cooldowns). + /// Pre-execution checks that fail for given context. + public async Task> RunChecksAsync(CommandContext ctx, bool help) + { + List fchecks = []; + if (this.ExecutionChecks.Any()) + { + foreach (CheckBaseAttribute ec in this.ExecutionChecks) + { + if (!await ec.ExecuteCheckAsync(ctx, help)) + { + fchecks.Add(ec); + } + } + } + + return fchecks; + } + + /// + /// Checks whether this command is equal to another one. + /// + /// Command to compare to. + /// Command to compare. + /// Whether the two commands are equal. + public static bool operator ==(Command? cmd1, Command? cmd2) + { + object? o1 = cmd1; + object? o2 = cmd2; + return (o1 != null || o2 == null) && (o1 == null || o2 != null) && ((o1 == null && o2 == null) || cmd1!.QualifiedName == cmd2!.QualifiedName); + } + + /// + /// Checks whether this command is not equal to another one. + /// + /// Command to compare to. + /// Command to compare. + /// Whether the two commands are not equal. + public static bool operator !=(Command? cmd1, Command? cmd2) => !(cmd1 == cmd2); + + /// + /// Checks whether this command equals another object. + /// + /// Object to compare to. + /// Whether this command is equal to another object. + public override bool Equals(object? obj) + { + object? o2 = this; + return (obj != null || o2 == null) && (obj == null || o2 != null) && ((obj == null && o2 == null) || (obj is Command cmd && cmd.QualifiedName == this.QualifiedName)); + } + + /// + /// Gets this command's hash code. + /// + /// This command's hash code. + public override int GetHashCode() => this.QualifiedName.GetHashCode(); + + /// + /// Returns a string representation of this command. + /// + /// String representation of this command. + public override string ToString() => this is CommandGroup g + ? $"Command Group: {this.QualifiedName}, {g.Children.Count} top-level children" + : $"Command: {this.QualifiedName}"; +} diff --git a/DSharpPlus.CommandsNext/Entities/CommandArgument.cs b/DSharpPlus.CommandsNext/Entities/CommandArgument.cs index 0f6144197f..c86fa8393c 100644 --- a/DSharpPlus.CommandsNext/Entities/CommandArgument.cs +++ b/DSharpPlus.CommandsNext/Entities/CommandArgument.cs @@ -1,47 +1,47 @@ -using System; -using System.Collections.Generic; - -namespace DSharpPlus.CommandsNext; - -public sealed class CommandArgument -{ - /// - /// Gets this argument's name. - /// - public string Name { get; internal set; } = string.Empty; - - /// - /// Gets this argument's type. - /// - public Type Type { get; internal set; } = null!; - - /// - /// Gets or sets whether this argument is an array argument. - /// - internal bool isArray { get; set; } = false; - - /// - /// Gets whether this argument is optional. - /// - public bool IsOptional { get; internal set; } - - /// - /// Gets this argument's default value. - /// - public object? DefaultValue { get; internal set; } - - /// - /// Gets whether this argument catches all remaining arguments. - /// - public bool IsCatchAll { get; internal set; } - - /// - /// Gets this argument's description. - /// - public string? Description { get; internal set; } - - /// - /// Gets the custom attributes attached to this argument. - /// - public IReadOnlyCollection CustomAttributes { get; internal set; } = Array.Empty(); -} +using System; +using System.Collections.Generic; + +namespace DSharpPlus.CommandsNext; + +public sealed class CommandArgument +{ + /// + /// Gets this argument's name. + /// + public string Name { get; internal set; } = string.Empty; + + /// + /// Gets this argument's type. + /// + public Type Type { get; internal set; } = null!; + + /// + /// Gets or sets whether this argument is an array argument. + /// + internal bool isArray { get; set; } = false; + + /// + /// Gets whether this argument is optional. + /// + public bool IsOptional { get; internal set; } + + /// + /// Gets this argument's default value. + /// + public object? DefaultValue { get; internal set; } + + /// + /// Gets whether this argument catches all remaining arguments. + /// + public bool IsCatchAll { get; internal set; } + + /// + /// Gets this argument's description. + /// + public string? Description { get; internal set; } + + /// + /// Gets the custom attributes attached to this argument. + /// + public IReadOnlyCollection CustomAttributes { get; internal set; } = Array.Empty(); +} diff --git a/DSharpPlus.CommandsNext/Entities/CommandGroup.cs b/DSharpPlus.CommandsNext/Entities/CommandGroup.cs index 8f8a01bb03..2cc41476db 100644 --- a/DSharpPlus.CommandsNext/Entities/CommandGroup.cs +++ b/DSharpPlus.CommandsNext/Entities/CommandGroup.cs @@ -1,80 +1,80 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using DSharpPlus.CommandsNext.Exceptions; - -namespace DSharpPlus.CommandsNext; - -/// -/// Represents a command group. -/// -public class CommandGroup : Command -{ - /// - /// Gets all the commands that belong to this module. - /// - public IReadOnlyList Children { get; internal set; } = Array.Empty(); - - /// - /// Gets whether this command is executable without subcommands. - /// - public bool IsExecutableWithoutSubcommands => this.Overloads.Count > 0; - - internal CommandGroup() : base() { } - - /// - /// Executes this command or its subcommand with specified context. - /// - /// Context to execute the command in. - /// Command's execution results. - public override async Task ExecuteAsync(CommandContext ctx) - { - int findpos = 0; - string? cn = CommandsNextUtilities.ExtractNextArgument(ctx.RawArgumentString, ref findpos, ctx.Config.QuotationMarks); - - if (cn != null) - { - (StringComparison comparison, StringComparer comparer) = ctx.Config.CaseSensitive - ? (StringComparison.InvariantCulture, StringComparer.InvariantCulture) - : (StringComparison.InvariantCultureIgnoreCase, StringComparer.InvariantCultureIgnoreCase); - - Command? cmd = this.Children.FirstOrDefault(xc => xc.Name.Equals(cn, comparison) || xc.Aliases.Contains(cn, comparer)); - - if (cmd is not null) - { - // pass the execution on - CommandContext xctx = new() - { - Client = ctx.Client, - Message = ctx.Message, - Command = cmd, - Config = ctx.Config, - RawArgumentString = ctx.RawArgumentString[findpos..], - Prefix = ctx.Prefix, - CommandsNext = ctx.CommandsNext, - Services = ctx.Services - }; - - IEnumerable fchecks = await cmd.RunChecksAsync(xctx, false); - return !fchecks.Any() - ? await cmd.ExecuteAsync(xctx) - : new CommandResult - { - IsSuccessful = false, - Exception = new ChecksFailedException(cmd, xctx, fchecks), - Context = xctx - }; - } - } - - return this.IsExecutableWithoutSubcommands - ? await base.ExecuteAsync(ctx) - : new CommandResult - { - IsSuccessful = false, - Exception = new InvalidOperationException("No matching subcommands were found, and this group is not executable."), - Context = ctx - }; - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using DSharpPlus.CommandsNext.Exceptions; + +namespace DSharpPlus.CommandsNext; + +/// +/// Represents a command group. +/// +public class CommandGroup : Command +{ + /// + /// Gets all the commands that belong to this module. + /// + public IReadOnlyList Children { get; internal set; } = Array.Empty(); + + /// + /// Gets whether this command is executable without subcommands. + /// + public bool IsExecutableWithoutSubcommands => this.Overloads.Count > 0; + + internal CommandGroup() : base() { } + + /// + /// Executes this command or its subcommand with specified context. + /// + /// Context to execute the command in. + /// Command's execution results. + public override async Task ExecuteAsync(CommandContext ctx) + { + int findpos = 0; + string? cn = CommandsNextUtilities.ExtractNextArgument(ctx.RawArgumentString, ref findpos, ctx.Config.QuotationMarks); + + if (cn != null) + { + (StringComparison comparison, StringComparer comparer) = ctx.Config.CaseSensitive + ? (StringComparison.InvariantCulture, StringComparer.InvariantCulture) + : (StringComparison.InvariantCultureIgnoreCase, StringComparer.InvariantCultureIgnoreCase); + + Command? cmd = this.Children.FirstOrDefault(xc => xc.Name.Equals(cn, comparison) || xc.Aliases.Contains(cn, comparer)); + + if (cmd is not null) + { + // pass the execution on + CommandContext xctx = new() + { + Client = ctx.Client, + Message = ctx.Message, + Command = cmd, + Config = ctx.Config, + RawArgumentString = ctx.RawArgumentString[findpos..], + Prefix = ctx.Prefix, + CommandsNext = ctx.CommandsNext, + Services = ctx.Services + }; + + IEnumerable fchecks = await cmd.RunChecksAsync(xctx, false); + return !fchecks.Any() + ? await cmd.ExecuteAsync(xctx) + : new CommandResult + { + IsSuccessful = false, + Exception = new ChecksFailedException(cmd, xctx, fchecks), + Context = xctx + }; + } + } + + return this.IsExecutableWithoutSubcommands + ? await base.ExecuteAsync(ctx) + : new CommandResult + { + IsSuccessful = false, + Exception = new InvalidOperationException("No matching subcommands were found, and this group is not executable."), + Context = ctx + }; + } +} diff --git a/DSharpPlus.CommandsNext/Entities/CommandHelpMessage.cs b/DSharpPlus.CommandsNext/Entities/CommandHelpMessage.cs index 6450929b83..6846c97cb0 100644 --- a/DSharpPlus.CommandsNext/Entities/CommandHelpMessage.cs +++ b/DSharpPlus.CommandsNext/Entities/CommandHelpMessage.cs @@ -1,30 +1,30 @@ -using DSharpPlus.Entities; - -namespace DSharpPlus.CommandsNext.Entities; - -/// -/// Represents a formatted help message. -/// -public readonly struct CommandHelpMessage -{ - /// - /// Gets the contents of the help message. - /// - public string? Content { get; } - - /// - /// Gets the embed attached to the help message. - /// - public DiscordEmbed? Embed { get; } - - /// - /// Creates a new instance of a help message. - /// - /// Contents of the message. - /// Embed to attach to the message. - public CommandHelpMessage(string? content = null, DiscordEmbed? embed = null) - { - this.Content = content; - this.Embed = embed; - } -} +using DSharpPlus.Entities; + +namespace DSharpPlus.CommandsNext.Entities; + +/// +/// Represents a formatted help message. +/// +public readonly struct CommandHelpMessage +{ + /// + /// Gets the contents of the help message. + /// + public string? Content { get; } + + /// + /// Gets the embed attached to the help message. + /// + public DiscordEmbed? Embed { get; } + + /// + /// Creates a new instance of a help message. + /// + /// Contents of the message. + /// Embed to attach to the message. + public CommandHelpMessage(string? content = null, DiscordEmbed? embed = null) + { + this.Content = content; + this.Embed = embed; + } +} diff --git a/DSharpPlus.CommandsNext/Entities/CommandModule.cs b/DSharpPlus.CommandsNext/Entities/CommandModule.cs index 91982d4b8e..077f5bfdcc 100644 --- a/DSharpPlus.CommandsNext/Entities/CommandModule.cs +++ b/DSharpPlus.CommandsNext/Entities/CommandModule.cs @@ -1,79 +1,79 @@ -using System; - -namespace DSharpPlus.CommandsNext.Entities; - -/// -/// Represents a base interface for all types of command modules. -/// -public interface ICommandModule -{ - /// - /// Gets the type of this module. - /// - public Type ModuleType { get; } - - /// - /// Returns an instance of this module. - /// - /// Services to instantiate the module with. - /// A created instance of this module. - public BaseCommandModule GetInstance(IServiceProvider services); -} - -/// -/// Represents a transient command module. This type of module is reinstated on every command call. -/// -public class TransientCommandModule : ICommandModule -{ - /// - /// Gets the type of this module. - /// - public Type ModuleType { get; } - - /// - /// Creates a new transient module. - /// - /// Type of the module to create. - internal TransientCommandModule(Type t) => this.ModuleType = t; - - /// - /// Creates a new instance of this module. - /// - /// Services to instantiate the module with. - /// Created module. - public BaseCommandModule GetInstance(IServiceProvider services) => (BaseCommandModule)this.ModuleType.CreateInstance(services); -} - -/// -/// Represents a singleton command module. This type of module is instantiated only when created. -/// -public class SingletonCommandModule : ICommandModule -{ - /// - /// Gets the type of this module. - /// - public Type ModuleType { get; } - - /// - /// Gets this module's instance. - /// - public BaseCommandModule Instance { get; } - - /// - /// Creates a new singleton module, and instantiates it. - /// - /// Type of the module to create. - /// Services to instantiate the module with. - internal SingletonCommandModule(Type t, IServiceProvider services) - { - this.ModuleType = t; - this.Instance = (BaseCommandModule)t.CreateInstance(services); - } - - /// - /// Returns the instance of this module. - /// - /// Services to instantiate the module with. - /// This module's instance. - public BaseCommandModule GetInstance(IServiceProvider services) => this.Instance; -} +using System; + +namespace DSharpPlus.CommandsNext.Entities; + +/// +/// Represents a base interface for all types of command modules. +/// +public interface ICommandModule +{ + /// + /// Gets the type of this module. + /// + public Type ModuleType { get; } + + /// + /// Returns an instance of this module. + /// + /// Services to instantiate the module with. + /// A created instance of this module. + public BaseCommandModule GetInstance(IServiceProvider services); +} + +/// +/// Represents a transient command module. This type of module is reinstated on every command call. +/// +public class TransientCommandModule : ICommandModule +{ + /// + /// Gets the type of this module. + /// + public Type ModuleType { get; } + + /// + /// Creates a new transient module. + /// + /// Type of the module to create. + internal TransientCommandModule(Type t) => this.ModuleType = t; + + /// + /// Creates a new instance of this module. + /// + /// Services to instantiate the module with. + /// Created module. + public BaseCommandModule GetInstance(IServiceProvider services) => (BaseCommandModule)this.ModuleType.CreateInstance(services); +} + +/// +/// Represents a singleton command module. This type of module is instantiated only when created. +/// +public class SingletonCommandModule : ICommandModule +{ + /// + /// Gets the type of this module. + /// + public Type ModuleType { get; } + + /// + /// Gets this module's instance. + /// + public BaseCommandModule Instance { get; } + + /// + /// Creates a new singleton module, and instantiates it. + /// + /// Type of the module to create. + /// Services to instantiate the module with. + internal SingletonCommandModule(Type t, IServiceProvider services) + { + this.ModuleType = t; + this.Instance = (BaseCommandModule)t.CreateInstance(services); + } + + /// + /// Returns the instance of this module. + /// + /// Services to instantiate the module with. + /// This module's instance. + public BaseCommandModule GetInstance(IServiceProvider services) => this.Instance; +} diff --git a/DSharpPlus.CommandsNext/Entities/CommandOverload.cs b/DSharpPlus.CommandsNext/Entities/CommandOverload.cs index 1f936c6524..3935e78adc 100644 --- a/DSharpPlus.CommandsNext/Entities/CommandOverload.cs +++ b/DSharpPlus.CommandsNext/Entities/CommandOverload.cs @@ -1,29 +1,29 @@ -using System; -using System.Collections.Generic; - -namespace DSharpPlus.CommandsNext; - -/// -/// Represents a specific overload of a command. -/// -public sealed class CommandOverload -{ - /// - /// Gets this command overload's arguments. - /// - public IReadOnlyList Arguments { get; internal set; } = Array.Empty(); - - /// - /// Gets this command overload's priority. - /// - public int Priority { get; internal set; } - - /// - /// Gets this command overload's delegate. - /// - internal Delegate callable { get; set; } = null!; - - internal object? invocationTarget { get; set; } - - internal CommandOverload() { } -} +using System; +using System.Collections.Generic; + +namespace DSharpPlus.CommandsNext; + +/// +/// Represents a specific overload of a command. +/// +public sealed class CommandOverload +{ + /// + /// Gets this command overload's arguments. + /// + public IReadOnlyList Arguments { get; internal set; } = Array.Empty(); + + /// + /// Gets this command overload's priority. + /// + public int Priority { get; internal set; } + + /// + /// Gets this command overload's delegate. + /// + internal Delegate callable { get; set; } = null!; + + internal object? invocationTarget { get; set; } + + internal CommandOverload() { } +} diff --git a/DSharpPlus.CommandsNext/Entities/CommandResult.cs b/DSharpPlus.CommandsNext/Entities/CommandResult.cs index ca2a37cf94..f4db92f534 100644 --- a/DSharpPlus.CommandsNext/Entities/CommandResult.cs +++ b/DSharpPlus.CommandsNext/Entities/CommandResult.cs @@ -1,24 +1,24 @@ -using System; - -namespace DSharpPlus.CommandsNext; - -/// -/// Represents a command's execution result. -/// -public struct CommandResult -{ - /// - /// Gets whether the command execution succeeded. - /// - public bool IsSuccessful { get; internal set; } - - /// - /// Gets the exception (if any) that occurred when executing the command. - /// - public Exception Exception { get; internal set; } - - /// - /// Gets the context in which the command was executed. - /// - public CommandContext Context { get; internal set; } -} +using System; + +namespace DSharpPlus.CommandsNext; + +/// +/// Represents a command's execution result. +/// +public struct CommandResult +{ + /// + /// Gets whether the command execution succeeded. + /// + public bool IsSuccessful { get; internal set; } + + /// + /// Gets the exception (if any) that occurred when executing the command. + /// + public Exception Exception { get; internal set; } + + /// + /// Gets the context in which the command was executed. + /// + public CommandContext Context { get; internal set; } +} diff --git a/DSharpPlus.CommandsNext/EventArgs/CommandContext.cs b/DSharpPlus.CommandsNext/EventArgs/CommandContext.cs index 0e93e76de1..10b68010fb 100644 --- a/DSharpPlus.CommandsNext/EventArgs/CommandContext.cs +++ b/DSharpPlus.CommandsNext/EventArgs/CommandContext.cs @@ -1,154 +1,154 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using DSharpPlus.Entities; -using Microsoft.Extensions.DependencyInjection; - -namespace DSharpPlus.CommandsNext; - -/// -/// Represents a context in which a command is executed. -/// -public sealed class CommandContext -{ - /// - /// Gets the client which received the message. - /// - public DiscordClient Client { get; internal set; } = null!; - - /// - /// Gets the message that triggered the execution. - /// - public DiscordMessage Message { get; internal set; } = null!; - - /// - /// Gets the channel in which the execution was triggered, - /// - public DiscordChannel Channel - => this.Message.Channel; - - /// - /// Gets the guild in which the execution was triggered. This property is null for commands sent over direct messages. - /// - public DiscordGuild Guild - => this.Channel.Guild; - - /// - /// Gets the user who triggered the execution. - /// - public DiscordUser User - => this.Message.Author; - - /// - /// Gets the member who triggered the execution. This property is null for commands sent over direct messages. - /// - public DiscordMember? Member - => this.lazyMember.Value; - - private readonly Lazy lazyMember; - - /// - /// Gets the CommandsNext service instance that handled this command. - /// - public CommandsNextExtension CommandsNext { get; internal set; } = null!; - - /// - /// Gets the service provider for this CNext instance. - /// - public IServiceProvider Services { get; internal set; } = null!; - - /// - /// Gets the command that is being executed. - /// - public Command? Command { get; internal set; } - - /// - /// Gets the overload of the command that is being executed. - /// - public CommandOverload Overload { get; internal set; } = null!; - - /// - /// Gets the list of raw arguments passed to the command. - /// - public IReadOnlyList RawArguments { get; internal set; } = Array.Empty(); - - /// - /// Gets the raw string from which the arguments were extracted. - /// - public string RawArgumentString { get; internal set; } = string.Empty; - - /// - /// Gets the prefix used to invoke the command. - /// - public string Prefix { get; internal set; } = string.Empty; - - internal CommandsNextConfiguration Config { get; set; } = null!; - - internal ServiceContext ServiceScopeContext { get; set; } - - internal CommandContext() => this.lazyMember = new Lazy(() => this.Guild is not null && this.Guild.Members.TryGetValue(this.User.Id, out DiscordMember? member) ? member : this.Guild?.GetMemberAsync(this.User.Id).GetAwaiter().GetResult()); - - /// - /// Quickly respond to the message that triggered the command. - /// - /// Message to respond with. - /// - public Task RespondAsync(string content) - => this.Message.RespondAsync(content); - - /// - /// Quickly respond to the message that triggered the command. - /// - /// Embed to attach. - /// - public Task RespondAsync(DiscordEmbed embed) - => this.Message.RespondAsync(embed); - - /// - /// Quickly respond to the message that triggered the command. - /// - /// Message to respond with. - /// Embed to attach. - /// - public Task RespondAsync(string content, DiscordEmbed embed) - => this.Message.RespondAsync(content, embed); - - /// - /// Quickly respond to the message that triggered the command. - /// - /// The Discord Message builder. - /// - public Task RespondAsync(DiscordMessageBuilder builder) - => this.Message.RespondAsync(builder); - - /// - /// Quickly respond to the message that triggered the command. - /// - /// The Discord Message builder. - /// - public Task RespondAsync(Action action) - => this.Message.RespondAsync(action); - - /// - /// Triggers typing in the channel containing the message that triggered the command. - /// - /// - public Task TriggerTypingAsync() - => this.Channel.TriggerTypingAsync(); - - internal readonly struct ServiceContext : IDisposable - { - public IServiceProvider Provider { get; } - public IServiceScope Scope { get; } - public bool IsInitialized { get; } - - public ServiceContext(IServiceProvider services, IServiceScope scope) - { - this.Provider = services; - this.Scope = scope; - this.IsInitialized = true; - } - - public readonly void Dispose() => this.Scope?.Dispose(); - } -} +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using DSharpPlus.Entities; +using Microsoft.Extensions.DependencyInjection; + +namespace DSharpPlus.CommandsNext; + +/// +/// Represents a context in which a command is executed. +/// +public sealed class CommandContext +{ + /// + /// Gets the client which received the message. + /// + public DiscordClient Client { get; internal set; } = null!; + + /// + /// Gets the message that triggered the execution. + /// + public DiscordMessage Message { get; internal set; } = null!; + + /// + /// Gets the channel in which the execution was triggered, + /// + public DiscordChannel Channel + => this.Message.Channel; + + /// + /// Gets the guild in which the execution was triggered. This property is null for commands sent over direct messages. + /// + public DiscordGuild Guild + => this.Channel.Guild; + + /// + /// Gets the user who triggered the execution. + /// + public DiscordUser User + => this.Message.Author; + + /// + /// Gets the member who triggered the execution. This property is null for commands sent over direct messages. + /// + public DiscordMember? Member + => this.lazyMember.Value; + + private readonly Lazy lazyMember; + + /// + /// Gets the CommandsNext service instance that handled this command. + /// + public CommandsNextExtension CommandsNext { get; internal set; } = null!; + + /// + /// Gets the service provider for this CNext instance. + /// + public IServiceProvider Services { get; internal set; } = null!; + + /// + /// Gets the command that is being executed. + /// + public Command? Command { get; internal set; } + + /// + /// Gets the overload of the command that is being executed. + /// + public CommandOverload Overload { get; internal set; } = null!; + + /// + /// Gets the list of raw arguments passed to the command. + /// + public IReadOnlyList RawArguments { get; internal set; } = Array.Empty(); + + /// + /// Gets the raw string from which the arguments were extracted. + /// + public string RawArgumentString { get; internal set; } = string.Empty; + + /// + /// Gets the prefix used to invoke the command. + /// + public string Prefix { get; internal set; } = string.Empty; + + internal CommandsNextConfiguration Config { get; set; } = null!; + + internal ServiceContext ServiceScopeContext { get; set; } + + internal CommandContext() => this.lazyMember = new Lazy(() => this.Guild is not null && this.Guild.Members.TryGetValue(this.User.Id, out DiscordMember? member) ? member : this.Guild?.GetMemberAsync(this.User.Id).GetAwaiter().GetResult()); + + /// + /// Quickly respond to the message that triggered the command. + /// + /// Message to respond with. + /// + public Task RespondAsync(string content) + => this.Message.RespondAsync(content); + + /// + /// Quickly respond to the message that triggered the command. + /// + /// Embed to attach. + /// + public Task RespondAsync(DiscordEmbed embed) + => this.Message.RespondAsync(embed); + + /// + /// Quickly respond to the message that triggered the command. + /// + /// Message to respond with. + /// Embed to attach. + /// + public Task RespondAsync(string content, DiscordEmbed embed) + => this.Message.RespondAsync(content, embed); + + /// + /// Quickly respond to the message that triggered the command. + /// + /// The Discord Message builder. + /// + public Task RespondAsync(DiscordMessageBuilder builder) + => this.Message.RespondAsync(builder); + + /// + /// Quickly respond to the message that triggered the command. + /// + /// The Discord Message builder. + /// + public Task RespondAsync(Action action) + => this.Message.RespondAsync(action); + + /// + /// Triggers typing in the channel containing the message that triggered the command. + /// + /// + public Task TriggerTypingAsync() + => this.Channel.TriggerTypingAsync(); + + internal readonly struct ServiceContext : IDisposable + { + public IServiceProvider Provider { get; } + public IServiceScope Scope { get; } + public bool IsInitialized { get; } + + public ServiceContext(IServiceProvider services, IServiceScope scope) + { + this.Provider = services; + this.Scope = scope; + this.IsInitialized = true; + } + + public readonly void Dispose() => this.Scope?.Dispose(); + } +} diff --git a/DSharpPlus.CommandsNext/EventArgs/CommandErrorEventArgs.cs b/DSharpPlus.CommandsNext/EventArgs/CommandErrorEventArgs.cs index fa944a09c1..47ada05aff 100644 --- a/DSharpPlus.CommandsNext/EventArgs/CommandErrorEventArgs.cs +++ b/DSharpPlus.CommandsNext/EventArgs/CommandErrorEventArgs.cs @@ -1,11 +1,11 @@ -using System; - -namespace DSharpPlus.CommandsNext; - -/// -/// Represents arguments for event. -/// -public class CommandErrorEventArgs : CommandEventArgs -{ - public Exception Exception { get; internal set; } = null!; -} +using System; + +namespace DSharpPlus.CommandsNext; + +/// +/// Represents arguments for event. +/// +public class CommandErrorEventArgs : CommandEventArgs +{ + public Exception Exception { get; internal set; } = null!; +} diff --git a/DSharpPlus.CommandsNext/EventArgs/CommandEventArgs.cs b/DSharpPlus.CommandsNext/EventArgs/CommandEventArgs.cs index 2cc8496214..ac2ba1d8ee 100644 --- a/DSharpPlus.CommandsNext/EventArgs/CommandEventArgs.cs +++ b/DSharpPlus.CommandsNext/EventArgs/CommandEventArgs.cs @@ -1,20 +1,20 @@ -using DSharpPlus.AsyncEvents; - -namespace DSharpPlus.CommandsNext; - -/// -/// Base class for all CNext-related events. -/// -public class CommandEventArgs : AsyncEventArgs -{ - /// - /// Gets the context in which the command was executed. - /// - public CommandContext Context { get; internal set; } = null!; - - /// - /// Gets the command that was executed. - /// - public Command? Command - => this.Context.Command; -} +using DSharpPlus.AsyncEvents; + +namespace DSharpPlus.CommandsNext; + +/// +/// Base class for all CNext-related events. +/// +public class CommandEventArgs : AsyncEventArgs +{ + /// + /// Gets the context in which the command was executed. + /// + public CommandContext Context { get; internal set; } = null!; + + /// + /// Gets the command that was executed. + /// + public Command? Command + => this.Context.Command; +} diff --git a/DSharpPlus.CommandsNext/EventArgs/CommandExecutionEventArgs.cs b/DSharpPlus.CommandsNext/EventArgs/CommandExecutionEventArgs.cs index c4d99ba78d..c1c37b785b 100644 --- a/DSharpPlus.CommandsNext/EventArgs/CommandExecutionEventArgs.cs +++ b/DSharpPlus.CommandsNext/EventArgs/CommandExecutionEventArgs.cs @@ -1,14 +1,14 @@ -namespace DSharpPlus.CommandsNext; - - -/// -/// Represents arguments for event. -/// -public class CommandExecutionEventArgs : CommandEventArgs -{ - /// - /// Gets the command that was executed. - /// - public new Command Command - => this.Context.Command!; -} +namespace DSharpPlus.CommandsNext; + + +/// +/// Represents arguments for event. +/// +public class CommandExecutionEventArgs : CommandEventArgs +{ + /// + /// Gets the command that was executed. + /// + public new Command Command + => this.Context.Command!; +} diff --git a/DSharpPlus.CommandsNext/Exceptions/ChecksFailedException.cs b/DSharpPlus.CommandsNext/Exceptions/ChecksFailedException.cs index 5e36a00dd6..9c86e6c4d9 100644 --- a/DSharpPlus.CommandsNext/Exceptions/ChecksFailedException.cs +++ b/DSharpPlus.CommandsNext/Exceptions/ChecksFailedException.cs @@ -1,41 +1,41 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using DSharpPlus.CommandsNext.Attributes; - -namespace DSharpPlus.CommandsNext.Exceptions; - -/// -/// Indicates that one or more checks for given command have failed. -/// -public class ChecksFailedException : Exception -{ - /// - /// Gets the command that was executed. - /// - public Command Command { get; } - - /// - /// Gets the context in which given command was executed. - /// - public CommandContext Context { get; } - - /// - /// Gets the checks that failed. - /// - public IReadOnlyList FailedChecks { get; } - - /// - /// Creates a new . - /// - /// Command that failed to execute. - /// Context in which the command was executed. - /// A collection of checks that failed. - public ChecksFailedException(Command command, CommandContext ctx, IEnumerable failedChecks) - : base("One or more pre-execution checks failed.") - { - this.Command = command; - this.Context = ctx; - this.FailedChecks = new ReadOnlyCollection(new List(failedChecks)); - } -} +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using DSharpPlus.CommandsNext.Attributes; + +namespace DSharpPlus.CommandsNext.Exceptions; + +/// +/// Indicates that one or more checks for given command have failed. +/// +public class ChecksFailedException : Exception +{ + /// + /// Gets the command that was executed. + /// + public Command Command { get; } + + /// + /// Gets the context in which given command was executed. + /// + public CommandContext Context { get; } + + /// + /// Gets the checks that failed. + /// + public IReadOnlyList FailedChecks { get; } + + /// + /// Creates a new . + /// + /// Command that failed to execute. + /// Context in which the command was executed. + /// A collection of checks that failed. + public ChecksFailedException(Command command, CommandContext ctx, IEnumerable failedChecks) + : base("One or more pre-execution checks failed.") + { + this.Command = command; + this.Context = ctx; + this.FailedChecks = new ReadOnlyCollection(new List(failedChecks)); + } +} diff --git a/DSharpPlus.CommandsNext/Exceptions/CommandNotFoundException.cs b/DSharpPlus.CommandsNext/Exceptions/CommandNotFoundException.cs index ce4282e77e..99703c6b15 100644 --- a/DSharpPlus.CommandsNext/Exceptions/CommandNotFoundException.cs +++ b/DSharpPlus.CommandsNext/Exceptions/CommandNotFoundException.cs @@ -1,27 +1,27 @@ -using System; - -namespace DSharpPlus.CommandsNext.Exceptions; - -/// -/// Thrown when the command service fails to find a command. -/// -public sealed class CommandNotFoundException : Exception -{ - /// - /// Gets the name of the command that was not found. - /// - public string CommandName { get; set; } - - /// - /// Creates a new . - /// - /// Name of the command that was not found. - public CommandNotFoundException(string command) - : base("Specified command was not found.") => this.CommandName = command; - - /// - /// Returns a string representation of this . - /// - /// A string representation. - public override string ToString() => $"{GetType()}: {this.Message}\nCommand name: {this.CommandName}"; // much like System.ArgumentNullException works -} +using System; + +namespace DSharpPlus.CommandsNext.Exceptions; + +/// +/// Thrown when the command service fails to find a command. +/// +public sealed class CommandNotFoundException : Exception +{ + /// + /// Gets the name of the command that was not found. + /// + public string CommandName { get; set; } + + /// + /// Creates a new . + /// + /// Name of the command that was not found. + public CommandNotFoundException(string command) + : base("Specified command was not found.") => this.CommandName = command; + + /// + /// Returns a string representation of this . + /// + /// A string representation. + public override string ToString() => $"{GetType()}: {this.Message}\nCommand name: {this.CommandName}"; // much like System.ArgumentNullException works +} diff --git a/DSharpPlus.CommandsNext/Exceptions/DuplicateCommandException.cs b/DSharpPlus.CommandsNext/Exceptions/DuplicateCommandException.cs index 9fdb77b8b4..03531b231e 100644 --- a/DSharpPlus.CommandsNext/Exceptions/DuplicateCommandException.cs +++ b/DSharpPlus.CommandsNext/Exceptions/DuplicateCommandException.cs @@ -1,27 +1,27 @@ -using System; - -namespace DSharpPlus.CommandsNext.Exceptions; - -/// -/// Indicates that given command name or alias is taken. -/// -public class DuplicateCommandException : Exception -{ - /// - /// Gets the name of the command that already exists. - /// - public string CommandName { get; } - - /// - /// Creates a new exception indicating that given command name is already taken. - /// - /// Name of the command that was taken. - internal DuplicateCommandException(string name) - : base($"A command or alias with the name '{name}' has already been registered.") => this.CommandName = name; - - /// - /// Returns a string representation of this . - /// - /// A string representation. - public override string ToString() => $"{GetType()}: {this.Message}\nCommand name: {this.CommandName}"; // much like System.ArgumentException works -} +using System; + +namespace DSharpPlus.CommandsNext.Exceptions; + +/// +/// Indicates that given command name or alias is taken. +/// +public class DuplicateCommandException : Exception +{ + /// + /// Gets the name of the command that already exists. + /// + public string CommandName { get; } + + /// + /// Creates a new exception indicating that given command name is already taken. + /// + /// Name of the command that was taken. + internal DuplicateCommandException(string name) + : base($"A command or alias with the name '{name}' has already been registered.") => this.CommandName = name; + + /// + /// Returns a string representation of this . + /// + /// A string representation. + public override string ToString() => $"{GetType()}: {this.Message}\nCommand name: {this.CommandName}"; // much like System.ArgumentException works +} diff --git a/DSharpPlus.CommandsNext/Exceptions/DuplicateOverloadException.cs b/DSharpPlus.CommandsNext/Exceptions/DuplicateOverloadException.cs index 83088107af..1e12f76634 100644 --- a/DSharpPlus.CommandsNext/Exceptions/DuplicateOverloadException.cs +++ b/DSharpPlus.CommandsNext/Exceptions/DuplicateOverloadException.cs @@ -1,43 +1,43 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; - -namespace DSharpPlus.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; } - - private string ArgumentSetKey { get; } - - /// - /// 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; - } - - /// - /// Returns a string representation of this . - /// - /// A string representation. - public override string ToString() => $"{GetType()}: {this.Message}\nCommand name: {this.CommandName}\nArgument types: {this.ArgumentSetKey}"; // much like System.ArgumentException works -} +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace DSharpPlus.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; } + + private string ArgumentSetKey { get; } + + /// + /// 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; + } + + /// + /// Returns a string representation of this . + /// + /// A string representation. + public override string ToString() => $"{GetType()}: {this.Message}\nCommand name: {this.CommandName}\nArgument types: {this.ArgumentSetKey}"; // much like System.ArgumentException works +} diff --git a/DSharpPlus.CommandsNext/Exceptions/InvalidOverloadException.cs b/DSharpPlus.CommandsNext/Exceptions/InvalidOverloadException.cs index a97fc6be02..08b82cbef7 100644 --- a/DSharpPlus.CommandsNext/Exceptions/InvalidOverloadException.cs +++ b/DSharpPlus.CommandsNext/Exceptions/InvalidOverloadException.cs @@ -1,52 +1,52 @@ -using System; -using System.Reflection; - -namespace DSharpPlus.CommandsNext.Exceptions; - -/// -/// Thrown when the command service fails to build a command due to a problem with its overload. -/// -public sealed class InvalidOverloadException : Exception -{ - /// - /// Gets the method that caused this exception. - /// - public MethodInfo Method { get; } - - /// - /// Gets or sets the argument that caused the problem. This can be null. - /// - public ParameterInfo? Parameter { get; } - - /// - /// Creates a new . - /// - /// Exception message. - /// Method that caused the problem. - /// Method argument that caused the problem. - public InvalidOverloadException(string message, MethodInfo method, ParameterInfo? parameter) - : base(message) - { - this.Method = method; - this.Parameter = parameter; - } - - /// - /// Creates a new . - /// - /// Exception message. - /// Method that caused the problem. - public InvalidOverloadException(string message, MethodInfo method) - : this(message, method, null) - { } - - /// - /// Returns a string representation of this . - /// - /// A string representation. - public override string ToString() => - // much like System.ArgumentNullException works - this.Parameter == null - ? $"{GetType()}: {this.Message}\nMethod: {this.Method} (declared in {this.Method.DeclaringType})" - : $"{GetType()}: {this.Message}\nMethod: {this.Method} (declared in {this.Method.DeclaringType})\nArgument: {this.Parameter.ParameterType} {this.Parameter.Name}"; -} +using System; +using System.Reflection; + +namespace DSharpPlus.CommandsNext.Exceptions; + +/// +/// Thrown when the command service fails to build a command due to a problem with its overload. +/// +public sealed class InvalidOverloadException : Exception +{ + /// + /// Gets the method that caused this exception. + /// + public MethodInfo Method { get; } + + /// + /// Gets or sets the argument that caused the problem. This can be null. + /// + public ParameterInfo? Parameter { get; } + + /// + /// Creates a new . + /// + /// Exception message. + /// Method that caused the problem. + /// Method argument that caused the problem. + public InvalidOverloadException(string message, MethodInfo method, ParameterInfo? parameter) + : base(message) + { + this.Method = method; + this.Parameter = parameter; + } + + /// + /// Creates a new . + /// + /// Exception message. + /// Method that caused the problem. + public InvalidOverloadException(string message, MethodInfo method) + : this(message, method, null) + { } + + /// + /// Returns a string representation of this . + /// + /// A string representation. + public override string ToString() => + // much like System.ArgumentNullException works + this.Parameter == null + ? $"{GetType()}: {this.Message}\nMethod: {this.Method} (declared in {this.Method.DeclaringType})" + : $"{GetType()}: {this.Message}\nMethod: {this.Method} (declared in {this.Method.DeclaringType})\nArgument: {this.Parameter.ParameterType} {this.Parameter.Name}"; +} diff --git a/DSharpPlus.CommandsNext/Executors/AsynchronousCommandExecutor.cs b/DSharpPlus.CommandsNext/Executors/AsynchronousCommandExecutor.cs index 88564ae864..31dba16b96 100644 --- a/DSharpPlus.CommandsNext/Executors/AsynchronousCommandExecutor.cs +++ b/DSharpPlus.CommandsNext/Executors/AsynchronousCommandExecutor.cs @@ -1,19 +1,19 @@ -using System; -using System.Threading.Tasks; - -namespace DSharpPlus.CommandsNext.Executors; - -/// -/// Executes commands using . -/// -public sealed class AsynchronousCommandExecutor : ICommandExecutor -{ - Task ICommandExecutor.ExecuteAsync(CommandContext ctx) - { - _ = ctx.CommandsNext.ExecuteCommandAsync(ctx); - return Task.CompletedTask; - } - - void IDisposable.Dispose() - { } -} +using System; +using System.Threading.Tasks; + +namespace DSharpPlus.CommandsNext.Executors; + +/// +/// Executes commands using . +/// +public sealed class AsynchronousCommandExecutor : ICommandExecutor +{ + Task ICommandExecutor.ExecuteAsync(CommandContext ctx) + { + _ = ctx.CommandsNext.ExecuteCommandAsync(ctx); + return Task.CompletedTask; + } + + void IDisposable.Dispose() + { } +} diff --git a/DSharpPlus.CommandsNext/Executors/ICommandExecutor.cs b/DSharpPlus.CommandsNext/Executors/ICommandExecutor.cs index b502399cfe..d4187175bd 100644 --- a/DSharpPlus.CommandsNext/Executors/ICommandExecutor.cs +++ b/DSharpPlus.CommandsNext/Executors/ICommandExecutor.cs @@ -1,17 +1,17 @@ -using System; -using System.Threading.Tasks; - -namespace DSharpPlus.CommandsNext.Executors; - -/// -/// Defines an API surface for all command executors. -/// -public interface ICommandExecutor : IDisposable -{ - /// - /// Executes a command from given context. - /// - /// Context to execute in. - /// Task encapsulating the async operation. - public Task ExecuteAsync(CommandContext ctx); -} +using System; +using System.Threading.Tasks; + +namespace DSharpPlus.CommandsNext.Executors; + +/// +/// Defines an API surface for all command executors. +/// +public interface ICommandExecutor : IDisposable +{ + /// + /// Executes a command from given context. + /// + /// Context to execute in. + /// Task encapsulating the async operation. + public Task ExecuteAsync(CommandContext ctx); +} diff --git a/DSharpPlus.CommandsNext/Executors/ParallelQueuedCommandExecutor.cs b/DSharpPlus.CommandsNext/Executors/ParallelQueuedCommandExecutor.cs index f24b22ab40..52bcb643bd 100644 --- a/DSharpPlus.CommandsNext/Executors/ParallelQueuedCommandExecutor.cs +++ b/DSharpPlus.CommandsNext/Executors/ParallelQueuedCommandExecutor.cs @@ -1,80 +1,80 @@ -using System; -using System.Threading; -using System.Threading.Channels; -using System.Threading.Tasks; - -namespace DSharpPlus.CommandsNext.Executors; - -/// -/// A command executor which uses a bounded pool of executors to execute commands. This can limit the impact of -/// commands on system resources, such as CPU usage. -/// -public sealed class ParallelQueuedCommandExecutor : ICommandExecutor -{ - /// - /// Gets the degree of parallelism of this executor. - /// - public int Parallelism { get; } - - private readonly CancellationTokenSource cts; - private readonly CancellationToken ct; - private readonly Channel queue; - private readonly ChannelWriter queueWriter; - private readonly ChannelReader queueReader; - private readonly Task[] tasks; - - /// - /// Creates a new executor, which uses up to 75% of system CPU resources. - /// - public ParallelQueuedCommandExecutor() - : this((Environment.ProcessorCount + 1) * 3 / 4) - { } - - /// - /// Creates a new executor with specified degree of parallelism. - /// - /// The number of workers to use. It is recommended this number does not exceed 150% of the physical CPU count. - public ParallelQueuedCommandExecutor(int parallelism) - { - this.Parallelism = parallelism; - - this.cts = new(); - this.ct = this.cts.Token; - this.queue = Channel.CreateUnbounded(); - this.queueReader = this.queue.Reader; - this.queueWriter = this.queue.Writer; - - this.tasks = new Task[parallelism]; - for (int i = 0; i < parallelism; i++) - { - this.tasks[i] = Task.Run(ExecuteAsync); - } - } - - /// - /// Disposes of the resources used by this executor. - /// - public void Dispose() - { - this.queueWriter.Complete(); - this.cts.Cancel(); - this.cts.Dispose(); - } - - async Task ICommandExecutor.ExecuteAsync(CommandContext ctx) - => await this.queueWriter.WriteAsync(ctx, this.ct); - - private async Task ExecuteAsync() - { - while (!this.ct.IsCancellationRequested) - { - CommandContext? ctx = await this.queueReader.ReadAsync(this.ct); - if (ctx is null) - { - continue; - } - - await ctx.CommandsNext.ExecuteCommandAsync(ctx); - } - } -} +using System; +using System.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; + +namespace DSharpPlus.CommandsNext.Executors; + +/// +/// A command executor which uses a bounded pool of executors to execute commands. This can limit the impact of +/// commands on system resources, such as CPU usage. +/// +public sealed class ParallelQueuedCommandExecutor : ICommandExecutor +{ + /// + /// Gets the degree of parallelism of this executor. + /// + public int Parallelism { get; } + + private readonly CancellationTokenSource cts; + private readonly CancellationToken ct; + private readonly Channel queue; + private readonly ChannelWriter queueWriter; + private readonly ChannelReader queueReader; + private readonly Task[] tasks; + + /// + /// Creates a new executor, which uses up to 75% of system CPU resources. + /// + public ParallelQueuedCommandExecutor() + : this((Environment.ProcessorCount + 1) * 3 / 4) + { } + + /// + /// Creates a new executor with specified degree of parallelism. + /// + /// The number of workers to use. It is recommended this number does not exceed 150% of the physical CPU count. + public ParallelQueuedCommandExecutor(int parallelism) + { + this.Parallelism = parallelism; + + this.cts = new(); + this.ct = this.cts.Token; + this.queue = Channel.CreateUnbounded(); + this.queueReader = this.queue.Reader; + this.queueWriter = this.queue.Writer; + + this.tasks = new Task[parallelism]; + for (int i = 0; i < parallelism; i++) + { + this.tasks[i] = Task.Run(ExecuteAsync); + } + } + + /// + /// Disposes of the resources used by this executor. + /// + public void Dispose() + { + this.queueWriter.Complete(); + this.cts.Cancel(); + this.cts.Dispose(); + } + + async Task ICommandExecutor.ExecuteAsync(CommandContext ctx) + => await this.queueWriter.WriteAsync(ctx, this.ct); + + private async Task ExecuteAsync() + { + while (!this.ct.IsCancellationRequested) + { + CommandContext? ctx = await this.queueReader.ReadAsync(this.ct); + if (ctx is null) + { + continue; + } + + await ctx.CommandsNext.ExecuteCommandAsync(ctx); + } + } +} diff --git a/DSharpPlus.CommandsNext/Executors/SynchronousCommandExecutor.cs b/DSharpPlus.CommandsNext/Executors/SynchronousCommandExecutor.cs index e8a4d4f1d7..d36bc23b48 100644 --- a/DSharpPlus.CommandsNext/Executors/SynchronousCommandExecutor.cs +++ b/DSharpPlus.CommandsNext/Executors/SynchronousCommandExecutor.cs @@ -1,16 +1,16 @@ -using System; -using System.Threading.Tasks; - -namespace DSharpPlus.CommandsNext.Executors; - -/// -/// Executes commands by awaiting them. -/// -public sealed class SynchronousCommandExecutor : ICommandExecutor -{ - Task ICommandExecutor.ExecuteAsync(CommandContext ctx) - => ctx.CommandsNext.ExecuteCommandAsync(ctx); - - void IDisposable.Dispose() - { } -} +using System; +using System.Threading.Tasks; + +namespace DSharpPlus.CommandsNext.Executors; + +/// +/// Executes commands by awaiting them. +/// +public sealed class SynchronousCommandExecutor : ICommandExecutor +{ + Task ICommandExecutor.ExecuteAsync(CommandContext ctx) + => ctx.CommandsNext.ExecuteCommandAsync(ctx); + + void IDisposable.Dispose() + { } +} diff --git a/DSharpPlus.CommandsNext/ExtensionMethods.cs b/DSharpPlus.CommandsNext/ExtensionMethods.cs index d4ccbf2136..c59cf37c9b 100644 --- a/DSharpPlus.CommandsNext/ExtensionMethods.cs +++ b/DSharpPlus.CommandsNext/ExtensionMethods.cs @@ -1,61 +1,61 @@ -using System; - -using DSharpPlus.Extensions; - -using Microsoft.Extensions.DependencyInjection; - -namespace DSharpPlus.CommandsNext; - -/// -/// Defines various extensions specific to CommandsNext. -/// -public static class ExtensionMethods -{ - /// - /// Adds the CommandsNext extension to this DiscordClientBuilder. - /// - /// The builder to register to. - /// Any setup code you want to run on the extension, such as registering commands and converters. - /// CommandsNext configuration to use. - /// The same builder for chaining. - public static DiscordClientBuilder UseCommandsNext - ( - this DiscordClientBuilder builder, - Action setup, - CommandsNextConfiguration configuration - ) - => builder.ConfigureServices(services => services.AddCommandsNextExtension(setup, configuration)); - - /// - /// Adds the CommandsNext extension to this service collection. - /// - /// The service collection to register to. - /// Any setup code you want to run on the extension, such as registering commands and converters. - /// CommandsNext configuration to use. - /// The same service collection for chaining. - public static IServiceCollection AddCommandsNextExtension - ( - this IServiceCollection services, - Action setup, - CommandsNextConfiguration configuration - ) - { - if (configuration.UseDefaultCommandHandler) - { - services.ConfigureEventHandlers(b => b.AddEventHandlers()); - } - - services.AddSingleton(provider => - { - DiscordClient client = provider.GetRequiredService(); - - CommandsNextExtension extension = new(configuration ?? new()); - extension.Setup(client); - setup(extension); - - return extension; - }); - - return services; - } -} +using System; + +using DSharpPlus.Extensions; + +using Microsoft.Extensions.DependencyInjection; + +namespace DSharpPlus.CommandsNext; + +/// +/// Defines various extensions specific to CommandsNext. +/// +public static class ExtensionMethods +{ + /// + /// Adds the CommandsNext extension to this DiscordClientBuilder. + /// + /// The builder to register to. + /// Any setup code you want to run on the extension, such as registering commands and converters. + /// CommandsNext configuration to use. + /// The same builder for chaining. + public static DiscordClientBuilder UseCommandsNext + ( + this DiscordClientBuilder builder, + Action setup, + CommandsNextConfiguration configuration + ) + => builder.ConfigureServices(services => services.AddCommandsNextExtension(setup, configuration)); + + /// + /// Adds the CommandsNext extension to this service collection. + /// + /// The service collection to register to. + /// Any setup code you want to run on the extension, such as registering commands and converters. + /// CommandsNext configuration to use. + /// The same service collection for chaining. + public static IServiceCollection AddCommandsNextExtension + ( + this IServiceCollection services, + Action setup, + CommandsNextConfiguration configuration + ) + { + if (configuration.UseDefaultCommandHandler) + { + services.ConfigureEventHandlers(b => b.AddEventHandlers()); + } + + services.AddSingleton(provider => + { + DiscordClient client = provider.GetRequiredService(); + + CommandsNextExtension extension = new(configuration ?? new()); + extension.Setup(client); + setup(extension); + + return extension; + }); + + return services; + } +} diff --git a/DSharpPlus.Interactivity/Enums/ButtonDisableBehavior.cs b/DSharpPlus.Interactivity/Enums/ButtonDisableBehavior.cs index e6869f024b..ce8d937c54 100644 --- a/DSharpPlus.Interactivity/Enums/ButtonDisableBehavior.cs +++ b/DSharpPlus.Interactivity/Enums/ButtonDisableBehavior.cs @@ -1,31 +1,31 @@ -// This file is part of the DSharpPlus project. -// -// Copyright (c) 2015 Mike Santiago -// Copyright (c) 2016-2025 DSharpPlus Contributors -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -namespace DSharpPlus.Interactivity.Enums; - - -public enum ButtonDisableBehavior -{ - Disable = 0, - Remove = 1 -} +// This file is part of the DSharpPlus project. +// +// Copyright (c) 2015 Mike Santiago +// Copyright (c) 2016-2025 DSharpPlus Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +namespace DSharpPlus.Interactivity.Enums; + + +public enum ButtonDisableBehavior +{ + Disable = 0, + Remove = 1 +} diff --git a/DSharpPlus.Interactivity/Enums/ButtonPaginationBehavior.cs b/DSharpPlus.Interactivity/Enums/ButtonPaginationBehavior.cs index aeb6c496ae..982fe92c65 100644 --- a/DSharpPlus.Interactivity/Enums/ButtonPaginationBehavior.cs +++ b/DSharpPlus.Interactivity/Enums/ButtonPaginationBehavior.cs @@ -1,25 +1,25 @@ -namespace DSharpPlus.Interactivity.Enums; - - -/// -/// Represents options of how to handle pagination timing out. -/// -public enum ButtonPaginationBehavior -{ - /// - /// The buttons should be disabled when pagination times out. - /// - Disable, - /// - /// The buttons should be left as is when pagination times out. - /// - Ignore, - /// - /// The entire message should be deleted when pagination times out. - /// - DeleteMessage, - /// - /// The buttons should be removed entirely when pagination times out. - /// - DeleteButtons, -} +namespace DSharpPlus.Interactivity.Enums; + + +/// +/// Represents options of how to handle pagination timing out. +/// +public enum ButtonPaginationBehavior +{ + /// + /// The buttons should be disabled when pagination times out. + /// + Disable, + /// + /// The buttons should be left as is when pagination times out. + /// + Ignore, + /// + /// The entire message should be deleted when pagination times out. + /// + DeleteMessage, + /// + /// The buttons should be removed entirely when pagination times out. + /// + DeleteButtons, +} diff --git a/DSharpPlus.Interactivity/Enums/InteractionResponseBehavior.cs b/DSharpPlus.Interactivity/Enums/InteractionResponseBehavior.cs index 1c5e63c915..6f78525e42 100644 --- a/DSharpPlus.Interactivity/Enums/InteractionResponseBehavior.cs +++ b/DSharpPlus.Interactivity/Enums/InteractionResponseBehavior.cs @@ -1,18 +1,18 @@ -namespace DSharpPlus.Interactivity.Enums; - - -public enum InteractionResponseBehavior -{ - /// - /// Indicates that invalid input should be ignored when waiting for interactions. This will cause the interaction to fail. - /// - Ignore, - /// - /// Indicates that invalid input should be ACK'd. The interaction will succeed, but nothing will happen. - /// - Ack, - /// - /// Indicates that invalid input should warrant an ephemeral error message. - /// - Respond -} +namespace DSharpPlus.Interactivity.Enums; + + +public enum InteractionResponseBehavior +{ + /// + /// Indicates that invalid input should be ignored when waiting for interactions. This will cause the interaction to fail. + /// + Ignore, + /// + /// Indicates that invalid input should be ACK'd. The interaction will succeed, but nothing will happen. + /// + Ack, + /// + /// Indicates that invalid input should warrant an ephemeral error message. + /// + Respond +} diff --git a/DSharpPlus.Interactivity/Enums/PaginationBehaviour.cs b/DSharpPlus.Interactivity/Enums/PaginationBehaviour.cs index 1b4e5ee843..447bbb0cef 100644 --- a/DSharpPlus.Interactivity/Enums/PaginationBehaviour.cs +++ b/DSharpPlus.Interactivity/Enums/PaginationBehaviour.cs @@ -1,19 +1,19 @@ -namespace DSharpPlus.Interactivity.Enums; - - -/// -/// Specifies how pagination will handle advancing past the first and last pages. -/// -public enum PaginationBehaviour -{ - /// - /// Going forward beyond the last page will loop back to the first page. - /// Likewise, going back from the first page will loop around to the last page. - /// - WrapAround = 0, - - /// - /// Attempting to go beyond the first or last page will be ignored. - /// - Ignore = 1 -} +namespace DSharpPlus.Interactivity.Enums; + + +/// +/// Specifies how pagination will handle advancing past the first and last pages. +/// +public enum PaginationBehaviour +{ + /// + /// Going forward beyond the last page will loop back to the first page. + /// Likewise, going back from the first page will loop around to the last page. + /// + WrapAround = 0, + + /// + /// Attempting to go beyond the first or last page will be ignored. + /// + Ignore = 1 +} diff --git a/DSharpPlus.Interactivity/Enums/PaginationButtonType.cs b/DSharpPlus.Interactivity/Enums/PaginationButtonType.cs index 3d72c0cc1a..7e54deaa55 100644 --- a/DSharpPlus.Interactivity/Enums/PaginationButtonType.cs +++ b/DSharpPlus.Interactivity/Enums/PaginationButtonType.cs @@ -1,34 +1,34 @@ -// This file is part of the DSharpPlus project. -// -// Copyright (c) 2015 Mike Santiago -// Copyright (c) 2016-2025 DSharpPlus Contributors -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -namespace DSharpPlus.Interactivity.Enums; - - -public enum PaginationButtonType -{ - SkipLeft = 0, - Left = 1, - Stop = 2, - Right = 3, - SkipRight = 4, -} +// This file is part of the DSharpPlus project. +// +// Copyright (c) 2015 Mike Santiago +// Copyright (c) 2016-2025 DSharpPlus Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +namespace DSharpPlus.Interactivity.Enums; + + +public enum PaginationButtonType +{ + SkipLeft = 0, + Left = 1, + Stop = 2, + Right = 3, + SkipRight = 4, +} diff --git a/DSharpPlus.Interactivity/Enums/PaginationDeletion.cs b/DSharpPlus.Interactivity/Enums/PaginationDeletion.cs index 6c6b690c34..d4c556dbce 100644 --- a/DSharpPlus.Interactivity/Enums/PaginationDeletion.cs +++ b/DSharpPlus.Interactivity/Enums/PaginationDeletion.cs @@ -1,23 +1,23 @@ -namespace DSharpPlus.Interactivity.Enums; - - -/// -/// Specifies what should be done once pagination times out. -/// -public enum PaginationDeletion -{ - /// - /// Reaction emojis will be deleted on timeout. - /// - DeleteEmojis = 0, - - /// - /// Reaction emojis will not be deleted on timeout. - /// - KeepEmojis = 1, - - /// - /// The message will be completely deleted on timeout. - /// - DeleteMessage = 2 -} +namespace DSharpPlus.Interactivity.Enums; + + +/// +/// Specifies what should be done once pagination times out. +/// +public enum PaginationDeletion +{ + /// + /// Reaction emojis will be deleted on timeout. + /// + DeleteEmojis = 0, + + /// + /// Reaction emojis will not be deleted on timeout. + /// + KeepEmojis = 1, + + /// + /// The message will be completely deleted on timeout. + /// + DeleteMessage = 2 +} diff --git a/DSharpPlus.Interactivity/Enums/PollBehaviour.cs b/DSharpPlus.Interactivity/Enums/PollBehaviour.cs index e39a746678..8d69ce2f84 100644 --- a/DSharpPlus.Interactivity/Enums/PollBehaviour.cs +++ b/DSharpPlus.Interactivity/Enums/PollBehaviour.cs @@ -1,18 +1,18 @@ -namespace DSharpPlus.Interactivity.Enums; - - -/// -/// Specifies what should be done when a poll times out. -/// -public enum PollBehaviour -{ - /// - /// Reaction emojis will not be deleted. - /// - KeepEmojis = 0, - - /// - /// Reaction emojis will be deleted. - /// - DeleteEmojis = 1 -} +namespace DSharpPlus.Interactivity.Enums; + + +/// +/// Specifies what should be done when a poll times out. +/// +public enum PollBehaviour +{ + /// + /// Reaction emojis will not be deleted. + /// + KeepEmojis = 0, + + /// + /// Reaction emojis will be deleted. + /// + DeleteEmojis = 1 +} diff --git a/DSharpPlus.Interactivity/Enums/SplitType.cs b/DSharpPlus.Interactivity/Enums/SplitType.cs index eb3ac9f604..40cabbab17 100644 --- a/DSharpPlus.Interactivity/Enums/SplitType.cs +++ b/DSharpPlus.Interactivity/Enums/SplitType.cs @@ -1,18 +1,18 @@ -namespace DSharpPlus.Interactivity.Enums; - - -/// -/// Specifies how to split a string. -/// -public enum SplitType -{ - /// - /// Splits string per 500 characters. - /// - Character, - - /// - /// Splits string per 15 lines. - /// - Line -} +namespace DSharpPlus.Interactivity.Enums; + + +/// +/// Specifies how to split a string. +/// +public enum SplitType +{ + /// + /// Splits string per 500 characters. + /// + Character, + + /// + /// Splits string per 15 lines. + /// + Line +} diff --git a/DSharpPlus.Interactivity/EventHandling/ComponentBased/ComponentEventWaiter.cs b/DSharpPlus.Interactivity/EventHandling/ComponentBased/ComponentEventWaiter.cs index dfd526e834..4d7f03ead0 100644 --- a/DSharpPlus.Interactivity/EventHandling/ComponentBased/ComponentEventWaiter.cs +++ b/DSharpPlus.Interactivity/EventHandling/ComponentBased/ComponentEventWaiter.cs @@ -1,160 +1,160 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using ConcurrentCollections; -using DSharpPlus.Entities; -using DSharpPlus.EventArgs; -using DSharpPlus.Interactivity.Enums; -using Microsoft.Extensions.Logging; - -namespace DSharpPlus.Interactivity.EventHandling; - -/// -/// A component-based version of -/// -internal class ComponentEventWaiter : IDisposable -{ - private readonly DiscordClient client; - private readonly ConcurrentHashSet matchRequests = []; - private readonly ConcurrentHashSet collectRequests = []; - - private readonly InteractivityConfiguration config; - - public ComponentEventWaiter(DiscordClient client, InteractivityConfiguration config) - { - this.client = client; - this.config = config; - } - - /// - /// Waits for a specified 's predicate to be fulfilled. - /// - /// The request to wait for. - /// The returned args, or null if it timed out. - public async Task WaitForMatchAsync(ComponentMatchRequest request) - { - this.matchRequests.Add(request); - - try - { - return await request.Tcs.Task; - } - catch (Exception e) - { - this.client.Logger.LogError(InteractivityEvents.InteractivityWaitError, e, "An exception was thrown while waiting for components."); - return null; - } - finally - { - this.matchRequests.TryRemove(request); - } - } - - /// - /// Collects reactions and returns the result when the 's cancellation token is canceled. - /// - /// The request to wait on. - /// The result from request's predicate over the period of time leading up to the token's cancellation. - public async Task> CollectMatchesAsync(ComponentCollectRequest request) - { - this.collectRequests.Add(request); - try - { - await request.Tcs.Task; - } - catch (Exception e) - { - this.client.Logger.LogError(InteractivityEvents.InteractivityCollectorError, e, "There was an error while collecting component event args."); - } - finally - { - this.collectRequests.TryRemove(request); - } - - return request.Collected.ToArray(); - } - - internal async Task HandleAsync(DiscordClient _, ComponentInteractionCreatedEventArgs args) - { - foreach (ComponentMatchRequest? mreq in this.matchRequests.ToArray()) - { - if (mreq.Message == args.Message && mreq.IsMatch(args)) - { - mreq.Tcs.TrySetResult(args); - } - else if (this.config.ResponseBehavior is InteractionResponseBehavior.Respond) - { - try - { - string responseMessage = this.config.ResponseMessage ?? this.config.ResponseMessageFactory(args, this.client.ServiceProvider); - - if (args.Interaction.ResponseState is DiscordInteractionResponseState.Unacknowledged) - { - await args.Interaction.CreateResponseAsync - ( - DiscordInteractionResponseType.ChannelMessageWithSource, - new() { Content = responseMessage, IsEphemeral = true } - ); - } - else - { - await args.Interaction.CreateFollowupMessageAsync - ( - new() { Content = responseMessage, IsEphemeral = true } - ); - } - } - catch (Exception e) - { - this.client.Logger.LogWarning(e, "An exception was thrown during an interactivity response."); - } - } - } - - foreach (ComponentCollectRequest? creq in this.collectRequests.ToArray()) - { - if (creq.Message == args.Message && creq.IsMatch(args)) - { - await args.Interaction.CreateResponseAsync(DiscordInteractionResponseType.DeferredMessageUpdate); - - if (creq.IsMatch(args)) - { - creq.Collected.Add(args); - } - else if (this.config.ResponseBehavior is InteractionResponseBehavior.Respond) - { - try - { - string responseMessage = this.config.ResponseMessage ?? this.config.ResponseMessageFactory(args, this.client.ServiceProvider); - - if (args.Interaction.ResponseState is DiscordInteractionResponseState.Unacknowledged) - { - await args.Interaction.CreateResponseAsync - ( - DiscordInteractionResponseType.ChannelMessageWithSource, - new() { Content = responseMessage, IsEphemeral = true } - ); - } - else - { - await args.Interaction.CreateFollowupMessageAsync - ( - new() { Content = responseMessage, IsEphemeral = true } - ); - } - } - catch (Exception e) - { - this.client.Logger.LogWarning(e, "An exception was thrown during an interactivity response."); - } - } - } - } - } - public void Dispose() - { - this.matchRequests.Clear(); - this.collectRequests.Clear(); - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using ConcurrentCollections; +using DSharpPlus.Entities; +using DSharpPlus.EventArgs; +using DSharpPlus.Interactivity.Enums; +using Microsoft.Extensions.Logging; + +namespace DSharpPlus.Interactivity.EventHandling; + +/// +/// A component-based version of +/// +internal class ComponentEventWaiter : IDisposable +{ + private readonly DiscordClient client; + private readonly ConcurrentHashSet matchRequests = []; + private readonly ConcurrentHashSet collectRequests = []; + + private readonly InteractivityConfiguration config; + + public ComponentEventWaiter(DiscordClient client, InteractivityConfiguration config) + { + this.client = client; + this.config = config; + } + + /// + /// Waits for a specified 's predicate to be fulfilled. + /// + /// The request to wait for. + /// The returned args, or null if it timed out. + public async Task WaitForMatchAsync(ComponentMatchRequest request) + { + this.matchRequests.Add(request); + + try + { + return await request.Tcs.Task; + } + catch (Exception e) + { + this.client.Logger.LogError(InteractivityEvents.InteractivityWaitError, e, "An exception was thrown while waiting for components."); + return null; + } + finally + { + this.matchRequests.TryRemove(request); + } + } + + /// + /// Collects reactions and returns the result when the 's cancellation token is canceled. + /// + /// The request to wait on. + /// The result from request's predicate over the period of time leading up to the token's cancellation. + public async Task> CollectMatchesAsync(ComponentCollectRequest request) + { + this.collectRequests.Add(request); + try + { + await request.Tcs.Task; + } + catch (Exception e) + { + this.client.Logger.LogError(InteractivityEvents.InteractivityCollectorError, e, "There was an error while collecting component event args."); + } + finally + { + this.collectRequests.TryRemove(request); + } + + return request.Collected.ToArray(); + } + + internal async Task HandleAsync(DiscordClient _, ComponentInteractionCreatedEventArgs args) + { + foreach (ComponentMatchRequest? mreq in this.matchRequests.ToArray()) + { + if (mreq.Message == args.Message && mreq.IsMatch(args)) + { + mreq.Tcs.TrySetResult(args); + } + else if (this.config.ResponseBehavior is InteractionResponseBehavior.Respond) + { + try + { + string responseMessage = this.config.ResponseMessage ?? this.config.ResponseMessageFactory(args, this.client.ServiceProvider); + + if (args.Interaction.ResponseState is DiscordInteractionResponseState.Unacknowledged) + { + await args.Interaction.CreateResponseAsync + ( + DiscordInteractionResponseType.ChannelMessageWithSource, + new() { Content = responseMessage, IsEphemeral = true } + ); + } + else + { + await args.Interaction.CreateFollowupMessageAsync + ( + new() { Content = responseMessage, IsEphemeral = true } + ); + } + } + catch (Exception e) + { + this.client.Logger.LogWarning(e, "An exception was thrown during an interactivity response."); + } + } + } + + foreach (ComponentCollectRequest? creq in this.collectRequests.ToArray()) + { + if (creq.Message == args.Message && creq.IsMatch(args)) + { + await args.Interaction.CreateResponseAsync(DiscordInteractionResponseType.DeferredMessageUpdate); + + if (creq.IsMatch(args)) + { + creq.Collected.Add(args); + } + else if (this.config.ResponseBehavior is InteractionResponseBehavior.Respond) + { + try + { + string responseMessage = this.config.ResponseMessage ?? this.config.ResponseMessageFactory(args, this.client.ServiceProvider); + + if (args.Interaction.ResponseState is DiscordInteractionResponseState.Unacknowledged) + { + await args.Interaction.CreateResponseAsync + ( + DiscordInteractionResponseType.ChannelMessageWithSource, + new() { Content = responseMessage, IsEphemeral = true } + ); + } + else + { + await args.Interaction.CreateFollowupMessageAsync + ( + new() { Content = responseMessage, IsEphemeral = true } + ); + } + } + catch (Exception e) + { + this.client.Logger.LogWarning(e, "An exception was thrown during an interactivity response."); + } + } + } + } + } + public void Dispose() + { + this.matchRequests.Clear(); + this.collectRequests.Clear(); + } +} diff --git a/DSharpPlus.Interactivity/EventHandling/ComponentBased/ComponentPaginator.cs b/DSharpPlus.Interactivity/EventHandling/ComponentBased/ComponentPaginator.cs index 748eaa0a5c..12aa191644 100644 --- a/DSharpPlus.Interactivity/EventHandling/ComponentBased/ComponentPaginator.cs +++ b/DSharpPlus.Interactivity/EventHandling/ComponentBased/ComponentPaginator.cs @@ -1,139 +1,139 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using DSharpPlus.Entities; -using DSharpPlus.EventArgs; -using DSharpPlus.Interactivity.Enums; -using Microsoft.Extensions.Logging; - -namespace DSharpPlus.Interactivity.EventHandling; - -internal class ComponentPaginator : IPaginator -{ - private readonly DiscordClient client; - private readonly InteractivityConfiguration config; - private readonly DiscordMessageBuilder builder = new(); - private readonly Dictionary requests = []; - - public ComponentPaginator(DiscordClient client, InteractivityConfiguration config) - { - this.client = client; - this.config = config; - } - - public async Task DoPaginationAsync(IPaginationRequest request) - { - ulong id = (await request.GetMessageAsync()).Id; - this.requests.Add(id, request); - - try - { - TaskCompletionSource tcs = await request.GetTaskCompletionSourceAsync(); - await tcs.Task; - } - catch (Exception ex) - { - this.client.Logger.LogError(InteractivityEvents.InteractivityPaginationError, ex, "There was an exception while paginating."); - } - finally - { - this.requests.Remove(id); - try - { - await request.DoCleanupAsync(); - } - catch (Exception ex) - { - this.client.Logger.LogError(InteractivityEvents.InteractivityPaginationError, ex, "There was an exception while cleaning up pagination."); - } - } - } - - public void Dispose() => this.requests.Clear(); - - internal async Task HandleAsync(DiscordClient _, ComponentInteractionCreatedEventArgs e) - { - if (!this.requests.TryGetValue(e.Message.Id, out IPaginationRequest? req)) - { - return; - } - - await e.Interaction.CreateResponseAsync(DiscordInteractionResponseType.DeferredMessageUpdate); - - if (await req.GetUserAsync() != e.User) - { - if (this.config.ResponseBehavior is InteractionResponseBehavior.Respond) - { - await e.Interaction.CreateFollowupMessageAsync(new() { Content = this.config.ResponseMessage, IsEphemeral = true }); - } - - return; - } - - if (req is InteractionPaginationRequest ipr) - { - ipr.RegenerateCTS(e.Interaction); // Necessary to ensure we don't prematurely yeet the CTS // - } - - await HandlePaginationAsync(req, e); - } - - private async Task HandlePaginationAsync(IPaginationRequest request, ComponentInteractionCreatedEventArgs args) - { - PaginationButtons buttons = this.config.PaginationButtons; - DiscordMessage msg = await request.GetMessageAsync(); - string id = args.Id; - TaskCompletionSource tcs = await request.GetTaskCompletionSourceAsync(); - - Task paginationTask = id switch - { - _ when id == buttons.SkipLeft.CustomId => request.SkipLeftAsync(), - _ when id == buttons.SkipRight.CustomId => request.SkipRightAsync(), - _ when id == buttons.Stop.CustomId => Task.FromResult(tcs.TrySetResult(true)), - _ when id == buttons.Left.CustomId => request.PreviousPageAsync(), - _ when id == buttons.Right.CustomId => request.NextPageAsync(), - _ => Task.CompletedTask - }; - - await paginationTask; - - if (id == buttons.Stop.CustomId) - { - return; - } - - Page page = await request.GetPageAsync(); - IEnumerable bts = await request.GetButtonsAsync(); - - if (request is InteractionPaginationRequest) - { - DiscordWebhookBuilder builder = new DiscordWebhookBuilder() - .WithContent(page.Content) - .AddEmbed(page.Embed) - .AddActionRowComponent(bts); - - foreach (DiscordActionRowComponent actionRow in page.Components) - { - builder.AddActionRowComponent(actionRow); - } - - await args.Interaction.EditOriginalResponseAsync(builder); - return; - } - - this.builder.Clear(); - - this.builder - .WithContent(page.Content) - .AddEmbed(page.Embed) - .AddActionRowComponent(bts); - - foreach (DiscordActionRowComponent actionRow in page.Components) - { - this.builder.AddActionRowComponent(actionRow); - } - - await this.builder.ModifyAsync(msg); - - } -} +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using DSharpPlus.Entities; +using DSharpPlus.EventArgs; +using DSharpPlus.Interactivity.Enums; +using Microsoft.Extensions.Logging; + +namespace DSharpPlus.Interactivity.EventHandling; + +internal class ComponentPaginator : IPaginator +{ + private readonly DiscordClient client; + private readonly InteractivityConfiguration config; + private readonly DiscordMessageBuilder builder = new(); + private readonly Dictionary requests = []; + + public ComponentPaginator(DiscordClient client, InteractivityConfiguration config) + { + this.client = client; + this.config = config; + } + + public async Task DoPaginationAsync(IPaginationRequest request) + { + ulong id = (await request.GetMessageAsync()).Id; + this.requests.Add(id, request); + + try + { + TaskCompletionSource tcs = await request.GetTaskCompletionSourceAsync(); + await tcs.Task; + } + catch (Exception ex) + { + this.client.Logger.LogError(InteractivityEvents.InteractivityPaginationError, ex, "There was an exception while paginating."); + } + finally + { + this.requests.Remove(id); + try + { + await request.DoCleanupAsync(); + } + catch (Exception ex) + { + this.client.Logger.LogError(InteractivityEvents.InteractivityPaginationError, ex, "There was an exception while cleaning up pagination."); + } + } + } + + public void Dispose() => this.requests.Clear(); + + internal async Task HandleAsync(DiscordClient _, ComponentInteractionCreatedEventArgs e) + { + if (!this.requests.TryGetValue(e.Message.Id, out IPaginationRequest? req)) + { + return; + } + + await e.Interaction.CreateResponseAsync(DiscordInteractionResponseType.DeferredMessageUpdate); + + if (await req.GetUserAsync() != e.User) + { + if (this.config.ResponseBehavior is InteractionResponseBehavior.Respond) + { + await e.Interaction.CreateFollowupMessageAsync(new() { Content = this.config.ResponseMessage, IsEphemeral = true }); + } + + return; + } + + if (req is InteractionPaginationRequest ipr) + { + ipr.RegenerateCTS(e.Interaction); // Necessary to ensure we don't prematurely yeet the CTS // + } + + await HandlePaginationAsync(req, e); + } + + private async Task HandlePaginationAsync(IPaginationRequest request, ComponentInteractionCreatedEventArgs args) + { + PaginationButtons buttons = this.config.PaginationButtons; + DiscordMessage msg = await request.GetMessageAsync(); + string id = args.Id; + TaskCompletionSource tcs = await request.GetTaskCompletionSourceAsync(); + + Task paginationTask = id switch + { + _ when id == buttons.SkipLeft.CustomId => request.SkipLeftAsync(), + _ when id == buttons.SkipRight.CustomId => request.SkipRightAsync(), + _ when id == buttons.Stop.CustomId => Task.FromResult(tcs.TrySetResult(true)), + _ when id == buttons.Left.CustomId => request.PreviousPageAsync(), + _ when id == buttons.Right.CustomId => request.NextPageAsync(), + _ => Task.CompletedTask + }; + + await paginationTask; + + if (id == buttons.Stop.CustomId) + { + return; + } + + Page page = await request.GetPageAsync(); + IEnumerable bts = await request.GetButtonsAsync(); + + if (request is InteractionPaginationRequest) + { + DiscordWebhookBuilder builder = new DiscordWebhookBuilder() + .WithContent(page.Content) + .AddEmbed(page.Embed) + .AddActionRowComponent(bts); + + foreach (DiscordActionRowComponent actionRow in page.Components) + { + builder.AddActionRowComponent(actionRow); + } + + await args.Interaction.EditOriginalResponseAsync(builder); + return; + } + + this.builder.Clear(); + + this.builder + .WithContent(page.Content) + .AddEmbed(page.Embed) + .AddActionRowComponent(bts); + + foreach (DiscordActionRowComponent actionRow in page.Components) + { + this.builder.AddActionRowComponent(actionRow); + } + + await this.builder.ModifyAsync(msg); + + } +} diff --git a/DSharpPlus.Interactivity/EventHandling/ComponentBased/PaginationButtons.cs b/DSharpPlus.Interactivity/EventHandling/ComponentBased/PaginationButtons.cs index ccf534541a..12eb7b6262 100644 --- a/DSharpPlus.Interactivity/EventHandling/ComponentBased/PaginationButtons.cs +++ b/DSharpPlus.Interactivity/EventHandling/ComponentBased/PaginationButtons.cs @@ -1,40 +1,40 @@ -using DSharpPlus.Entities; - -namespace DSharpPlus.Interactivity.EventHandling; - -public class PaginationButtons -{ - public DiscordButtonComponent SkipLeft { internal get; set; } - public DiscordButtonComponent Left { internal get; set; } - public DiscordButtonComponent Stop { internal get; set; } - public DiscordButtonComponent Right { internal get; set; } - public DiscordButtonComponent SkipRight { internal get; set; } - - internal DiscordButtonComponent[] ButtonArray => - // This isn't great but I can't figure out how to pass these elements by ref :( - [ // And yes, it should be by ref to begin with, but in testing it refuses to update. - this.SkipLeft, // So I have no idea what that's about, and this array is "cheap-enough" and infrequent - this.Left, // enough to the point that it *should* be fine. - this.Stop, - this.Right, - this.SkipRight - ]; - - public PaginationButtons() - { - this.SkipLeft = new(DiscordButtonStyle.Secondary, "leftskip", null, false, new(DiscordEmoji.FromUnicode("⏮"))); - this.Left = new(DiscordButtonStyle.Secondary, "left", null, false, new(DiscordEmoji.FromUnicode("◀"))); - this.Stop = new(DiscordButtonStyle.Secondary, "stop", null, false, new(DiscordEmoji.FromUnicode("⏹"))); - this.Right = new(DiscordButtonStyle.Secondary, "right", null, false, new(DiscordEmoji.FromUnicode("▶"))); - this.SkipRight = new(DiscordButtonStyle.Secondary, "rightskip", null, false, new(DiscordEmoji.FromUnicode("⏭"))); - } - - public PaginationButtons(PaginationButtons other) - { - this.Stop = new(other.Stop); - this.Left = new(other.Left); - this.Right = new(other.Right); - this.SkipLeft = new(other.SkipLeft); - this.SkipRight = new(other.SkipRight); - } -} +using DSharpPlus.Entities; + +namespace DSharpPlus.Interactivity.EventHandling; + +public class PaginationButtons +{ + public DiscordButtonComponent SkipLeft { internal get; set; } + public DiscordButtonComponent Left { internal get; set; } + public DiscordButtonComponent Stop { internal get; set; } + public DiscordButtonComponent Right { internal get; set; } + public DiscordButtonComponent SkipRight { internal get; set; } + + internal DiscordButtonComponent[] ButtonArray => + // This isn't great but I can't figure out how to pass these elements by ref :( + [ // And yes, it should be by ref to begin with, but in testing it refuses to update. + this.SkipLeft, // So I have no idea what that's about, and this array is "cheap-enough" and infrequent + this.Left, // enough to the point that it *should* be fine. + this.Stop, + this.Right, + this.SkipRight + ]; + + public PaginationButtons() + { + this.SkipLeft = new(DiscordButtonStyle.Secondary, "leftskip", null, false, new(DiscordEmoji.FromUnicode("⏮"))); + this.Left = new(DiscordButtonStyle.Secondary, "left", null, false, new(DiscordEmoji.FromUnicode("◀"))); + this.Stop = new(DiscordButtonStyle.Secondary, "stop", null, false, new(DiscordEmoji.FromUnicode("⏹"))); + this.Right = new(DiscordButtonStyle.Secondary, "right", null, false, new(DiscordEmoji.FromUnicode("▶"))); + this.SkipRight = new(DiscordButtonStyle.Secondary, "rightskip", null, false, new(DiscordEmoji.FromUnicode("⏭"))); + } + + public PaginationButtons(PaginationButtons other) + { + this.Stop = new(other.Stop); + this.Left = new(other.Left); + this.Right = new(other.Right); + this.SkipLeft = new(other.SkipLeft); + this.SkipRight = new(other.SkipRight); + } +} diff --git a/DSharpPlus.Interactivity/EventHandling/ComponentBased/Requests/ButtonPaginationRequest.cs b/DSharpPlus.Interactivity/EventHandling/ComponentBased/Requests/ButtonPaginationRequest.cs index 73676e0986..b24cb35fee 100644 --- a/DSharpPlus.Interactivity/EventHandling/ComponentBased/Requests/ButtonPaginationRequest.cs +++ b/DSharpPlus.Interactivity/EventHandling/ComponentBased/Requests/ButtonPaginationRequest.cs @@ -1,181 +1,181 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using DSharpPlus.Entities; -using DSharpPlus.Interactivity.Enums; - -namespace DSharpPlus.Interactivity.EventHandling; - -internal class ButtonPaginationRequest : IPaginationRequest -{ - private int index; - private readonly List pages = []; - - private readonly TaskCompletionSource tcs = new(); - - private readonly CancellationToken token; - private readonly DiscordUser user; - private readonly DiscordMessage message; - private readonly PaginationButtons buttons; - private readonly PaginationBehaviour wrapBehavior; - private readonly ButtonPaginationBehavior behaviorBehavior; - - public ButtonPaginationRequest(DiscordMessage message, DiscordUser user, - PaginationBehaviour behavior, ButtonPaginationBehavior behaviorBehavior, - PaginationButtons buttons, IEnumerable pages, CancellationToken token) - { - this.user = user; - this.token = token; - this.buttons = new(buttons); - this.message = message; - this.wrapBehavior = behavior; - this.behaviorBehavior = behaviorBehavior; - this.pages.AddRange(pages); - - this.token.Register(() => this.tcs.TrySetResult(false)); - } - - public int PageCount => this.pages.Count; - - public Task GetPageAsync() - { - Task page = Task.FromResult(this.pages[this.index]); - - if (this.PageCount is 1) - { - this.buttons.SkipLeft.Disable(); - this.buttons.Left.Disable(); - this.buttons.Right.Disable(); - this.buttons.SkipRight.Disable(); - - this.buttons.Stop.Enable(); - return page; - } - - if (this.wrapBehavior is PaginationBehaviour.WrapAround) - { - return page; - } - - this.buttons.SkipLeft.Disabled = this.index < 2; - - this.buttons.Left.Disabled = this.index < 1; - - this.buttons.Right.Disabled = this.index >= this.PageCount - 1; - - this.buttons.SkipRight.Disabled = this.index >= this.PageCount - 2; - - return page; - } - - public Task SkipLeftAsync() - { - if (this.wrapBehavior is PaginationBehaviour.WrapAround) - { - this.index = this.index is 0 ? this.pages.Count - 1 : 0; - return Task.CompletedTask; - } - - this.index = 0; - - return Task.CompletedTask; - } - - public Task SkipRightAsync() - { - if (this.wrapBehavior is PaginationBehaviour.WrapAround) - { - this.index = this.index == this.PageCount - 1 ? 0 : this.PageCount - 1; - return Task.CompletedTask; - } - - this.index = this.pages.Count - 1; - - return Task.CompletedTask; - } - - public Task NextPageAsync() - { - this.index++; - - if (this.wrapBehavior is PaginationBehaviour.WrapAround) - { - if (this.index >= this.PageCount) - { - this.index = 0; - } - - return Task.CompletedTask; - } - - this.index = Math.Min(this.index, this.PageCount - 1); - - return Task.CompletedTask; - } - - public Task PreviousPageAsync() - { - this.index--; - - if (this.wrapBehavior is PaginationBehaviour.WrapAround) - { - if (this.index is -1) - { - this.index = this.pages.Count - 1; - } - - return Task.CompletedTask; - } - - this.index = Math.Max(this.index, 0); - - return Task.CompletedTask; - } - - public Task GetEmojisAsync() - => Task.FromException(new NotSupportedException("Emojis aren't supported for this request.")); - - public Task> GetButtonsAsync() - => Task.FromResult((IEnumerable)this.buttons.ButtonArray); - - public Task GetMessageAsync() => Task.FromResult(this.message); - - public Task GetUserAsync() => Task.FromResult(this.user); - - public Task> GetTaskCompletionSourceAsync() => Task.FromResult(this.tcs); - - // This is essentially the stop method. // - public async Task DoCleanupAsync() - { - switch (this.behaviorBehavior) - { - case ButtonPaginationBehavior.Disable: - IEnumerable buttons = this.buttons.ButtonArray.Select(b => b.Disable()); - - DiscordMessageBuilder builder = new DiscordMessageBuilder() - .WithContent(this.pages[this.index].Content) - .AddEmbed(this.pages[this.index].Embed) - .AddActionRowComponent(buttons); - - await builder.ModifyAsync(this.message); - break; - - case ButtonPaginationBehavior.DeleteButtons: - builder = new DiscordMessageBuilder() - .WithContent(this.pages[this.index].Content) - .AddEmbed(this.pages[this.index].Embed); - - await builder.ModifyAsync(this.message); - break; - - case ButtonPaginationBehavior.DeleteMessage: - await this.message.DeleteAsync(); - break; - - case ButtonPaginationBehavior.Ignore: - break; - } - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using DSharpPlus.Entities; +using DSharpPlus.Interactivity.Enums; + +namespace DSharpPlus.Interactivity.EventHandling; + +internal class ButtonPaginationRequest : IPaginationRequest +{ + private int index; + private readonly List pages = []; + + private readonly TaskCompletionSource tcs = new(); + + private readonly CancellationToken token; + private readonly DiscordUser user; + private readonly DiscordMessage message; + private readonly PaginationButtons buttons; + private readonly PaginationBehaviour wrapBehavior; + private readonly ButtonPaginationBehavior behaviorBehavior; + + public ButtonPaginationRequest(DiscordMessage message, DiscordUser user, + PaginationBehaviour behavior, ButtonPaginationBehavior behaviorBehavior, + PaginationButtons buttons, IEnumerable pages, CancellationToken token) + { + this.user = user; + this.token = token; + this.buttons = new(buttons); + this.message = message; + this.wrapBehavior = behavior; + this.behaviorBehavior = behaviorBehavior; + this.pages.AddRange(pages); + + this.token.Register(() => this.tcs.TrySetResult(false)); + } + + public int PageCount => this.pages.Count; + + public Task GetPageAsync() + { + Task page = Task.FromResult(this.pages[this.index]); + + if (this.PageCount is 1) + { + this.buttons.SkipLeft.Disable(); + this.buttons.Left.Disable(); + this.buttons.Right.Disable(); + this.buttons.SkipRight.Disable(); + + this.buttons.Stop.Enable(); + return page; + } + + if (this.wrapBehavior is PaginationBehaviour.WrapAround) + { + return page; + } + + this.buttons.SkipLeft.Disabled = this.index < 2; + + this.buttons.Left.Disabled = this.index < 1; + + this.buttons.Right.Disabled = this.index >= this.PageCount - 1; + + this.buttons.SkipRight.Disabled = this.index >= this.PageCount - 2; + + return page; + } + + public Task SkipLeftAsync() + { + if (this.wrapBehavior is PaginationBehaviour.WrapAround) + { + this.index = this.index is 0 ? this.pages.Count - 1 : 0; + return Task.CompletedTask; + } + + this.index = 0; + + return Task.CompletedTask; + } + + public Task SkipRightAsync() + { + if (this.wrapBehavior is PaginationBehaviour.WrapAround) + { + this.index = this.index == this.PageCount - 1 ? 0 : this.PageCount - 1; + return Task.CompletedTask; + } + + this.index = this.pages.Count - 1; + + return Task.CompletedTask; + } + + public Task NextPageAsync() + { + this.index++; + + if (this.wrapBehavior is PaginationBehaviour.WrapAround) + { + if (this.index >= this.PageCount) + { + this.index = 0; + } + + return Task.CompletedTask; + } + + this.index = Math.Min(this.index, this.PageCount - 1); + + return Task.CompletedTask; + } + + public Task PreviousPageAsync() + { + this.index--; + + if (this.wrapBehavior is PaginationBehaviour.WrapAround) + { + if (this.index is -1) + { + this.index = this.pages.Count - 1; + } + + return Task.CompletedTask; + } + + this.index = Math.Max(this.index, 0); + + return Task.CompletedTask; + } + + public Task GetEmojisAsync() + => Task.FromException(new NotSupportedException("Emojis aren't supported for this request.")); + + public Task> GetButtonsAsync() + => Task.FromResult((IEnumerable)this.buttons.ButtonArray); + + public Task GetMessageAsync() => Task.FromResult(this.message); + + public Task GetUserAsync() => Task.FromResult(this.user); + + public Task> GetTaskCompletionSourceAsync() => Task.FromResult(this.tcs); + + // This is essentially the stop method. // + public async Task DoCleanupAsync() + { + switch (this.behaviorBehavior) + { + case ButtonPaginationBehavior.Disable: + IEnumerable buttons = this.buttons.ButtonArray.Select(b => b.Disable()); + + DiscordMessageBuilder builder = new DiscordMessageBuilder() + .WithContent(this.pages[this.index].Content) + .AddEmbed(this.pages[this.index].Embed) + .AddActionRowComponent(buttons); + + await builder.ModifyAsync(this.message); + break; + + case ButtonPaginationBehavior.DeleteButtons: + builder = new DiscordMessageBuilder() + .WithContent(this.pages[this.index].Content) + .AddEmbed(this.pages[this.index].Embed); + + await builder.ModifyAsync(this.message); + break; + + case ButtonPaginationBehavior.DeleteMessage: + await this.message.DeleteAsync(); + break; + + case ButtonPaginationBehavior.Ignore: + break; + } + } +} diff --git a/DSharpPlus.Interactivity/EventHandling/ComponentBased/Requests/ComponentCollectRequest.cs b/DSharpPlus.Interactivity/EventHandling/ComponentBased/Requests/ComponentCollectRequest.cs index c8d1a0f5a4..b454f9d2e5 100644 --- a/DSharpPlus.Interactivity/EventHandling/ComponentBased/Requests/ComponentCollectRequest.cs +++ b/DSharpPlus.Interactivity/EventHandling/ComponentBased/Requests/ComponentCollectRequest.cs @@ -1,19 +1,19 @@ -using System; -using System.Collections.Concurrent; -using System.Threading; -using DSharpPlus.Entities; -using DSharpPlus.EventArgs; - -namespace DSharpPlus.Interactivity.EventHandling; - -/// -/// Represents a component event that is being waited for. -/// -internal sealed class ComponentCollectRequest : ComponentMatchRequest -{ - public ConcurrentBag Collected { get; private set; } - - public ComponentCollectRequest(DiscordMessage message, Func predicate, CancellationToken cancellation) : - base(message, predicate, cancellation) - { } -} +using System; +using System.Collections.Concurrent; +using System.Threading; +using DSharpPlus.Entities; +using DSharpPlus.EventArgs; + +namespace DSharpPlus.Interactivity.EventHandling; + +/// +/// Represents a component event that is being waited for. +/// +internal sealed class ComponentCollectRequest : ComponentMatchRequest +{ + public ConcurrentBag Collected { get; private set; } + + public ComponentCollectRequest(DiscordMessage message, Func predicate, CancellationToken cancellation) : + base(message, predicate, cancellation) + { } +} diff --git a/DSharpPlus.Interactivity/EventHandling/ComponentBased/Requests/ComponentMatchRequest.cs b/DSharpPlus.Interactivity/EventHandling/ComponentBased/Requests/ComponentMatchRequest.cs index b8578ff95f..0917369cac 100644 --- a/DSharpPlus.Interactivity/EventHandling/ComponentBased/Requests/ComponentMatchRequest.cs +++ b/DSharpPlus.Interactivity/EventHandling/ComponentBased/Requests/ComponentMatchRequest.cs @@ -1,36 +1,36 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using DSharpPlus.Entities; -using DSharpPlus.EventArgs; - -namespace DSharpPlus.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; - - 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 // - } - - public bool IsMatch(ComponentInteractionCreatedEventArgs args) => this.predicate(args); -} +using System; +using System.Threading; +using System.Threading.Tasks; +using DSharpPlus.Entities; +using DSharpPlus.EventArgs; + +namespace DSharpPlus.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; + + 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 // + } + + public bool IsMatch(ComponentInteractionCreatedEventArgs args) => this.predicate(args); +} diff --git a/DSharpPlus.Interactivity/EventHandling/ComponentBased/Requests/InteractionPaginationRequest.cs b/DSharpPlus.Interactivity/EventHandling/ComponentBased/Requests/InteractionPaginationRequest.cs index 7cab3f4bc4..7ae2b09965 100644 --- a/DSharpPlus.Interactivity/EventHandling/ComponentBased/Requests/InteractionPaginationRequest.cs +++ b/DSharpPlus.Interactivity/EventHandling/ComponentBased/Requests/InteractionPaginationRequest.cs @@ -1,195 +1,195 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using DSharpPlus.Entities; -using DSharpPlus.Interactivity.Enums; - -namespace DSharpPlus.Interactivity.EventHandling; - -internal class InteractionPaginationRequest : IPaginationRequest -{ - private int index; - private readonly List pages = []; - - private readonly TaskCompletionSource tcs = new(); - - private DiscordInteraction lastInteraction; - private CancellationTokenSource interactionCts; - - private readonly CancellationToken token; - private readonly DiscordUser user; - private readonly DiscordMessage message; - private readonly PaginationButtons buttons; - private readonly PaginationBehaviour wrapBehavior; - private readonly ButtonPaginationBehavior behaviorBehavior; - - public InteractionPaginationRequest(DiscordInteraction interaction, DiscordMessage message, DiscordUser user, - PaginationBehaviour behavior, ButtonPaginationBehavior behaviorBehavior, - PaginationButtons buttons, IEnumerable pages, CancellationToken token) - { - this.user = user; - this.token = token; - this.buttons = new(buttons); - this.message = message; - this.wrapBehavior = behavior; - this.behaviorBehavior = behaviorBehavior; - this.pages.AddRange(pages); - - RegenerateCTS(interaction); - this.token.Register(() => this.tcs.TrySetResult(false)); - } - - public int PageCount => this.pages.Count; - - internal void RegenerateCTS(DiscordInteraction interaction) - { - this.interactionCts?.Dispose(); - this.lastInteraction = interaction; - this.interactionCts = new(TimeSpan.FromSeconds((60 * 15) - 5)); - this.interactionCts.Token.Register(() => this.tcs.TrySetResult(false)); - } - - public Task GetPageAsync() - { - Task page = Task.FromResult(this.pages[this.index]); - - if (this.PageCount is 1) - { - foreach (DiscordButtonComponent button in this.buttons.ButtonArray) - { - button.Disable(); - } - - this.buttons.Stop.Enable(); - return page; - } - - if (this.wrapBehavior is PaginationBehaviour.WrapAround) - { - return page; - } - - this.buttons.SkipLeft.Disabled = this.index < 2; - - this.buttons.Left.Disabled = this.index < 1; - - this.buttons.Right.Disabled = this.index == this.PageCount - 1; - - this.buttons.SkipRight.Disabled = this.index >= this.PageCount - 2; - - return page; - } - - public Task SkipLeftAsync() - { - if (this.wrapBehavior is PaginationBehaviour.WrapAround) - { - this.index = this.index is 0 ? this.pages.Count - 1 : 0; - return Task.CompletedTask; - } - - this.index = 0; - - return Task.CompletedTask; - } - - public Task SkipRightAsync() - { - if (this.wrapBehavior is PaginationBehaviour.WrapAround) - { - this.index = this.index == this.PageCount - 1 ? 0 : this.PageCount - 1; - return Task.CompletedTask; - } - - this.index = this.pages.Count - 1; - - return Task.CompletedTask; - } - - public Task NextPageAsync() - { - this.index++; - - if (this.wrapBehavior is PaginationBehaviour.WrapAround) - { - if (this.index >= this.PageCount) - { - this.index = 0; - } - - return Task.CompletedTask; - } - - this.index = Math.Min(this.index, this.PageCount - 1); - - return Task.CompletedTask; - } - - public Task PreviousPageAsync() - { - this.index--; - - if (this.wrapBehavior is PaginationBehaviour.WrapAround) - { - if (this.index is -1) - { - this.index = this.pages.Count - 1; - } - - return Task.CompletedTask; - } - - this.index = Math.Max(this.index, 0); - - return Task.CompletedTask; - } - - public Task GetEmojisAsync() - => Task.FromException(new NotSupportedException("Emojis aren't supported for this request.")); - - public Task> GetButtonsAsync() - => Task.FromResult((IEnumerable)this.buttons.ButtonArray); - - public Task GetMessageAsync() => Task.FromResult(this.message); - - public Task GetUserAsync() => Task.FromResult(this.user); - - public Task> GetTaskCompletionSourceAsync() => Task.FromResult(this.tcs); - - // This is essentially the stop method. // - public async Task DoCleanupAsync() - { - switch (this.behaviorBehavior) - { - case ButtonPaginationBehavior.Disable: - IEnumerable buttons = this.buttons.ButtonArray - .Select(b => new DiscordButtonComponent(b)) - .Select(b => b.Disable()); - - DiscordWebhookBuilder builder = new DiscordWebhookBuilder() - .WithContent(this.pages[this.index].Content) - .AddEmbed(this.pages[this.index].Embed) - .AddActionRowComponent(buttons); - - await this.lastInteraction.EditOriginalResponseAsync(builder); - break; - - case ButtonPaginationBehavior.DeleteButtons: - builder = new DiscordWebhookBuilder() - .WithContent(this.pages[this.index].Content) - .AddEmbed(this.pages[this.index].Embed); - - await this.lastInteraction.EditOriginalResponseAsync(builder); - break; - - case ButtonPaginationBehavior.DeleteMessage: - await this.lastInteraction.DeleteOriginalResponseAsync(); - break; - - case ButtonPaginationBehavior.Ignore: - break; - } - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using DSharpPlus.Entities; +using DSharpPlus.Interactivity.Enums; + +namespace DSharpPlus.Interactivity.EventHandling; + +internal class InteractionPaginationRequest : IPaginationRequest +{ + private int index; + private readonly List pages = []; + + private readonly TaskCompletionSource tcs = new(); + + private DiscordInteraction lastInteraction; + private CancellationTokenSource interactionCts; + + private readonly CancellationToken token; + private readonly DiscordUser user; + private readonly DiscordMessage message; + private readonly PaginationButtons buttons; + private readonly PaginationBehaviour wrapBehavior; + private readonly ButtonPaginationBehavior behaviorBehavior; + + public InteractionPaginationRequest(DiscordInteraction interaction, DiscordMessage message, DiscordUser user, + PaginationBehaviour behavior, ButtonPaginationBehavior behaviorBehavior, + PaginationButtons buttons, IEnumerable pages, CancellationToken token) + { + this.user = user; + this.token = token; + this.buttons = new(buttons); + this.message = message; + this.wrapBehavior = behavior; + this.behaviorBehavior = behaviorBehavior; + this.pages.AddRange(pages); + + RegenerateCTS(interaction); + this.token.Register(() => this.tcs.TrySetResult(false)); + } + + public int PageCount => this.pages.Count; + + internal void RegenerateCTS(DiscordInteraction interaction) + { + this.interactionCts?.Dispose(); + this.lastInteraction = interaction; + this.interactionCts = new(TimeSpan.FromSeconds((60 * 15) - 5)); + this.interactionCts.Token.Register(() => this.tcs.TrySetResult(false)); + } + + public Task GetPageAsync() + { + Task page = Task.FromResult(this.pages[this.index]); + + if (this.PageCount is 1) + { + foreach (DiscordButtonComponent button in this.buttons.ButtonArray) + { + button.Disable(); + } + + this.buttons.Stop.Enable(); + return page; + } + + if (this.wrapBehavior is PaginationBehaviour.WrapAround) + { + return page; + } + + this.buttons.SkipLeft.Disabled = this.index < 2; + + this.buttons.Left.Disabled = this.index < 1; + + this.buttons.Right.Disabled = this.index == this.PageCount - 1; + + this.buttons.SkipRight.Disabled = this.index >= this.PageCount - 2; + + return page; + } + + public Task SkipLeftAsync() + { + if (this.wrapBehavior is PaginationBehaviour.WrapAround) + { + this.index = this.index is 0 ? this.pages.Count - 1 : 0; + return Task.CompletedTask; + } + + this.index = 0; + + return Task.CompletedTask; + } + + public Task SkipRightAsync() + { + if (this.wrapBehavior is PaginationBehaviour.WrapAround) + { + this.index = this.index == this.PageCount - 1 ? 0 : this.PageCount - 1; + return Task.CompletedTask; + } + + this.index = this.pages.Count - 1; + + return Task.CompletedTask; + } + + public Task NextPageAsync() + { + this.index++; + + if (this.wrapBehavior is PaginationBehaviour.WrapAround) + { + if (this.index >= this.PageCount) + { + this.index = 0; + } + + return Task.CompletedTask; + } + + this.index = Math.Min(this.index, this.PageCount - 1); + + return Task.CompletedTask; + } + + public Task PreviousPageAsync() + { + this.index--; + + if (this.wrapBehavior is PaginationBehaviour.WrapAround) + { + if (this.index is -1) + { + this.index = this.pages.Count - 1; + } + + return Task.CompletedTask; + } + + this.index = Math.Max(this.index, 0); + + return Task.CompletedTask; + } + + public Task GetEmojisAsync() + => Task.FromException(new NotSupportedException("Emojis aren't supported for this request.")); + + public Task> GetButtonsAsync() + => Task.FromResult((IEnumerable)this.buttons.ButtonArray); + + public Task GetMessageAsync() => Task.FromResult(this.message); + + public Task GetUserAsync() => Task.FromResult(this.user); + + public Task> GetTaskCompletionSourceAsync() => Task.FromResult(this.tcs); + + // This is essentially the stop method. // + public async Task DoCleanupAsync() + { + switch (this.behaviorBehavior) + { + case ButtonPaginationBehavior.Disable: + IEnumerable buttons = this.buttons.ButtonArray + .Select(b => new DiscordButtonComponent(b)) + .Select(b => b.Disable()); + + DiscordWebhookBuilder builder = new DiscordWebhookBuilder() + .WithContent(this.pages[this.index].Content) + .AddEmbed(this.pages[this.index].Embed) + .AddActionRowComponent(buttons); + + await this.lastInteraction.EditOriginalResponseAsync(builder); + break; + + case ButtonPaginationBehavior.DeleteButtons: + builder = new DiscordWebhookBuilder() + .WithContent(this.pages[this.index].Content) + .AddEmbed(this.pages[this.index].Embed); + + await this.lastInteraction.EditOriginalResponseAsync(builder); + break; + + case ButtonPaginationBehavior.DeleteMessage: + await this.lastInteraction.DeleteOriginalResponseAsync(); + break; + + case ButtonPaginationBehavior.Ignore: + break; + } + } +} diff --git a/DSharpPlus.Interactivity/EventHandling/EventWaiter.cs b/DSharpPlus.Interactivity/EventHandling/EventWaiter.cs index 82abb719cb..5594bec4e5 100644 --- a/DSharpPlus.Interactivity/EventHandling/EventWaiter.cs +++ b/DSharpPlus.Interactivity/EventHandling/EventWaiter.cs @@ -1,134 +1,134 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; -using System.Threading.Tasks; -using ConcurrentCollections; -using DSharpPlus.AsyncEvents; -using Microsoft.Extensions.Logging; - -namespace DSharpPlus.Interactivity.EventHandling; - -/// -/// Eventwaiter is a class that serves as a layer between the InteractivityExtension -/// and the DiscordClient to listen to an event and check for matches to a predicate. -/// -/// -internal class EventWaiter : IDisposable where T : AsyncEventArgs -{ - private DiscordClient client; - private AsyncEvent @event; - private AsyncEventHandler handler; - private ConcurrentHashSet> matchrequests; - private ConcurrentHashSet> collectrequests; - private bool disposed = false; - - /// - /// Creates a new Eventwaiter object. - /// - /// The extension to register to. - public EventWaiter(InteractivityExtension extension) - { - this.client = extension.Client; - - this.@event = (AsyncEvent)extension.eventDistributor.GetOrAdd - ( - typeof(T), - new AsyncEvent(extension.errorHandler) - ); - - this.matchrequests = []; - this.collectrequests = []; - this.handler = new AsyncEventHandler(HandleEvent); - this.@event.Register(this.handler); - } - - /// - /// Waits for a match to a specific request, else returns null. - /// - /// Request to match - /// - public async Task WaitForMatchAsync(MatchRequest request) - { - T result = null; - this.matchrequests.Add(request); - try - { - result = await request.tcs.Task; - } - catch (Exception ex) - { - this.client.Logger.LogError(InteractivityEvents.InteractivityWaitError, ex, "An exception occurred while waiting for {Request}", typeof(T).Name); - } - finally - { - request.Dispose(); - this.matchrequests.TryRemove(request); - } - - return result; - } - - public async Task> CollectMatchesAsync(CollectRequest request) - { - ReadOnlyCollection result; - this.collectrequests.Add(request); - try - { - await request.tcs.Task; - } - catch (Exception ex) - { - this.client.Logger.LogError(InteractivityEvents.InteractivityWaitError, ex, "An exception occurred while collecting from {Request}", typeof(T).Name); - } - finally - { - result = new ReadOnlyCollection(new HashSet(request.collected).ToList()); - request.Dispose(); - this.collectrequests.TryRemove(request); - } - return result; - } - - private Task HandleEvent(DiscordClient client, T eventargs) - { - if (!this.disposed) - { - foreach (MatchRequest req in this.matchrequests) - { - if (req.predicate(eventargs)) - { - req.tcs.TrySetResult(eventargs); - } - } - - foreach (CollectRequest req in this.collectrequests) - { - if (req.predicate(eventargs)) - { - req.collected.Add(eventargs); - } - } - } - - return Task.CompletedTask; - } - - /// - /// Disposes this EventWaiter - /// - public void Dispose() - { - if (this.disposed) - { - return; - } - - this.disposed = true; - this.matchrequests.Clear(); - this.collectrequests.Clear(); - - // Satisfy rule CA1816. Can be removed if this class is sealed. - GC.SuppressFinalize(this); - } -} +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Threading.Tasks; +using ConcurrentCollections; +using DSharpPlus.AsyncEvents; +using Microsoft.Extensions.Logging; + +namespace DSharpPlus.Interactivity.EventHandling; + +/// +/// Eventwaiter is a class that serves as a layer between the InteractivityExtension +/// and the DiscordClient to listen to an event and check for matches to a predicate. +/// +/// +internal class EventWaiter : IDisposable where T : AsyncEventArgs +{ + private DiscordClient client; + private AsyncEvent @event; + private AsyncEventHandler handler; + private ConcurrentHashSet> matchrequests; + private ConcurrentHashSet> collectrequests; + private bool disposed = false; + + /// + /// Creates a new Eventwaiter object. + /// + /// The extension to register to. + public EventWaiter(InteractivityExtension extension) + { + this.client = extension.Client; + + this.@event = (AsyncEvent)extension.eventDistributor.GetOrAdd + ( + typeof(T), + new AsyncEvent(extension.errorHandler) + ); + + this.matchrequests = []; + this.collectrequests = []; + this.handler = new AsyncEventHandler(HandleEvent); + this.@event.Register(this.handler); + } + + /// + /// Waits for a match to a specific request, else returns null. + /// + /// Request to match + /// + public async Task WaitForMatchAsync(MatchRequest request) + { + T result = null; + this.matchrequests.Add(request); + try + { + result = await request.tcs.Task; + } + catch (Exception ex) + { + this.client.Logger.LogError(InteractivityEvents.InteractivityWaitError, ex, "An exception occurred while waiting for {Request}", typeof(T).Name); + } + finally + { + request.Dispose(); + this.matchrequests.TryRemove(request); + } + + return result; + } + + public async Task> CollectMatchesAsync(CollectRequest request) + { + ReadOnlyCollection result; + this.collectrequests.Add(request); + try + { + await request.tcs.Task; + } + catch (Exception ex) + { + this.client.Logger.LogError(InteractivityEvents.InteractivityWaitError, ex, "An exception occurred while collecting from {Request}", typeof(T).Name); + } + finally + { + result = new ReadOnlyCollection(new HashSet(request.collected).ToList()); + request.Dispose(); + this.collectrequests.TryRemove(request); + } + return result; + } + + private Task HandleEvent(DiscordClient client, T eventargs) + { + if (!this.disposed) + { + foreach (MatchRequest req in this.matchrequests) + { + if (req.predicate(eventargs)) + { + req.tcs.TrySetResult(eventargs); + } + } + + foreach (CollectRequest req in this.collectrequests) + { + if (req.predicate(eventargs)) + { + req.collected.Add(eventargs); + } + } + } + + return Task.CompletedTask; + } + + /// + /// Disposes this EventWaiter + /// + public void Dispose() + { + if (this.disposed) + { + return; + } + + this.disposed = true; + this.matchrequests.Clear(); + this.collectrequests.Clear(); + + // Satisfy rule CA1816. Can be removed if this class is sealed. + GC.SuppressFinalize(this); + } +} diff --git a/DSharpPlus.Interactivity/EventHandling/IPaginator.cs b/DSharpPlus.Interactivity/EventHandling/IPaginator.cs index d080da7b71..838c40128a 100644 --- a/DSharpPlus.Interactivity/EventHandling/IPaginator.cs +++ b/DSharpPlus.Interactivity/EventHandling/IPaginator.cs @@ -1,18 +1,18 @@ -using System.Threading.Tasks; - -namespace DSharpPlus.Interactivity.EventHandling; - -internal interface IPaginator -{ - /// - /// Paginates. - /// - /// The request to paginate. - /// A task that completes when the pagination finishes or times out. - public Task DoPaginationAsync(IPaginationRequest request); - - /// - /// Disposes this EventWaiter - /// - public void Dispose(); -} +using System.Threading.Tasks; + +namespace DSharpPlus.Interactivity.EventHandling; + +internal interface IPaginator +{ + /// + /// Paginates. + /// + /// The request to paginate. + /// A task that completes when the pagination finishes or times out. + public Task DoPaginationAsync(IPaginationRequest request); + + /// + /// Disposes this EventWaiter + /// + public void Dispose(); +} diff --git a/DSharpPlus.Interactivity/EventHandling/ModalEventWaiter.cs b/DSharpPlus.Interactivity/EventHandling/ModalEventWaiter.cs index a344460f06..082e3a90f1 100644 --- a/DSharpPlus.Interactivity/EventHandling/ModalEventWaiter.cs +++ b/DSharpPlus.Interactivity/EventHandling/ModalEventWaiter.cs @@ -1,68 +1,68 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using ConcurrentCollections; -using DSharpPlus.EventArgs; -using Microsoft.Extensions.Logging; - -namespace DSharpPlus.Interactivity.EventHandling; - -/// -/// Modal version of -/// -internal class ModalEventWaiter : IDisposable -{ - private DiscordClient Client { get; } - - /// - /// Collection of representing requests to wait for modals. - /// - private ConcurrentHashSet MatchRequests { get; } = []; - - public ModalEventWaiter(DiscordClient client) - => this.Client = client; - - /// - /// Waits for a specified 's predicate to be fulfilled. - /// - /// The request to wait for a match. - /// The returned args, or null if it timed out. - public async Task WaitForMatchAsync(ModalMatchRequest request) - { - this.MatchRequests.Add(request); - - try - { - return await request.Tcs.Task; // awaits request until completion or cancellation - } - catch (Exception e) - { - this.Client.Logger.LogError(InteractivityEvents.InteractivityWaitError, e, "An exception was thrown while waiting for a modal."); - return null; - } - finally - { - this.MatchRequests.TryRemove(request); - } - } - - /// - /// Is called whenever is fired. Checks to see submitted modal matches any of the current requests. - /// - /// - /// The to match. - /// A task that represents matching the requests. - internal Task Handle(DiscordClient _, ModalSubmittedEventArgs args) - { - foreach (ModalMatchRequest? req in this.MatchRequests.ToArray()) // ToArray to get a copy of the collection that won't be modified during iteration - { - if (req.ModalId == args.Interaction.Data.CustomId && req.IsMatch(args)) // will catch all matches - { - req.Tcs.TrySetResult(args); - } - } - return Task.CompletedTask; - } - - public void Dispose() => this.MatchRequests.Clear(); -} +using System; +using System.Linq; +using System.Threading.Tasks; +using ConcurrentCollections; +using DSharpPlus.EventArgs; +using Microsoft.Extensions.Logging; + +namespace DSharpPlus.Interactivity.EventHandling; + +/// +/// Modal version of +/// +internal class ModalEventWaiter : IDisposable +{ + private DiscordClient Client { get; } + + /// + /// Collection of representing requests to wait for modals. + /// + private ConcurrentHashSet MatchRequests { get; } = []; + + public ModalEventWaiter(DiscordClient client) + => this.Client = client; + + /// + /// Waits for a specified 's predicate to be fulfilled. + /// + /// The request to wait for a match. + /// The returned args, or null if it timed out. + public async Task WaitForMatchAsync(ModalMatchRequest request) + { + this.MatchRequests.Add(request); + + try + { + return await request.Tcs.Task; // awaits request until completion or cancellation + } + catch (Exception e) + { + this.Client.Logger.LogError(InteractivityEvents.InteractivityWaitError, e, "An exception was thrown while waiting for a modal."); + return null; + } + finally + { + this.MatchRequests.TryRemove(request); + } + } + + /// + /// Is called whenever is fired. Checks to see submitted modal matches any of the current requests. + /// + /// + /// The to match. + /// A task that represents matching the requests. + internal Task Handle(DiscordClient _, ModalSubmittedEventArgs args) + { + foreach (ModalMatchRequest? req in this.MatchRequests.ToArray()) // ToArray to get a copy of the collection that won't be modified during iteration + { + if (req.ModalId == args.Interaction.Data.CustomId && req.IsMatch(args)) // will catch all matches + { + req.Tcs.TrySetResult(args); + } + } + return Task.CompletedTask; + } + + public void Dispose() => this.MatchRequests.Clear(); +} diff --git a/DSharpPlus.Interactivity/EventHandling/Paginator.cs b/DSharpPlus.Interactivity/EventHandling/Paginator.cs index 562499388f..c7c1dd35ce 100644 --- a/DSharpPlus.Interactivity/EventHandling/Paginator.cs +++ b/DSharpPlus.Interactivity/EventHandling/Paginator.cs @@ -1,276 +1,276 @@ -using System; -using System.Threading.Tasks; -using ConcurrentCollections; -using DSharpPlus.Entities; -using DSharpPlus.EventArgs; -using DSharpPlus.Interactivity.Enums; -using Microsoft.Extensions.Logging; - -namespace DSharpPlus.Interactivity.EventHandling; - -internal class Paginator : IPaginator -{ - private DiscordClient client; - private ConcurrentHashSet requests; - - /// - /// Creates a new Eventwaiter object. - /// - /// Your DiscordClient - public Paginator(DiscordClient client) - { - this.client = client; - this.requests = []; - } - - public async Task DoPaginationAsync(IPaginationRequest request) - { - await ResetReactionsAsync(request); - this.requests.Add(request); - try - { - TaskCompletionSource tcs = await request.GetTaskCompletionSourceAsync(); - await tcs.Task; - } - catch (Exception ex) - { - this.client.Logger.LogError(InteractivityEvents.InteractivityPaginationError, ex, "Exception occurred while paginating"); - } - finally - { - this.requests.TryRemove(request); - try - { - await request.DoCleanupAsync(); - } - catch (Exception ex) - { - this.client.Logger.LogError(InteractivityEvents.InteractivityPaginationError, ex, "Exception occurred while paginating"); - } - } - } - - internal Task HandleReactionAdd(DiscordClient client, MessageReactionAddedEventArgs eventargs) - { - if (this.requests.Count == 0) - { - return Task.CompletedTask; - } - - _ = Task.Run(async () => - { - foreach (IPaginationRequest req in this.requests) - { - PaginationEmojis emojis = await req.GetEmojisAsync(); - DiscordMessage msg = await req.GetMessageAsync(); - DiscordUser usr = await req.GetUserAsync(); - - if (msg.Id == eventargs.Message.Id) - { - if (eventargs.User.Id == usr.Id) - { - if (req.PageCount > 1 && - (eventargs.Emoji == emojis.Left || - eventargs.Emoji == emojis.SkipLeft || - eventargs.Emoji == emojis.Right || - eventargs.Emoji == emojis.SkipRight || - eventargs.Emoji == emojis.Stop)) - { - await PaginateAsync(req, eventargs.Emoji); - } - else if (eventargs.Emoji == emojis.Stop && - req is PaginationRequest paginationRequest && - paginationRequest.PaginationDeletion == PaginationDeletion.DeleteMessage) - { - await PaginateAsync(req, eventargs.Emoji); - } - else - { - await msg.DeleteReactionAsync(eventargs.Emoji, eventargs.User); - } - } - else if (eventargs.User.Id != this.client.CurrentUser.Id) - { - if (eventargs.Emoji != emojis.Left && - eventargs.Emoji != emojis.SkipLeft && - eventargs.Emoji != emojis.Right && - eventargs.Emoji != emojis.SkipRight && - eventargs.Emoji != emojis.Stop) - { - await msg.DeleteReactionAsync(eventargs.Emoji, eventargs.User); - } - } - } - } - }); - return Task.CompletedTask; - } - - internal Task HandleReactionRemove(DiscordClient client, MessageReactionRemovedEventArgs eventargs) - { - if (this.requests.Count == 0) - { - return Task.CompletedTask; - } - - _ = Task.Run(async () => - { - foreach (IPaginationRequest req in this.requests) - { - PaginationEmojis emojis = await req.GetEmojisAsync(); - DiscordMessage msg = await req.GetMessageAsync(); - DiscordUser usr = await req.GetUserAsync(); - - if (msg.Id == eventargs.Message.Id) - { - if (eventargs.User.Id == usr.Id) - { - if (req.PageCount > 1 && - (eventargs.Emoji == emojis.Left || - eventargs.Emoji == emojis.SkipLeft || - eventargs.Emoji == emojis.Right || - eventargs.Emoji == emojis.SkipRight || - eventargs.Emoji == emojis.Stop)) - { - await PaginateAsync(req, eventargs.Emoji); - } - else if (eventargs.Emoji == emojis.Stop && - req is PaginationRequest paginationRequest && - paginationRequest.PaginationDeletion == PaginationDeletion.DeleteMessage) - { - await PaginateAsync(req, eventargs.Emoji); - } - } - } - } - }); - - return Task.CompletedTask; - } - - internal Task HandleReactionClear(DiscordClient client, MessageReactionsClearedEventArgs eventargs) - { - if (this.requests.Count == 0) - { - return Task.CompletedTask; - } - - _ = Task.Run(async () => - { - foreach (IPaginationRequest req in this.requests) - { - DiscordMessage msg = await req.GetMessageAsync(); - - if (msg.Id == eventargs.Message.Id) - { - await ResetReactionsAsync(req); - } - } - }); - - return Task.CompletedTask; - } - - private static async Task ResetReactionsAsync(IPaginationRequest p) - { - DiscordMessage msg = await p.GetMessageAsync(); - PaginationEmojis emojis = await p.GetEmojisAsync(); - - // Test permissions to avoid a 403: - // https://totally-not.a-sketchy.site/3pXpRLK.png - // Yes, this is an issue - // No, we should not require people to guarantee MANAGE_MESSAGES - // Need to check following: - // - In guild? - // - If yes, check if have permission - // - If all above fail (DM || guild && no permission), skip this - DiscordChannel? chn = msg.Channel; - DiscordGuild? gld = chn?.Guild; - DiscordMember? mbr = gld?.CurrentMember; - - if (mbr != null /* == is guild and cache is valid */ && chn.PermissionsFor(mbr).HasPermission(DiscordPermission.ManageChannels)) /* == has permissions */ - { - await msg.DeleteAllReactionsAsync("Pagination"); - } - // ENDOF: 403 fix - - if (p.PageCount > 1) - { - if (emojis.SkipLeft != null) - { - await msg.CreateReactionAsync(emojis.SkipLeft); - } - - if (emojis.Left != null) - { - await msg.CreateReactionAsync(emojis.Left); - } - - if (emojis.Right != null) - { - await msg.CreateReactionAsync(emojis.Right); - } - - if (emojis.SkipRight != null) - { - await msg.CreateReactionAsync(emojis.SkipRight); - } - - if (emojis.Stop != null) - { - await msg.CreateReactionAsync(emojis.Stop); - } - } - else if (emojis.Stop != null && p is PaginationRequest paginationRequest && paginationRequest.PaginationDeletion == PaginationDeletion.DeleteMessage) - { - await msg.CreateReactionAsync(emojis.Stop); - } - } - - private static async Task PaginateAsync(IPaginationRequest p, DiscordEmoji emoji) - { - PaginationEmojis emojis = await p.GetEmojisAsync(); - DiscordMessage msg = await p.GetMessageAsync(); - - if (emoji == emojis.SkipLeft) - { - await p.SkipLeftAsync(); - } - else if (emoji == emojis.Left) - { - await p.PreviousPageAsync(); - } - else if (emoji == emojis.Right) - { - await p.NextPageAsync(); - } - else if (emoji == emojis.SkipRight) - { - await p.SkipRightAsync(); - } - else if (emoji == emojis.Stop) - { - TaskCompletionSource tcs = await p.GetTaskCompletionSourceAsync(); - tcs.TrySetResult(true); - return; - } - - Page page = await p.GetPageAsync(); - DiscordMessageBuilder builder = new DiscordMessageBuilder() - .WithContent(page.Content) - .AddEmbed(page.Embed); - - await builder.ModifyAsync(msg); - } - - /// - /// Disposes this EventWaiter - /// - public void Dispose() - { - // Why doesn't this class implement IDisposable? - - this.requests?.Clear(); - this.requests = null!; - } -} +using System; +using System.Threading.Tasks; +using ConcurrentCollections; +using DSharpPlus.Entities; +using DSharpPlus.EventArgs; +using DSharpPlus.Interactivity.Enums; +using Microsoft.Extensions.Logging; + +namespace DSharpPlus.Interactivity.EventHandling; + +internal class Paginator : IPaginator +{ + private DiscordClient client; + private ConcurrentHashSet requests; + + /// + /// Creates a new Eventwaiter object. + /// + /// Your DiscordClient + public Paginator(DiscordClient client) + { + this.client = client; + this.requests = []; + } + + public async Task DoPaginationAsync(IPaginationRequest request) + { + await ResetReactionsAsync(request); + this.requests.Add(request); + try + { + TaskCompletionSource tcs = await request.GetTaskCompletionSourceAsync(); + await tcs.Task; + } + catch (Exception ex) + { + this.client.Logger.LogError(InteractivityEvents.InteractivityPaginationError, ex, "Exception occurred while paginating"); + } + finally + { + this.requests.TryRemove(request); + try + { + await request.DoCleanupAsync(); + } + catch (Exception ex) + { + this.client.Logger.LogError(InteractivityEvents.InteractivityPaginationError, ex, "Exception occurred while paginating"); + } + } + } + + internal Task HandleReactionAdd(DiscordClient client, MessageReactionAddedEventArgs eventargs) + { + if (this.requests.Count == 0) + { + return Task.CompletedTask; + } + + _ = Task.Run(async () => + { + foreach (IPaginationRequest req in this.requests) + { + PaginationEmojis emojis = await req.GetEmojisAsync(); + DiscordMessage msg = await req.GetMessageAsync(); + DiscordUser usr = await req.GetUserAsync(); + + if (msg.Id == eventargs.Message.Id) + { + if (eventargs.User.Id == usr.Id) + { + if (req.PageCount > 1 && + (eventargs.Emoji == emojis.Left || + eventargs.Emoji == emojis.SkipLeft || + eventargs.Emoji == emojis.Right || + eventargs.Emoji == emojis.SkipRight || + eventargs.Emoji == emojis.Stop)) + { + await PaginateAsync(req, eventargs.Emoji); + } + else if (eventargs.Emoji == emojis.Stop && + req is PaginationRequest paginationRequest && + paginationRequest.PaginationDeletion == PaginationDeletion.DeleteMessage) + { + await PaginateAsync(req, eventargs.Emoji); + } + else + { + await msg.DeleteReactionAsync(eventargs.Emoji, eventargs.User); + } + } + else if (eventargs.User.Id != this.client.CurrentUser.Id) + { + if (eventargs.Emoji != emojis.Left && + eventargs.Emoji != emojis.SkipLeft && + eventargs.Emoji != emojis.Right && + eventargs.Emoji != emojis.SkipRight && + eventargs.Emoji != emojis.Stop) + { + await msg.DeleteReactionAsync(eventargs.Emoji, eventargs.User); + } + } + } + } + }); + return Task.CompletedTask; + } + + internal Task HandleReactionRemove(DiscordClient client, MessageReactionRemovedEventArgs eventargs) + { + if (this.requests.Count == 0) + { + return Task.CompletedTask; + } + + _ = Task.Run(async () => + { + foreach (IPaginationRequest req in this.requests) + { + PaginationEmojis emojis = await req.GetEmojisAsync(); + DiscordMessage msg = await req.GetMessageAsync(); + DiscordUser usr = await req.GetUserAsync(); + + if (msg.Id == eventargs.Message.Id) + { + if (eventargs.User.Id == usr.Id) + { + if (req.PageCount > 1 && + (eventargs.Emoji == emojis.Left || + eventargs.Emoji == emojis.SkipLeft || + eventargs.Emoji == emojis.Right || + eventargs.Emoji == emojis.SkipRight || + eventargs.Emoji == emojis.Stop)) + { + await PaginateAsync(req, eventargs.Emoji); + } + else if (eventargs.Emoji == emojis.Stop && + req is PaginationRequest paginationRequest && + paginationRequest.PaginationDeletion == PaginationDeletion.DeleteMessage) + { + await PaginateAsync(req, eventargs.Emoji); + } + } + } + } + }); + + return Task.CompletedTask; + } + + internal Task HandleReactionClear(DiscordClient client, MessageReactionsClearedEventArgs eventargs) + { + if (this.requests.Count == 0) + { + return Task.CompletedTask; + } + + _ = Task.Run(async () => + { + foreach (IPaginationRequest req in this.requests) + { + DiscordMessage msg = await req.GetMessageAsync(); + + if (msg.Id == eventargs.Message.Id) + { + await ResetReactionsAsync(req); + } + } + }); + + return Task.CompletedTask; + } + + private static async Task ResetReactionsAsync(IPaginationRequest p) + { + DiscordMessage msg = await p.GetMessageAsync(); + PaginationEmojis emojis = await p.GetEmojisAsync(); + + // Test permissions to avoid a 403: + // https://totally-not.a-sketchy.site/3pXpRLK.png + // Yes, this is an issue + // No, we should not require people to guarantee MANAGE_MESSAGES + // Need to check following: + // - In guild? + // - If yes, check if have permission + // - If all above fail (DM || guild && no permission), skip this + DiscordChannel? chn = msg.Channel; + DiscordGuild? gld = chn?.Guild; + DiscordMember? mbr = gld?.CurrentMember; + + if (mbr != null /* == is guild and cache is valid */ && chn.PermissionsFor(mbr).HasPermission(DiscordPermission.ManageChannels)) /* == has permissions */ + { + await msg.DeleteAllReactionsAsync("Pagination"); + } + // ENDOF: 403 fix + + if (p.PageCount > 1) + { + if (emojis.SkipLeft != null) + { + await msg.CreateReactionAsync(emojis.SkipLeft); + } + + if (emojis.Left != null) + { + await msg.CreateReactionAsync(emojis.Left); + } + + if (emojis.Right != null) + { + await msg.CreateReactionAsync(emojis.Right); + } + + if (emojis.SkipRight != null) + { + await msg.CreateReactionAsync(emojis.SkipRight); + } + + if (emojis.Stop != null) + { + await msg.CreateReactionAsync(emojis.Stop); + } + } + else if (emojis.Stop != null && p is PaginationRequest paginationRequest && paginationRequest.PaginationDeletion == PaginationDeletion.DeleteMessage) + { + await msg.CreateReactionAsync(emojis.Stop); + } + } + + private static async Task PaginateAsync(IPaginationRequest p, DiscordEmoji emoji) + { + PaginationEmojis emojis = await p.GetEmojisAsync(); + DiscordMessage msg = await p.GetMessageAsync(); + + if (emoji == emojis.SkipLeft) + { + await p.SkipLeftAsync(); + } + else if (emoji == emojis.Left) + { + await p.PreviousPageAsync(); + } + else if (emoji == emojis.Right) + { + await p.NextPageAsync(); + } + else if (emoji == emojis.SkipRight) + { + await p.SkipRightAsync(); + } + else if (emoji == emojis.Stop) + { + TaskCompletionSource tcs = await p.GetTaskCompletionSourceAsync(); + tcs.TrySetResult(true); + return; + } + + Page page = await p.GetPageAsync(); + DiscordMessageBuilder builder = new DiscordMessageBuilder() + .WithContent(page.Content) + .AddEmbed(page.Embed); + + await builder.ModifyAsync(msg); + } + + /// + /// Disposes this EventWaiter + /// + public void Dispose() + { + // Why doesn't this class implement IDisposable? + + this.requests?.Clear(); + this.requests = null!; + } +} diff --git a/DSharpPlus.Interactivity/EventHandling/Poller.cs b/DSharpPlus.Interactivity/EventHandling/Poller.cs index 5dd6ca17b2..db100a85ab 100644 --- a/DSharpPlus.Interactivity/EventHandling/Poller.cs +++ b/DSharpPlus.Interactivity/EventHandling/Poller.cs @@ -1,126 +1,126 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; -using System.Threading.Tasks; -using ConcurrentCollections; -using DSharpPlus.Entities; -using DSharpPlus.EventArgs; -using Microsoft.Extensions.Logging; - -namespace DSharpPlus.Interactivity.EventHandling; - -internal class Poller -{ - private DiscordClient client; - private ConcurrentHashSet requests; - - /// - /// Creates a new Eventwaiter object. - /// - /// Your DiscordClient - public Poller(DiscordClient client) - { - this.client = client; - this.requests = []; - } - - public async Task> DoPollAsync(PollRequest request) - { - ReadOnlyCollection result; - this.requests.Add(request); - try - { - await request.tcs.Task; - } - catch (Exception ex) - { - this.client.Logger.LogError(InteractivityEvents.InteractivityPollError, ex, "Exception occurred while polling"); - } - finally - { - result = new ReadOnlyCollection(new HashSet(request.collected).ToList()); - request.Dispose(); - this.requests.TryRemove(request); - } - return result; - } - - internal Task HandleReactionAdd(DiscordClient client, MessageReactionAddedEventArgs eventargs) - { - if (this.requests.Count == 0) - { - return Task.CompletedTask; - } - - _ = Task.Run(async () => - { - foreach (PollRequest req in this.requests) - { - // match message - if (req.message.Id == eventargs.Message.Id && req.message.ChannelId == eventargs.Channel.Id) - { - if (req.emojis.Contains(eventargs.Emoji) && !req.collected.Any(x => x.Voted.Contains(eventargs.User))) - { - if (eventargs.User.Id != this.client.CurrentUser.Id) - { - req.AddReaction(eventargs.Emoji, eventargs.User); - } - } - else - { - DiscordMember member = await eventargs.Channel.Guild.GetMemberAsync(client.CurrentUser.Id); - if (eventargs.Channel.PermissionsFor(member).HasPermission(DiscordPermission.ManageMessages)) - { - await eventargs.Message.DeleteReactionAsync(eventargs.Emoji, eventargs.User); - } - } - } - } - }); - return Task.CompletedTask; - } - - internal Task HandleReactionRemove(DiscordClient client, MessageReactionRemovedEventArgs eventargs) - { - foreach (PollRequest req in this.requests) - { - // match message - if (req.message.Id == eventargs.Message.Id && req.message.ChannelId == eventargs.Channel.Id) - { - if (eventargs.User.Id != this.client.CurrentUser.Id) - { - req.RemoveReaction(eventargs.Emoji, eventargs.User); - } - } - } - return Task.CompletedTask; - } - - internal Task HandleReactionClear(DiscordClient client, MessageReactionsClearedEventArgs eventargs) - { - foreach (PollRequest req in this.requests) - { - // match message - if (req.message.Id == eventargs.Message.Id && req.message.ChannelId == eventargs.Channel.Id) - { - req.ClearCollected(); - } - } - return Task.CompletedTask; - } - - /// - /// Disposes this EventWaiter - /// - public void Dispose() - { - // Why doesn't this class implement IDisposable? - - if (this.requests != null) - { - this.requests.Clear(); - this.requests = null!; - } - } -} +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Threading.Tasks; +using ConcurrentCollections; +using DSharpPlus.Entities; +using DSharpPlus.EventArgs; +using Microsoft.Extensions.Logging; + +namespace DSharpPlus.Interactivity.EventHandling; + +internal class Poller +{ + private DiscordClient client; + private ConcurrentHashSet requests; + + /// + /// Creates a new Eventwaiter object. + /// + /// Your DiscordClient + public Poller(DiscordClient client) + { + this.client = client; + this.requests = []; + } + + public async Task> DoPollAsync(PollRequest request) + { + ReadOnlyCollection result; + this.requests.Add(request); + try + { + await request.tcs.Task; + } + catch (Exception ex) + { + this.client.Logger.LogError(InteractivityEvents.InteractivityPollError, ex, "Exception occurred while polling"); + } + finally + { + result = new ReadOnlyCollection(new HashSet(request.collected).ToList()); + request.Dispose(); + this.requests.TryRemove(request); + } + return result; + } + + internal Task HandleReactionAdd(DiscordClient client, MessageReactionAddedEventArgs eventargs) + { + if (this.requests.Count == 0) + { + return Task.CompletedTask; + } + + _ = Task.Run(async () => + { + foreach (PollRequest req in this.requests) + { + // match message + if (req.message.Id == eventargs.Message.Id && req.message.ChannelId == eventargs.Channel.Id) + { + if (req.emojis.Contains(eventargs.Emoji) && !req.collected.Any(x => x.Voted.Contains(eventargs.User))) + { + if (eventargs.User.Id != this.client.CurrentUser.Id) + { + req.AddReaction(eventargs.Emoji, eventargs.User); + } + } + else + { + DiscordMember member = await eventargs.Channel.Guild.GetMemberAsync(client.CurrentUser.Id); + if (eventargs.Channel.PermissionsFor(member).HasPermission(DiscordPermission.ManageMessages)) + { + await eventargs.Message.DeleteReactionAsync(eventargs.Emoji, eventargs.User); + } + } + } + } + }); + return Task.CompletedTask; + } + + internal Task HandleReactionRemove(DiscordClient client, MessageReactionRemovedEventArgs eventargs) + { + foreach (PollRequest req in this.requests) + { + // match message + if (req.message.Id == eventargs.Message.Id && req.message.ChannelId == eventargs.Channel.Id) + { + if (eventargs.User.Id != this.client.CurrentUser.Id) + { + req.RemoveReaction(eventargs.Emoji, eventargs.User); + } + } + } + return Task.CompletedTask; + } + + internal Task HandleReactionClear(DiscordClient client, MessageReactionsClearedEventArgs eventargs) + { + foreach (PollRequest req in this.requests) + { + // match message + if (req.message.Id == eventargs.Message.Id && req.message.ChannelId == eventargs.Channel.Id) + { + req.ClearCollected(); + } + } + return Task.CompletedTask; + } + + /// + /// Disposes this EventWaiter + /// + public void Dispose() + { + // Why doesn't this class implement IDisposable? + + if (this.requests != null) + { + this.requests.Clear(); + this.requests = null!; + } + } +} diff --git a/DSharpPlus.Interactivity/EventHandling/ReactionCollector.cs b/DSharpPlus.Interactivity/EventHandling/ReactionCollector.cs index ab37ae7132..18adb55c95 100644 --- a/DSharpPlus.Interactivity/EventHandling/ReactionCollector.cs +++ b/DSharpPlus.Interactivity/EventHandling/ReactionCollector.cs @@ -1,201 +1,201 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -using ConcurrentCollections; - -using DSharpPlus.AsyncEvents; -using DSharpPlus.Entities; -using DSharpPlus.EventArgs; - -using Microsoft.Extensions.Logging; - -namespace DSharpPlus.Interactivity.EventHandling; - -// nice documentation lmfao -/// -/// Eventwaiter is a class that serves as a layer between the InteractivityExtension -/// and the DiscordClient to listen to an event and check for matches to a predicate. -/// -internal class ReactionCollector : IDisposable -{ - private DiscordClient client; - private AsyncEvent reactionAddEvent; - private AsyncEventHandler reactionAddHandler; - private AsyncEvent reactionRemoveEvent; - private AsyncEventHandler reactionRemoveHandler; - private AsyncEvent reactionClearEvent; - private AsyncEventHandler reactionClearHandler; - private ConcurrentHashSet requests; - - /// - /// Creates a new Eventwaiter object. - /// - /// Your DiscordClient - public ReactionCollector(InteractivityExtension extension) - { - this.requests = []; - this.client = extension.Client; - - this.reactionAddEvent = (AsyncEvent)extension.eventDistributor.GetOrAdd - ( - typeof(MessageReactionAddedEventArgs), - new AsyncEvent(extension.errorHandler) - ); - - this.reactionRemoveEvent = (AsyncEvent)extension.eventDistributor.GetOrAdd - ( - typeof(MessageReactionRemovedEventArgs), - new AsyncEvent(extension.errorHandler) - ); - - this.reactionClearEvent = (AsyncEvent)extension.eventDistributor.GetOrAdd - ( - typeof(MessageReactionsClearedEventArgs), - new AsyncEvent(extension.errorHandler) - ); - - // Registering handlers - this.reactionAddHandler = new AsyncEventHandler(HandleReactionAdd); - this.reactionAddEvent.Register(this.reactionAddHandler); - - this.reactionRemoveHandler = new AsyncEventHandler(HandleReactionRemove); - this.reactionRemoveEvent.Register(this.reactionRemoveHandler); - - this.reactionClearHandler = new AsyncEventHandler(HandleReactionClear); - this.reactionClearEvent.Register(this.reactionClearHandler); - } - - public async Task> CollectAsync(ReactionCollectRequest request) - { - this.requests.Add(request); - ReadOnlyCollection? result; - - try - { - await request.tcs.Task; - } - catch (Exception ex) - { - this.client.Logger.LogError(InteractivityEvents.InteractivityCollectorError, ex, "Exception occurred while collecting reactions"); - } - finally - { - result = new ReadOnlyCollection(new HashSet(request.collected).ToList()); - request.Dispose(); - this.requests.TryRemove(request); - } - return result; - } - - private Task HandleReactionAdd(DiscordClient client, MessageReactionAddedEventArgs eventargs) - { - // foreach request add - foreach (ReactionCollectRequest req in this.requests) - { - if (req.message.Id == eventargs.Message.Id) - { - if (req.collected.Any(x => x.Emoji == eventargs.Emoji && x.Users.Any(y => y.Id != eventargs.User.Id))) - { - Reaction reaction = req.collected.First(x => x.Emoji == eventargs.Emoji && x.Users.Any(y => y.Id != eventargs.User.Id)); - req.collected.TryRemove(reaction); - reaction.Users.Add(eventargs.User); - req.collected.Add(reaction); - } - else - { - req.collected.Add(new Reaction() - { - Emoji = eventargs.Emoji, - Users = [eventargs.User] - }); - } - } - } - return Task.CompletedTask; - } - - private Task HandleReactionRemove(DiscordClient client, MessageReactionRemovedEventArgs eventargs) - { - // foreach request remove - foreach (ReactionCollectRequest req in this.requests) - { - if (req.message.Id == eventargs.Message.Id) - { - if (req.collected.Any(x => x.Emoji == eventargs.Emoji && x.Users.Any(y => y.Id == eventargs.User.Id))) - { - Reaction reaction = req.collected.First(x => x.Emoji == eventargs.Emoji && x.Users.Any(y => y.Id == eventargs.User.Id)); - req.collected.TryRemove(reaction); - reaction.Users.TryRemove(eventargs.User); - if (reaction.Users.Count > 0) - { - req.collected.Add(reaction); - } - } - } - } - return Task.CompletedTask; - } - - private Task HandleReactionClear(DiscordClient client, MessageReactionsClearedEventArgs eventargs) - { - // foreach request add - foreach (ReactionCollectRequest req in this.requests) - { - if (req.message.Id == eventargs.Message.Id) - { - req.collected.Clear(); - } - } - return Task.CompletedTask; - } - - /// - /// Disposes this EventWaiter - /// - public void Dispose() - { - this.requests.Clear(); - - // Satisfy rule CA1816. Can be removed if this class is sealed. - GC.SuppressFinalize(this); - } -} - -public class ReactionCollectRequest : IDisposable -{ - internal TaskCompletionSource tcs; - internal CancellationTokenSource ct; - internal TimeSpan timeout; - internal DiscordMessage message; - internal ConcurrentHashSet collected; - - public ReactionCollectRequest(DiscordMessage msg, TimeSpan timeout) - { - this.message = msg; - this.collected = []; - this.timeout = timeout; - this.tcs = new TaskCompletionSource(); - this.ct = new CancellationTokenSource(this.timeout); - this.ct.Token.Register(() => this.tcs.TrySetResult(null)); - } - - public void Dispose() - { - this.ct.Dispose(); - this.collected.Clear(); - - // Satisfy rule CA1816. Can be removed if this class is sealed. - GC.SuppressFinalize(this); - } -} - -public class Reaction -{ - public DiscordEmoji Emoji { get; internal set; } - public ConcurrentHashSet Users { get; internal set; } - public int Total => this.Users.Count; -} +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using ConcurrentCollections; + +using DSharpPlus.AsyncEvents; +using DSharpPlus.Entities; +using DSharpPlus.EventArgs; + +using Microsoft.Extensions.Logging; + +namespace DSharpPlus.Interactivity.EventHandling; + +// nice documentation lmfao +/// +/// Eventwaiter is a class that serves as a layer between the InteractivityExtension +/// and the DiscordClient to listen to an event and check for matches to a predicate. +/// +internal class ReactionCollector : IDisposable +{ + private DiscordClient client; + private AsyncEvent reactionAddEvent; + private AsyncEventHandler reactionAddHandler; + private AsyncEvent reactionRemoveEvent; + private AsyncEventHandler reactionRemoveHandler; + private AsyncEvent reactionClearEvent; + private AsyncEventHandler reactionClearHandler; + private ConcurrentHashSet requests; + + /// + /// Creates a new Eventwaiter object. + /// + /// Your DiscordClient + public ReactionCollector(InteractivityExtension extension) + { + this.requests = []; + this.client = extension.Client; + + this.reactionAddEvent = (AsyncEvent)extension.eventDistributor.GetOrAdd + ( + typeof(MessageReactionAddedEventArgs), + new AsyncEvent(extension.errorHandler) + ); + + this.reactionRemoveEvent = (AsyncEvent)extension.eventDistributor.GetOrAdd + ( + typeof(MessageReactionRemovedEventArgs), + new AsyncEvent(extension.errorHandler) + ); + + this.reactionClearEvent = (AsyncEvent)extension.eventDistributor.GetOrAdd + ( + typeof(MessageReactionsClearedEventArgs), + new AsyncEvent(extension.errorHandler) + ); + + // Registering handlers + this.reactionAddHandler = new AsyncEventHandler(HandleReactionAdd); + this.reactionAddEvent.Register(this.reactionAddHandler); + + this.reactionRemoveHandler = new AsyncEventHandler(HandleReactionRemove); + this.reactionRemoveEvent.Register(this.reactionRemoveHandler); + + this.reactionClearHandler = new AsyncEventHandler(HandleReactionClear); + this.reactionClearEvent.Register(this.reactionClearHandler); + } + + public async Task> CollectAsync(ReactionCollectRequest request) + { + this.requests.Add(request); + ReadOnlyCollection? result; + + try + { + await request.tcs.Task; + } + catch (Exception ex) + { + this.client.Logger.LogError(InteractivityEvents.InteractivityCollectorError, ex, "Exception occurred while collecting reactions"); + } + finally + { + result = new ReadOnlyCollection(new HashSet(request.collected).ToList()); + request.Dispose(); + this.requests.TryRemove(request); + } + return result; + } + + private Task HandleReactionAdd(DiscordClient client, MessageReactionAddedEventArgs eventargs) + { + // foreach request add + foreach (ReactionCollectRequest req in this.requests) + { + if (req.message.Id == eventargs.Message.Id) + { + if (req.collected.Any(x => x.Emoji == eventargs.Emoji && x.Users.Any(y => y.Id != eventargs.User.Id))) + { + Reaction reaction = req.collected.First(x => x.Emoji == eventargs.Emoji && x.Users.Any(y => y.Id != eventargs.User.Id)); + req.collected.TryRemove(reaction); + reaction.Users.Add(eventargs.User); + req.collected.Add(reaction); + } + else + { + req.collected.Add(new Reaction() + { + Emoji = eventargs.Emoji, + Users = [eventargs.User] + }); + } + } + } + return Task.CompletedTask; + } + + private Task HandleReactionRemove(DiscordClient client, MessageReactionRemovedEventArgs eventargs) + { + // foreach request remove + foreach (ReactionCollectRequest req in this.requests) + { + if (req.message.Id == eventargs.Message.Id) + { + if (req.collected.Any(x => x.Emoji == eventargs.Emoji && x.Users.Any(y => y.Id == eventargs.User.Id))) + { + Reaction reaction = req.collected.First(x => x.Emoji == eventargs.Emoji && x.Users.Any(y => y.Id == eventargs.User.Id)); + req.collected.TryRemove(reaction); + reaction.Users.TryRemove(eventargs.User); + if (reaction.Users.Count > 0) + { + req.collected.Add(reaction); + } + } + } + } + return Task.CompletedTask; + } + + private Task HandleReactionClear(DiscordClient client, MessageReactionsClearedEventArgs eventargs) + { + // foreach request add + foreach (ReactionCollectRequest req in this.requests) + { + if (req.message.Id == eventargs.Message.Id) + { + req.collected.Clear(); + } + } + return Task.CompletedTask; + } + + /// + /// Disposes this EventWaiter + /// + public void Dispose() + { + this.requests.Clear(); + + // Satisfy rule CA1816. Can be removed if this class is sealed. + GC.SuppressFinalize(this); + } +} + +public class ReactionCollectRequest : IDisposable +{ + internal TaskCompletionSource tcs; + internal CancellationTokenSource ct; + internal TimeSpan timeout; + internal DiscordMessage message; + internal ConcurrentHashSet collected; + + public ReactionCollectRequest(DiscordMessage msg, TimeSpan timeout) + { + this.message = msg; + this.collected = []; + this.timeout = timeout; + this.tcs = new TaskCompletionSource(); + this.ct = new CancellationTokenSource(this.timeout); + this.ct.Token.Register(() => this.tcs.TrySetResult(null)); + } + + public void Dispose() + { + this.ct.Dispose(); + this.collected.Clear(); + + // Satisfy rule CA1816. Can be removed if this class is sealed. + GC.SuppressFinalize(this); + } +} + +public class Reaction +{ + public DiscordEmoji Emoji { get; internal set; } + public ConcurrentHashSet Users { get; internal set; } + public int Total => this.Users.Count; +} diff --git a/DSharpPlus.Interactivity/EventHandling/Requests/CollectRequest.cs b/DSharpPlus.Interactivity/EventHandling/Requests/CollectRequest.cs index 6d643a1624..8b47ca9288 100644 --- a/DSharpPlus.Interactivity/EventHandling/Requests/CollectRequest.cs +++ b/DSharpPlus.Interactivity/EventHandling/Requests/CollectRequest.cs @@ -1,61 +1,61 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using ConcurrentCollections; -using DSharpPlus.AsyncEvents; - -namespace DSharpPlus.Interactivity.EventHandling; - -/// -/// CollectRequest is a class that serves as a representation of -/// EventArgs that are being collected within a specific time frame. -/// -/// -internal class CollectRequest : IDisposable where T : AsyncEventArgs -{ - internal TaskCompletionSource tcs; - internal CancellationTokenSource ct; - internal Func predicate; - internal TimeSpan timeout; - internal ConcurrentHashSet collected; - - /// - /// Creates a new CollectRequest object. - /// - /// Predicate to match - /// Timeout time - public CollectRequest(Func predicate, TimeSpan timeout) - { - this.tcs = new TaskCompletionSource(); - this.ct = new CancellationTokenSource(timeout); - this.predicate = predicate; - this.ct.Token.Register(() => this.tcs.TrySetResult(true)); - this.timeout = timeout; - this.collected = []; - } - - /// - /// Disposes this CollectRequest. - /// - public void Dispose() - { - this.ct.Dispose(); - this.tcs = null!; - this.predicate = null!; - - if (this.collected != null) - { - this.collected.Clear(); - this.collected = null!; - } - } -} - -/* - ^ ^ -( Quack! )> (ミචᆽචミ) - -(somewhere on twitter I read amazon had a duck -that said meow so I had to add a cat that says quack) - -*/ +using System; +using System.Threading; +using System.Threading.Tasks; +using ConcurrentCollections; +using DSharpPlus.AsyncEvents; + +namespace DSharpPlus.Interactivity.EventHandling; + +/// +/// CollectRequest is a class that serves as a representation of +/// EventArgs that are being collected within a specific time frame. +/// +/// +internal class CollectRequest : IDisposable where T : AsyncEventArgs +{ + internal TaskCompletionSource tcs; + internal CancellationTokenSource ct; + internal Func predicate; + internal TimeSpan timeout; + internal ConcurrentHashSet collected; + + /// + /// Creates a new CollectRequest object. + /// + /// Predicate to match + /// Timeout time + public CollectRequest(Func predicate, TimeSpan timeout) + { + this.tcs = new TaskCompletionSource(); + this.ct = new CancellationTokenSource(timeout); + this.predicate = predicate; + this.ct.Token.Register(() => this.tcs.TrySetResult(true)); + this.timeout = timeout; + this.collected = []; + } + + /// + /// Disposes this CollectRequest. + /// + public void Dispose() + { + this.ct.Dispose(); + this.tcs = null!; + this.predicate = null!; + + if (this.collected != null) + { + this.collected.Clear(); + this.collected = null!; + } + } +} + +/* + ^ ^ +( Quack! )> (ミචᆽචミ) + +(somewhere on twitter I read amazon had a duck +that said meow so I had to add a cat that says quack) + +*/ diff --git a/DSharpPlus.Interactivity/EventHandling/Requests/IPaginationRequest.cs b/DSharpPlus.Interactivity/EventHandling/Requests/IPaginationRequest.cs index 16ec22a7d2..568eb07f0a 100644 --- a/DSharpPlus.Interactivity/EventHandling/Requests/IPaginationRequest.cs +++ b/DSharpPlus.Interactivity/EventHandling/Requests/IPaginationRequest.cs @@ -1,80 +1,80 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using DSharpPlus.Entities; - -namespace DSharpPlus.Interactivity.EventHandling; - -public interface IPaginationRequest -{ - /// - /// Returns the number of pages. - /// - /// - public int PageCount { get; } - - /// - /// Returns the current page. - /// - /// - public Task GetPageAsync(); - - /// - /// Tells the request to set its index to the first page. - /// - /// - public Task SkipLeftAsync(); - - /// - /// Tells the request to set its index to the last page. - /// - /// - public Task SkipRightAsync(); - - /// - /// Tells the request to increase its index by one. - /// - /// - public Task NextPageAsync(); - - /// - /// Tells the request to decrease its index by one. - /// - /// - public Task PreviousPageAsync(); - - /// - /// Requests message emojis from pagination request. - /// - /// - public Task GetEmojisAsync(); - - /// - /// Requests the message buttons from the pagination request. - /// - /// The buttons. - public Task> GetButtonsAsync(); - - /// - /// Gets pagination message from this request. - /// - /// - public Task GetMessageAsync(); - - /// - /// Gets the user this pagination applies to. - /// - /// - public Task GetUserAsync(); - - /// - /// Get this request's Task Completion Source. - /// - /// - public Task> GetTaskCompletionSourceAsync(); - - /// - /// Tells the request to perform cleanup. - /// - /// - public Task DoCleanupAsync(); -} +using System.Collections.Generic; +using System.Threading.Tasks; +using DSharpPlus.Entities; + +namespace DSharpPlus.Interactivity.EventHandling; + +public interface IPaginationRequest +{ + /// + /// Returns the number of pages. + /// + /// + public int PageCount { get; } + + /// + /// Returns the current page. + /// + /// + public Task GetPageAsync(); + + /// + /// Tells the request to set its index to the first page. + /// + /// + public Task SkipLeftAsync(); + + /// + /// Tells the request to set its index to the last page. + /// + /// + public Task SkipRightAsync(); + + /// + /// Tells the request to increase its index by one. + /// + /// + public Task NextPageAsync(); + + /// + /// Tells the request to decrease its index by one. + /// + /// + public Task PreviousPageAsync(); + + /// + /// Requests message emojis from pagination request. + /// + /// + public Task GetEmojisAsync(); + + /// + /// Requests the message buttons from the pagination request. + /// + /// The buttons. + public Task> GetButtonsAsync(); + + /// + /// Gets pagination message from this request. + /// + /// + public Task GetMessageAsync(); + + /// + /// Gets the user this pagination applies to. + /// + /// + public Task GetUserAsync(); + + /// + /// Get this request's Task Completion Source. + /// + /// + public Task> GetTaskCompletionSourceAsync(); + + /// + /// Tells the request to perform cleanup. + /// + /// + public Task DoCleanupAsync(); +} diff --git a/DSharpPlus.Interactivity/EventHandling/Requests/MatchRequest.cs b/DSharpPlus.Interactivity/EventHandling/Requests/MatchRequest.cs index 98e8ad355b..a3d339a6e5 100644 --- a/DSharpPlus.Interactivity/EventHandling/Requests/MatchRequest.cs +++ b/DSharpPlus.Interactivity/EventHandling/Requests/MatchRequest.cs @@ -1,46 +1,46 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using DSharpPlus.AsyncEvents; - -namespace DSharpPlus.Interactivity.EventHandling; - -/// -/// MatchRequest is a class that serves as a representation of a -/// match that is being waited for. -/// -/// -internal class MatchRequest : IDisposable where T : AsyncEventArgs -{ - internal TaskCompletionSource tcs; - internal CancellationTokenSource ct; - internal Func predicate; - internal TimeSpan timeout; - - /// - /// Creates a new MatchRequest object. - /// - /// Predicate to match - /// Timeout time - public MatchRequest(Func predicate, TimeSpan timeout) - { - this.tcs = new TaskCompletionSource(); - this.ct = new CancellationTokenSource(timeout); - this.predicate = predicate; - this.ct.Token.Register(() => this.tcs.TrySetResult(null)); - this.timeout = timeout; - } - - /// - /// Disposes this MatchRequest. - /// - public void Dispose() - { - this.ct?.Dispose(); - this.tcs = null!; - this.predicate = null!; - - // Satisfy rule CA1816. Can be removed if this class is sealed. - GC.SuppressFinalize(this); - } -} +using System; +using System.Threading; +using System.Threading.Tasks; +using DSharpPlus.AsyncEvents; + +namespace DSharpPlus.Interactivity.EventHandling; + +/// +/// MatchRequest is a class that serves as a representation of a +/// match that is being waited for. +/// +/// +internal class MatchRequest : IDisposable where T : AsyncEventArgs +{ + internal TaskCompletionSource tcs; + internal CancellationTokenSource ct; + internal Func predicate; + internal TimeSpan timeout; + + /// + /// Creates a new MatchRequest object. + /// + /// Predicate to match + /// Timeout time + public MatchRequest(Func predicate, TimeSpan timeout) + { + this.tcs = new TaskCompletionSource(); + this.ct = new CancellationTokenSource(timeout); + this.predicate = predicate; + this.ct.Token.Register(() => this.tcs.TrySetResult(null)); + this.timeout = timeout; + } + + /// + /// Disposes this MatchRequest. + /// + public void Dispose() + { + this.ct?.Dispose(); + this.tcs = null!; + this.predicate = null!; + + // Satisfy rule CA1816. Can be removed if this class is sealed. + GC.SuppressFinalize(this); + } +} diff --git a/DSharpPlus.Interactivity/EventHandling/Requests/ModalMatchRequest.cs b/DSharpPlus.Interactivity/EventHandling/Requests/ModalMatchRequest.cs index 4bd2cfaa3c..988aae1da4 100644 --- a/DSharpPlus.Interactivity/EventHandling/Requests/ModalMatchRequest.cs +++ b/DSharpPlus.Interactivity/EventHandling/Requests/ModalMatchRequest.cs @@ -1,45 +1,45 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using DSharpPlus.EventArgs; - -namespace DSharpPlus.Interactivity.EventHandling; - -/// -/// Represents a match request for a modal of the given Id and predicate. -/// -internal class ModalMatchRequest -{ - /// - /// The custom Id of the modal. - /// - public string ModalId { get; } - - /// - /// The completion source that represents the result of the match. - /// - public TaskCompletionSource Tcs { get; private set; } = new(); - - protected CancellationToken Cancellation { get; } - - /// - /// The predicate/criteria that this match will be fulfilled under. - /// - protected Func Predicate { get; } - - public ModalMatchRequest(string modal_id, Func predicate, CancellationToken cancellation) - { - this.ModalId = modal_id; - this.Predicate = predicate; - this.Cancellation = cancellation; - this.Cancellation.Register(() => this.Tcs.TrySetResult(null)); // "TrySetCancelled would probably be better but I digress" - Velvet // "TrySetCancelled throws an exception when you await the task, actually" - Velvet, 2022 - } - - /// - /// Checks whether the matches the predicate criteria. - /// - /// The to check. - /// Whether the matches the predicate. - public bool IsMatch(ModalSubmittedEventArgs args) - => this.Predicate(args); -} +using System; +using System.Threading; +using System.Threading.Tasks; +using DSharpPlus.EventArgs; + +namespace DSharpPlus.Interactivity.EventHandling; + +/// +/// Represents a match request for a modal of the given Id and predicate. +/// +internal class ModalMatchRequest +{ + /// + /// The custom Id of the modal. + /// + public string ModalId { get; } + + /// + /// The completion source that represents the result of the match. + /// + public TaskCompletionSource Tcs { get; private set; } = new(); + + protected CancellationToken Cancellation { get; } + + /// + /// The predicate/criteria that this match will be fulfilled under. + /// + protected Func Predicate { get; } + + public ModalMatchRequest(string modal_id, Func predicate, CancellationToken cancellation) + { + this.ModalId = modal_id; + this.Predicate = predicate; + this.Cancellation = cancellation; + this.Cancellation.Register(() => this.Tcs.TrySetResult(null)); // "TrySetCancelled would probably be better but I digress" - Velvet // "TrySetCancelled throws an exception when you await the task, actually" - Velvet, 2022 + } + + /// + /// Checks whether the matches the predicate criteria. + /// + /// The to check. + /// Whether the matches the predicate. + public bool IsMatch(ModalSubmittedEventArgs args) + => this.Predicate(args); +} diff --git a/DSharpPlus.Interactivity/EventHandling/Requests/PaginationEmojis.cs b/DSharpPlus.Interactivity/EventHandling/Requests/PaginationEmojis.cs index f459dfa807..1f09e2a67d 100644 --- a/DSharpPlus.Interactivity/EventHandling/Requests/PaginationEmojis.cs +++ b/DSharpPlus.Interactivity/EventHandling/Requests/PaginationEmojis.cs @@ -1,85 +1,85 @@ -using System; -using System.Collections.Generic; -using DSharpPlus.Entities; - -namespace DSharpPlus.Interactivity; - -public class PaginationEmojis -{ - public DiscordEmoji SkipLeft; - public DiscordEmoji SkipRight; - public DiscordEmoji Left; - public DiscordEmoji Right; - public DiscordEmoji Stop; - - public PaginationEmojis() - { - this.Left = DiscordEmoji.FromUnicode("◀"); - this.Right = DiscordEmoji.FromUnicode("▶"); - this.SkipLeft = DiscordEmoji.FromUnicode("⏮"); - this.SkipRight = DiscordEmoji.FromUnicode("⏭"); - this.Stop = DiscordEmoji.FromUnicode("⏹"); - } -} - -public class Page -{ - public string Content { get; set; } - public DiscordEmbed Embed { get; set; } - - public IReadOnlyList Components { get; } - - public Page(string content = "", DiscordEmbed? embed = null, IReadOnlyList components = null) - { - this.Content = content; - this.Embed = embed; - - if (components is null or []) - { - this.Components = []; - - return; - } - - if (components[0] is DiscordActionRowComponent arc) - { - if (components.Count > 4) - { - throw new ArgumentException("Pages can only contain four rows of components"); - } - - this.Components = [arc]; - } - else - { - List componentRows = []; - List currentRow = new(5); - - foreach (DiscordComponent component in components) - { - if (component is BaseDiscordSelectComponent) - { - componentRows.Add(new([component])); - - continue; - } - - if (currentRow.Count == 5) - { - componentRows.Add(new DiscordActionRowComponent(currentRow)); - currentRow = new List(5); - } - - currentRow.Add(component); - } - - if (currentRow.Count > 0) - { - componentRows.Add(new DiscordActionRowComponent(currentRow)); - } - - this.Components = componentRows; - - } - } -} +using System; +using System.Collections.Generic; +using DSharpPlus.Entities; + +namespace DSharpPlus.Interactivity; + +public class PaginationEmojis +{ + public DiscordEmoji SkipLeft; + public DiscordEmoji SkipRight; + public DiscordEmoji Left; + public DiscordEmoji Right; + public DiscordEmoji Stop; + + public PaginationEmojis() + { + this.Left = DiscordEmoji.FromUnicode("◀"); + this.Right = DiscordEmoji.FromUnicode("▶"); + this.SkipLeft = DiscordEmoji.FromUnicode("⏮"); + this.SkipRight = DiscordEmoji.FromUnicode("⏭"); + this.Stop = DiscordEmoji.FromUnicode("⏹"); + } +} + +public class Page +{ + public string Content { get; set; } + public DiscordEmbed Embed { get; set; } + + public IReadOnlyList Components { get; } + + public Page(string content = "", DiscordEmbed? embed = null, IReadOnlyList components = null) + { + this.Content = content; + this.Embed = embed; + + if (components is null or []) + { + this.Components = []; + + return; + } + + if (components[0] is DiscordActionRowComponent arc) + { + if (components.Count > 4) + { + throw new ArgumentException("Pages can only contain four rows of components"); + } + + this.Components = [arc]; + } + else + { + List componentRows = []; + List currentRow = new(5); + + foreach (DiscordComponent component in components) + { + if (component is BaseDiscordSelectComponent) + { + componentRows.Add(new([component])); + + continue; + } + + if (currentRow.Count == 5) + { + componentRows.Add(new DiscordActionRowComponent(currentRow)); + currentRow = new List(5); + } + + currentRow.Add(component); + } + + if (currentRow.Count > 0) + { + componentRows.Add(new DiscordActionRowComponent(currentRow)); + } + + this.Components = componentRows; + + } + } +} diff --git a/DSharpPlus.Interactivity/EventHandling/Requests/PaginationRequest.cs b/DSharpPlus.Interactivity/EventHandling/Requests/PaginationRequest.cs index f277118947..cc03984ee1 100644 --- a/DSharpPlus.Interactivity/EventHandling/Requests/PaginationRequest.cs +++ b/DSharpPlus.Interactivity/EventHandling/Requests/PaginationRequest.cs @@ -1,195 +1,195 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using DSharpPlus.Entities; -using DSharpPlus.Interactivity.Enums; - -namespace DSharpPlus.Interactivity.EventHandling; - -internal class PaginationRequest : IPaginationRequest -{ - private TaskCompletionSource tcs; - private readonly CancellationTokenSource ct; - private readonly List pages; - private readonly PaginationBehaviour behaviour; - private readonly DiscordMessage message; - private readonly PaginationEmojis emojis; - private readonly DiscordUser user; - private int index = 0; - - /// - /// Creates a new Pagination request - /// - /// Message to paginate - /// User to allow control for - /// Behaviour during pagination - /// Behavior on pagination end - /// Emojis for this pagination object - /// Timeout time - /// Pagination pages - internal PaginationRequest(DiscordMessage message, DiscordUser user, PaginationBehaviour behaviour, PaginationDeletion deletion, - PaginationEmojis emojis, TimeSpan timeout, params Page[] pages) - { - this.tcs = new(); - this.ct = new(timeout); - this.ct.Token.Register(() => this.tcs.TrySetResult(true)); - - this.message = message; - this.user = user; - - this.PaginationDeletion = deletion; - this.behaviour = behaviour; - this.emojis = emojis; - - this.pages = [.. pages]; - } - - public int PageCount => this.pages.Count; - - public PaginationDeletion PaginationDeletion { get; } - - public async Task GetPageAsync() - { - await Task.Yield(); - - return this.pages[this.index]; - } - - public async Task SkipLeftAsync() - { - await Task.Yield(); - - this.index = 0; - } - - public async Task SkipRightAsync() - { - await Task.Yield(); - - this.index = this.pages.Count - 1; - } - - public async Task NextPageAsync() - { - await Task.Yield(); - - switch (this.behaviour) - { - case PaginationBehaviour.Ignore: - if (this.index == this.pages.Count - 1) - { - break; - } - else - { - this.index++; - } - - break; - - case PaginationBehaviour.WrapAround: - if (this.index == this.pages.Count - 1) - { - this.index = 0; - } - else - { - this.index++; - } - - break; - } - } - - public async Task PreviousPageAsync() - { - await Task.Yield(); - - switch (this.behaviour) - { - case PaginationBehaviour.Ignore: - if (this.index == 0) - { - break; - } - else - { - this.index--; - } - - break; - - case PaginationBehaviour.WrapAround: - if (this.index == 0) - { - this.index = this.pages.Count - 1; - } - else - { - this.index--; - } - - break; - } - } - - public async Task GetEmojisAsync() - { - await Task.Yield(); - - return this.emojis; - } - - public Task> GetButtonsAsync() - => throw new NotSupportedException("This request does not support buttons."); - - public async Task GetMessageAsync() - { - await Task.Yield(); - - return this.message; - } - - public async Task GetUserAsync() - { - await Task.Yield(); - - return this.user; - } - - public async Task DoCleanupAsync() - { - switch (this.PaginationDeletion) - { - case PaginationDeletion.DeleteEmojis: - await this.message.DeleteAllReactionsAsync(); - break; - - case PaginationDeletion.DeleteMessage: - await this.message.DeleteAsync(); - break; - - case PaginationDeletion.KeepEmojis: - break; - } - } - - public async Task> GetTaskCompletionSourceAsync() - { - await Task.Yield(); - - return this.tcs; - } - - /// - /// Disposes this PaginationRequest. - /// - public void Dispose() - { - // Why doesn't this class implement IDisposable? - - this.ct?.Dispose(); - this.tcs = null!; - } -} +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using DSharpPlus.Entities; +using DSharpPlus.Interactivity.Enums; + +namespace DSharpPlus.Interactivity.EventHandling; + +internal class PaginationRequest : IPaginationRequest +{ + private TaskCompletionSource tcs; + private readonly CancellationTokenSource ct; + private readonly List pages; + private readonly PaginationBehaviour behaviour; + private readonly DiscordMessage message; + private readonly PaginationEmojis emojis; + private readonly DiscordUser user; + private int index = 0; + + /// + /// Creates a new Pagination request + /// + /// Message to paginate + /// User to allow control for + /// Behaviour during pagination + /// Behavior on pagination end + /// Emojis for this pagination object + /// Timeout time + /// Pagination pages + internal PaginationRequest(DiscordMessage message, DiscordUser user, PaginationBehaviour behaviour, PaginationDeletion deletion, + PaginationEmojis emojis, TimeSpan timeout, params Page[] pages) + { + this.tcs = new(); + this.ct = new(timeout); + this.ct.Token.Register(() => this.tcs.TrySetResult(true)); + + this.message = message; + this.user = user; + + this.PaginationDeletion = deletion; + this.behaviour = behaviour; + this.emojis = emojis; + + this.pages = [.. pages]; + } + + public int PageCount => this.pages.Count; + + public PaginationDeletion PaginationDeletion { get; } + + public async Task GetPageAsync() + { + await Task.Yield(); + + return this.pages[this.index]; + } + + public async Task SkipLeftAsync() + { + await Task.Yield(); + + this.index = 0; + } + + public async Task SkipRightAsync() + { + await Task.Yield(); + + this.index = this.pages.Count - 1; + } + + public async Task NextPageAsync() + { + await Task.Yield(); + + switch (this.behaviour) + { + case PaginationBehaviour.Ignore: + if (this.index == this.pages.Count - 1) + { + break; + } + else + { + this.index++; + } + + break; + + case PaginationBehaviour.WrapAround: + if (this.index == this.pages.Count - 1) + { + this.index = 0; + } + else + { + this.index++; + } + + break; + } + } + + public async Task PreviousPageAsync() + { + await Task.Yield(); + + switch (this.behaviour) + { + case PaginationBehaviour.Ignore: + if (this.index == 0) + { + break; + } + else + { + this.index--; + } + + break; + + case PaginationBehaviour.WrapAround: + if (this.index == 0) + { + this.index = this.pages.Count - 1; + } + else + { + this.index--; + } + + break; + } + } + + public async Task GetEmojisAsync() + { + await Task.Yield(); + + return this.emojis; + } + + public Task> GetButtonsAsync() + => throw new NotSupportedException("This request does not support buttons."); + + public async Task GetMessageAsync() + { + await Task.Yield(); + + return this.message; + } + + public async Task GetUserAsync() + { + await Task.Yield(); + + return this.user; + } + + public async Task DoCleanupAsync() + { + switch (this.PaginationDeletion) + { + case PaginationDeletion.DeleteEmojis: + await this.message.DeleteAllReactionsAsync(); + break; + + case PaginationDeletion.DeleteMessage: + await this.message.DeleteAsync(); + break; + + case PaginationDeletion.KeepEmojis: + break; + } + } + + public async Task> GetTaskCompletionSourceAsync() + { + await Task.Yield(); + + return this.tcs; + } + + /// + /// Disposes this PaginationRequest. + /// + public void Dispose() + { + // Why doesn't this class implement IDisposable? + + this.ct?.Dispose(); + this.tcs = null!; + } +} diff --git a/DSharpPlus.Interactivity/EventHandling/Requests/PollRequest.cs b/DSharpPlus.Interactivity/EventHandling/Requests/PollRequest.cs index 32ca845135..6766519fd2 100644 --- a/DSharpPlus.Interactivity/EventHandling/Requests/PollRequest.cs +++ b/DSharpPlus.Interactivity/EventHandling/Requests/PollRequest.cs @@ -1,102 +1,102 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using ConcurrentCollections; -using DSharpPlus.Entities; - -namespace DSharpPlus.Interactivity.EventHandling; - -public class PollRequest -{ - internal TaskCompletionSource tcs; - internal CancellationTokenSource ct; - internal TimeSpan timeout; - internal ConcurrentHashSet collected; - internal DiscordMessage message; - internal List emojis; - - /// - /// - /// - /// - /// - /// - public PollRequest(DiscordMessage message, TimeSpan timeout, IEnumerable emojis) - { - this.tcs = new TaskCompletionSource(); - this.ct = new CancellationTokenSource(timeout); - this.ct.Token.Register(() => this.tcs.TrySetResult(true)); - this.timeout = timeout; - this.emojis = [..emojis]; - this.collected = []; - this.message = message; - - foreach (DiscordEmoji e in this.emojis) - { - this.collected.Add(new PollEmoji(e)); - } - } - - internal void ClearCollected() - { - this.collected.Clear(); - foreach (DiscordEmoji e in this.emojis) - { - this.collected.Add(new PollEmoji(e)); - } - } - - internal void RemoveReaction(DiscordEmoji emoji, DiscordUser member) - { - if (this.collected.Any(x => x.Emoji == emoji)) - { - if (this.collected.Any(x => x.Voted.Contains(member))) - { - PollEmoji e = this.collected.First(x => x.Emoji == emoji); - this.collected.TryRemove(e); - e.Voted.TryRemove(member); - this.collected.Add(e); - } - } - } - - internal void AddReaction(DiscordEmoji emoji, DiscordUser member) - { - if (this.collected.Any(x => x.Emoji == emoji)) - { - if (!this.collected.Any(x => x.Voted.Contains(member))) - { - PollEmoji e = this.collected.First(x => x.Emoji == emoji); - this.collected.TryRemove(e); - e.Voted.Add(member); - this.collected.Add(e); - } - } - } - - /// - /// Disposes this PollRequest. - /// - public void Dispose() - { - // Why doesn't this class implement IDisposable? - - this.ct?.Dispose(); - this.tcs = null!; - } -} - -public class PollEmoji -{ - internal PollEmoji(DiscordEmoji emoji) - { - this.Emoji = emoji; - this.Voted = []; - } - - public DiscordEmoji Emoji; - public ConcurrentHashSet Voted; - public int Total => this.Voted.Count; -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using ConcurrentCollections; +using DSharpPlus.Entities; + +namespace DSharpPlus.Interactivity.EventHandling; + +public class PollRequest +{ + internal TaskCompletionSource tcs; + internal CancellationTokenSource ct; + internal TimeSpan timeout; + internal ConcurrentHashSet collected; + internal DiscordMessage message; + internal List emojis; + + /// + /// + /// + /// + /// + /// + public PollRequest(DiscordMessage message, TimeSpan timeout, IEnumerable emojis) + { + this.tcs = new TaskCompletionSource(); + this.ct = new CancellationTokenSource(timeout); + this.ct.Token.Register(() => this.tcs.TrySetResult(true)); + this.timeout = timeout; + this.emojis = [..emojis]; + this.collected = []; + this.message = message; + + foreach (DiscordEmoji e in this.emojis) + { + this.collected.Add(new PollEmoji(e)); + } + } + + internal void ClearCollected() + { + this.collected.Clear(); + foreach (DiscordEmoji e in this.emojis) + { + this.collected.Add(new PollEmoji(e)); + } + } + + internal void RemoveReaction(DiscordEmoji emoji, DiscordUser member) + { + if (this.collected.Any(x => x.Emoji == emoji)) + { + if (this.collected.Any(x => x.Voted.Contains(member))) + { + PollEmoji e = this.collected.First(x => x.Emoji == emoji); + this.collected.TryRemove(e); + e.Voted.TryRemove(member); + this.collected.Add(e); + } + } + } + + internal void AddReaction(DiscordEmoji emoji, DiscordUser member) + { + if (this.collected.Any(x => x.Emoji == emoji)) + { + if (!this.collected.Any(x => x.Voted.Contains(member))) + { + PollEmoji e = this.collected.First(x => x.Emoji == emoji); + this.collected.TryRemove(e); + e.Voted.Add(member); + this.collected.Add(e); + } + } + } + + /// + /// Disposes this PollRequest. + /// + public void Dispose() + { + // Why doesn't this class implement IDisposable? + + this.ct?.Dispose(); + this.tcs = null!; + } +} + +public class PollEmoji +{ + internal PollEmoji(DiscordEmoji emoji) + { + this.Emoji = emoji; + this.Voted = []; + } + + public DiscordEmoji Emoji; + public ConcurrentHashSet Voted; + public int Total => this.Voted.Count; +} diff --git a/DSharpPlus.Interactivity/Extensions/ChannelExtensions.cs b/DSharpPlus.Interactivity/Extensions/ChannelExtensions.cs index d93463d09d..042f8b2cc7 100644 --- a/DSharpPlus.Interactivity/Extensions/ChannelExtensions.cs +++ b/DSharpPlus.Interactivity/Extensions/ChannelExtensions.cs @@ -1,116 +1,116 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using DSharpPlus.Entities; -using DSharpPlus.EventArgs; -using DSharpPlus.Interactivity.Enums; -using DSharpPlus.Interactivity.EventHandling; - -namespace DSharpPlus.Interactivity.Extensions; - -/// -/// Interactivity extension methods for . -/// -public static class ChannelExtensions -{ - /// - /// Waits for the next message sent in this channel that satisfies the predicate. - /// - /// The channel to monitor. - /// A predicate that should return if a message matches. - /// Overrides the timeout set in - /// Thrown if interactivity is not enabled for the client associated with the channel. - public static Task> GetNextMessageAsync(this DiscordChannel channel, Func predicate, TimeSpan? timeoutOverride = null) - => GetInteractivity(channel).WaitForMessageAsync(msg => msg.ChannelId == channel.Id && predicate(msg), timeoutOverride); - - /// - /// Waits for the next message sent in this channel. - /// - /// The channel to monitor. - /// Overrides the timeout set in - /// Thrown if interactivity is not enabled for the client associated with the channel. - public static Task> GetNextMessageAsync(this DiscordChannel channel, TimeSpan? timeoutOverride = null) - => channel.GetNextMessageAsync(_ => true, timeoutOverride); - - /// - /// Waits for the next message sent in this channel from a specific user. - /// - /// The channel to monitor. - /// The target user. - /// Overrides the timeout set in - /// Thrown if interactivity is not enabled for the client associated with the channel. - public static Task> GetNextMessageAsync(this DiscordChannel channel, DiscordUser user, TimeSpan? timeoutOverride = null) - => channel.GetNextMessageAsync(msg => msg.Author.Id == user.Id, timeoutOverride); - - /// - /// Waits for a specific user to start typing in this channel. - /// - /// The target channel. - /// The target user. - /// Overrides the timeout set in - /// Thrown if interactivity is not enabled for the client associated with the channel. - public static Task> WaitForUserTypingAsync(this DiscordChannel channel, DiscordUser user, TimeSpan? timeoutOverride = null) - => GetInteractivity(channel).WaitForUserTypingAsync(user, channel, timeoutOverride); - - /// - /// Sends a new paginated message. - /// - /// Target channel. - /// The user that'll be able to control the pages. - /// A collection of to display. - /// Pagination emojis. - /// Pagination behaviour (when hitting max and min indices). - /// Deletion behaviour. - /// Override timeout period. - /// Thrown if interactivity is not enabled for the client associated with the channel. - public static Task SendPaginatedMessageAsync(this DiscordChannel channel, DiscordUser user, IEnumerable pages, PaginationEmojis emojis, PaginationBehaviour? behaviour = default, PaginationDeletion? deletion = default, TimeSpan? timeoutoverride = null) - => GetInteractivity(channel).SendPaginatedMessageAsync(channel, user, pages, emojis, behaviour, deletion, timeoutoverride); - - /// - /// Sends a new paginated message with buttons. - /// - /// Target channel. - /// The user that'll be able to control the pages. - /// A collection of to display. - /// Pagination buttons (leave null to default to ones on configuration). - /// Pagination behaviour. - /// Deletion behaviour - /// A custom cancellation token that can be cancelled at any point. - /// Thrown if interactivity is not enabled for the client associated with the channel. - public static Task SendPaginatedMessageAsync(this DiscordChannel channel, DiscordUser user, IEnumerable pages, PaginationButtons buttons, PaginationBehaviour? behaviour = default, ButtonPaginationBehavior? deletion = default, CancellationToken token = default) - => GetInteractivity(channel).SendPaginatedMessageAsync(channel, user, pages, buttons, behaviour, deletion, token); - - /// - public static Task SendPaginatedMessageAsync(this DiscordChannel channel, DiscordUser user, IEnumerable pages, PaginationBehaviour? behaviour = default, ButtonPaginationBehavior? deletion = default, CancellationToken token = default) - => channel.SendPaginatedMessageAsync(user, pages, default, behaviour, deletion, token); - - /// - /// Sends a new paginated message with buttons. - /// - /// Target channel. - /// The user that'll be able to control the pages. - /// A collection of to display. - /// Pagination buttons (leave null to default to ones on configuration). - /// Pagination behaviour. - /// Deletion behaviour - /// Override timeout period. - /// Thrown if interactivity is not enabled for the client associated with the channel. - public static Task SendPaginatedMessageAsync(this DiscordChannel channel, DiscordUser user, IEnumerable pages, PaginationButtons buttons, TimeSpan? timeoutoverride, PaginationBehaviour? behaviour = default, ButtonPaginationBehavior? deletion = default) - => GetInteractivity(channel).SendPaginatedMessageAsync(channel, user, pages, buttons, timeoutoverride, behaviour, deletion); - - /// - public static Task SendPaginatedMessageAsync(this DiscordChannel channel, DiscordUser user, IEnumerable pages, TimeSpan? timeoutoverride, PaginationBehaviour? behaviour = default, ButtonPaginationBehavior? deletion = default) - => channel.SendPaginatedMessageAsync(user, pages, default, timeoutoverride, behaviour, deletion); - - /// - /// Retrieves an interactivity instance from a channel instance. - /// - internal static InteractivityExtension GetInteractivity(DiscordChannel channel) - { - DiscordClient client = (DiscordClient)channel.Discord; - InteractivityExtension interactivity = client.GetInteractivity(); - - return interactivity ?? throw new InvalidOperationException($"Interactivity is not enabled for this {(client.isShard ? "shard" : "client")}."); - } -} +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using DSharpPlus.Entities; +using DSharpPlus.EventArgs; +using DSharpPlus.Interactivity.Enums; +using DSharpPlus.Interactivity.EventHandling; + +namespace DSharpPlus.Interactivity.Extensions; + +/// +/// Interactivity extension methods for . +/// +public static class ChannelExtensions +{ + /// + /// Waits for the next message sent in this channel that satisfies the predicate. + /// + /// The channel to monitor. + /// A predicate that should return if a message matches. + /// Overrides the timeout set in + /// Thrown if interactivity is not enabled for the client associated with the channel. + public static Task> GetNextMessageAsync(this DiscordChannel channel, Func predicate, TimeSpan? timeoutOverride = null) + => GetInteractivity(channel).WaitForMessageAsync(msg => msg.ChannelId == channel.Id && predicate(msg), timeoutOverride); + + /// + /// Waits for the next message sent in this channel. + /// + /// The channel to monitor. + /// Overrides the timeout set in + /// Thrown if interactivity is not enabled for the client associated with the channel. + public static Task> GetNextMessageAsync(this DiscordChannel channel, TimeSpan? timeoutOverride = null) + => channel.GetNextMessageAsync(_ => true, timeoutOverride); + + /// + /// Waits for the next message sent in this channel from a specific user. + /// + /// The channel to monitor. + /// The target user. + /// Overrides the timeout set in + /// Thrown if interactivity is not enabled for the client associated with the channel. + public static Task> GetNextMessageAsync(this DiscordChannel channel, DiscordUser user, TimeSpan? timeoutOverride = null) + => channel.GetNextMessageAsync(msg => msg.Author.Id == user.Id, timeoutOverride); + + /// + /// Waits for a specific user to start typing in this channel. + /// + /// The target channel. + /// The target user. + /// Overrides the timeout set in + /// Thrown if interactivity is not enabled for the client associated with the channel. + public static Task> WaitForUserTypingAsync(this DiscordChannel channel, DiscordUser user, TimeSpan? timeoutOverride = null) + => GetInteractivity(channel).WaitForUserTypingAsync(user, channel, timeoutOverride); + + /// + /// Sends a new paginated message. + /// + /// Target channel. + /// The user that'll be able to control the pages. + /// A collection of to display. + /// Pagination emojis. + /// Pagination behaviour (when hitting max and min indices). + /// Deletion behaviour. + /// Override timeout period. + /// Thrown if interactivity is not enabled for the client associated with the channel. + public static Task SendPaginatedMessageAsync(this DiscordChannel channel, DiscordUser user, IEnumerable pages, PaginationEmojis emojis, PaginationBehaviour? behaviour = default, PaginationDeletion? deletion = default, TimeSpan? timeoutoverride = null) + => GetInteractivity(channel).SendPaginatedMessageAsync(channel, user, pages, emojis, behaviour, deletion, timeoutoverride); + + /// + /// Sends a new paginated message with buttons. + /// + /// Target channel. + /// The user that'll be able to control the pages. + /// A collection of to display. + /// Pagination buttons (leave null to default to ones on configuration). + /// Pagination behaviour. + /// Deletion behaviour + /// A custom cancellation token that can be cancelled at any point. + /// Thrown if interactivity is not enabled for the client associated with the channel. + public static Task SendPaginatedMessageAsync(this DiscordChannel channel, DiscordUser user, IEnumerable pages, PaginationButtons buttons, PaginationBehaviour? behaviour = default, ButtonPaginationBehavior? deletion = default, CancellationToken token = default) + => GetInteractivity(channel).SendPaginatedMessageAsync(channel, user, pages, buttons, behaviour, deletion, token); + + /// + public static Task SendPaginatedMessageAsync(this DiscordChannel channel, DiscordUser user, IEnumerable pages, PaginationBehaviour? behaviour = default, ButtonPaginationBehavior? deletion = default, CancellationToken token = default) + => channel.SendPaginatedMessageAsync(user, pages, default, behaviour, deletion, token); + + /// + /// Sends a new paginated message with buttons. + /// + /// Target channel. + /// The user that'll be able to control the pages. + /// A collection of to display. + /// Pagination buttons (leave null to default to ones on configuration). + /// Pagination behaviour. + /// Deletion behaviour + /// Override timeout period. + /// Thrown if interactivity is not enabled for the client associated with the channel. + public static Task SendPaginatedMessageAsync(this DiscordChannel channel, DiscordUser user, IEnumerable pages, PaginationButtons buttons, TimeSpan? timeoutoverride, PaginationBehaviour? behaviour = default, ButtonPaginationBehavior? deletion = default) + => GetInteractivity(channel).SendPaginatedMessageAsync(channel, user, pages, buttons, timeoutoverride, behaviour, deletion); + + /// + public static Task SendPaginatedMessageAsync(this DiscordChannel channel, DiscordUser user, IEnumerable pages, TimeSpan? timeoutoverride, PaginationBehaviour? behaviour = default, ButtonPaginationBehavior? deletion = default) + => channel.SendPaginatedMessageAsync(user, pages, default, timeoutoverride, behaviour, deletion); + + /// + /// Retrieves an interactivity instance from a channel instance. + /// + internal static InteractivityExtension GetInteractivity(DiscordChannel channel) + { + DiscordClient client = (DiscordClient)channel.Discord; + InteractivityExtension interactivity = client.GetInteractivity(); + + return interactivity ?? throw new InvalidOperationException($"Interactivity is not enabled for this {(client.isShard ? "shard" : "client")}."); + } +} diff --git a/DSharpPlus.Interactivity/Extensions/ClientExtensions.cs b/DSharpPlus.Interactivity/Extensions/ClientExtensions.cs index fe5568f70a..b8a2c59476 100644 --- a/DSharpPlus.Interactivity/Extensions/ClientExtensions.cs +++ b/DSharpPlus.Interactivity/Extensions/ClientExtensions.cs @@ -1,57 +1,57 @@ -using DSharpPlus.Extensions; - -using Microsoft.Extensions.DependencyInjection; - -namespace DSharpPlus.Interactivity.Extensions; - -/// -/// Interactivity extension methods for . -/// -public static class ClientExtensions -{ - /// - /// Enables interactivity for this instance. - /// - /// The client builder to enable interactivity for. - /// A configuration instance. Default configuration values will be used if none is provided. - /// The client builder for chaining. - public static DiscordClientBuilder UseInteractivity - ( - this DiscordClientBuilder builder, - InteractivityConfiguration? configuration = null - ) - { - builder.ConfigureServices(services => services.AddInteractivityExtension(configuration)); - - return builder; - } - - /// - /// Adds interactivity to the present service collection. - /// - /// The service collection to enable interactivity for. - /// A configuration instance. Default configuration values will be used if none is provided. - /// The service collection for chaining. - public static IServiceCollection AddInteractivityExtension - ( - this IServiceCollection services, - InteractivityConfiguration? configuration = null - ) - { - services.ConfigureEventHandlers(b => b.AddEventHandlers(ServiceLifetime.Transient)) - .AddSingleton(provider => - { - DiscordClient client = provider.GetRequiredService(); - - InteractivityExtension extension = new(configuration ?? new()); - extension.Setup(client); - - return extension; - }); - - return services; - } - - internal static InteractivityExtension GetInteractivity(this DiscordClient client) - => client.ServiceProvider.GetRequiredService(); -} +using DSharpPlus.Extensions; + +using Microsoft.Extensions.DependencyInjection; + +namespace DSharpPlus.Interactivity.Extensions; + +/// +/// Interactivity extension methods for . +/// +public static class ClientExtensions +{ + /// + /// Enables interactivity for this instance. + /// + /// The client builder to enable interactivity for. + /// A configuration instance. Default configuration values will be used if none is provided. + /// The client builder for chaining. + public static DiscordClientBuilder UseInteractivity + ( + this DiscordClientBuilder builder, + InteractivityConfiguration? configuration = null + ) + { + builder.ConfigureServices(services => services.AddInteractivityExtension(configuration)); + + return builder; + } + + /// + /// Adds interactivity to the present service collection. + /// + /// The service collection to enable interactivity for. + /// A configuration instance. Default configuration values will be used if none is provided. + /// The service collection for chaining. + public static IServiceCollection AddInteractivityExtension + ( + this IServiceCollection services, + InteractivityConfiguration? configuration = null + ) + { + services.ConfigureEventHandlers(b => b.AddEventHandlers(ServiceLifetime.Transient)) + .AddSingleton(provider => + { + DiscordClient client = provider.GetRequiredService(); + + InteractivityExtension extension = new(configuration ?? new()); + extension.Setup(client); + + return extension; + }); + + return services; + } + + internal static InteractivityExtension GetInteractivity(this DiscordClient client) + => client.ServiceProvider.GetRequiredService(); +} diff --git a/DSharpPlus.Interactivity/Extensions/InteractionExtensions.cs b/DSharpPlus.Interactivity/Extensions/InteractionExtensions.cs index 628c1c3c41..f9ac235535 100644 --- a/DSharpPlus.Interactivity/Extensions/InteractionExtensions.cs +++ b/DSharpPlus.Interactivity/Extensions/InteractionExtensions.cs @@ -1,36 +1,36 @@ -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using DSharpPlus.Entities; -using DSharpPlus.Interactivity.Enums; -using DSharpPlus.Interactivity.EventHandling; - -namespace DSharpPlus.Interactivity.Extensions; - -public static class InteractionExtensions -{ - /// - /// Sends a paginated message in response to an interaction. - /// - /// The interaction to create a response to. - /// Whether the response should be ephemeral. - /// The user to listen for button presses from. - /// The pages to paginate. - /// Optional: custom buttons - /// Pagination behaviour. - /// Deletion behaviour - /// A custom cancellation token that can be cancelled at any point. - public static Task SendPaginatedResponseAsync - ( - this DiscordInteraction interaction, - bool ephemeral, - DiscordUser user, - IEnumerable pages, - PaginationButtons buttons = null, - PaginationBehaviour? behaviour = default, - ButtonPaginationBehavior? deletion = default, - CancellationToken token = default - ) - => ChannelExtensions.GetInteractivity(interaction.Channel) - .SendPaginatedResponseAsync(interaction, ephemeral, user, pages, buttons, behaviour, deletion, default, default, token); -} +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using DSharpPlus.Entities; +using DSharpPlus.Interactivity.Enums; +using DSharpPlus.Interactivity.EventHandling; + +namespace DSharpPlus.Interactivity.Extensions; + +public static class InteractionExtensions +{ + /// + /// Sends a paginated message in response to an interaction. + /// + /// The interaction to create a response to. + /// Whether the response should be ephemeral. + /// The user to listen for button presses from. + /// The pages to paginate. + /// Optional: custom buttons + /// Pagination behaviour. + /// Deletion behaviour + /// A custom cancellation token that can be cancelled at any point. + public static Task SendPaginatedResponseAsync + ( + this DiscordInteraction interaction, + bool ephemeral, + DiscordUser user, + IEnumerable pages, + PaginationButtons buttons = null, + PaginationBehaviour? behaviour = default, + ButtonPaginationBehavior? deletion = default, + CancellationToken token = default + ) + => ChannelExtensions.GetInteractivity(interaction.Channel) + .SendPaginatedResponseAsync(interaction, ephemeral, user, pages, buttons, behaviour, deletion, default, default, token); +} diff --git a/DSharpPlus.Interactivity/Extensions/MessageExtensions.cs b/DSharpPlus.Interactivity/Extensions/MessageExtensions.cs index c88287fa18..01de205bc2 100644 --- a/DSharpPlus.Interactivity/Extensions/MessageExtensions.cs +++ b/DSharpPlus.Interactivity/Extensions/MessageExtensions.cs @@ -1,222 +1,222 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Threading; -using System.Threading.Tasks; -using DSharpPlus.Entities; -using DSharpPlus.EventArgs; -using DSharpPlus.Interactivity.Enums; -using DSharpPlus.Interactivity.EventHandling; - -namespace DSharpPlus.Interactivity.Extensions; - -/// -/// Interactivity extension methods for . -/// -public static class MessageExtensions -{ - /// - /// Waits for the next message that has the same author and channel as this message. - /// - /// Original message. - /// Overrides the timeout set in - public static Task> GetNextMessageAsync(this DiscordMessage message, TimeSpan? timeoutOverride = null) - => message.Channel.GetNextMessageAsync(message.Author, timeoutOverride); - - /// - /// Waits for the next message with the same author and channel as this message, which also satisfies a predicate. - /// - /// Original message. - /// A predicate that should return if a message matches. - /// Overrides the timeout set in - public static Task> GetNextMessageAsync(this DiscordMessage message, Func predicate, TimeSpan? timeoutOverride = null) - => message.Channel.GetNextMessageAsync(msg => msg.Author.Id == message.Author.Id && message.ChannelId == msg.ChannelId && predicate(msg), timeoutOverride); - - /// - /// Waits for any button to be pressed on the specified message. - /// - /// The message to wait on. - public static Task> WaitForButtonAsync(this DiscordMessage message) - => GetInteractivity(message).WaitForButtonAsync(message); - - /// - /// Waits for any button to be pressed on the specified message. - /// - /// The message to wait on. - /// Overrides the timeout set in - public static Task> WaitForButtonAsync(this DiscordMessage message, TimeSpan? timeoutOverride = null) - => GetInteractivity(message).WaitForButtonAsync(message, timeoutOverride); - - /// - /// Waits for any button to be pressed on the specified message. - /// - /// The message to wait on. - /// A custom cancellation token that can be cancelled at any point. - public static Task> WaitForButtonAsync(this DiscordMessage message, CancellationToken token) - => GetInteractivity(message).WaitForButtonAsync(message, token); - - /// - /// Waits for a button with the specified Id to be pressed on the specified message. - /// - /// The message to wait on. - /// The Id of the button to wait for. - /// Overrides the timeout set in - public static Task> WaitForButtonAsync(this DiscordMessage message, string id, TimeSpan? timeoutOverride = null) - => GetInteractivity(message).WaitForButtonAsync(message, id, timeoutOverride); - - /// - /// Waits for a button with the specified Id to be pressed on the specified message. - /// - /// The message to wait on. - /// The Id of the button to wait for. - /// A custom cancellation token that can be cancelled at any point. - public static Task> WaitForButtonAsync(this DiscordMessage message, string id, CancellationToken token) - => GetInteractivity(message).WaitForButtonAsync(message, id, token); - - /// - /// Waits for any button to be pressed on the specified message by the specified user. - /// - /// The message to wait on. - /// The user to wait for button input from. - /// Overrides the timeout set in - public static Task> WaitForButtonAsync(this DiscordMessage message, DiscordUser user, TimeSpan? timeoutOverride = null) - => GetInteractivity(message).WaitForButtonAsync(message, user, timeoutOverride); - - /// - /// Waits for any button to be pressed on the specified message by the specified user. - /// - /// The message to wait on. - /// The user to wait for button input from. - /// A custom cancellation token that can be cancelled at any point. - public static Task> WaitForButtonAsync(this DiscordMessage message, DiscordUser user, CancellationToken token) - => GetInteractivity(message).WaitForButtonAsync(message, user, token); - - /// - /// Waits for any button to be interacted with. - /// - /// The message to wait on. - /// The predicate to filter interactions by. - /// Override the timeout specified in - public static Task> WaitForButtonAsync(this DiscordMessage message, Func predicate, TimeSpan? timeoutOverride = null) - => GetInteractivity(message).WaitForButtonAsync(message, predicate, timeoutOverride); - - /// - /// Waits for any button to be interacted with. - /// - /// The message to wait on. - /// The predicate to filter interactions by. - /// A token to cancel interactivity with at any time. Pass to wait indefinitely. - public static Task> WaitForButtonAsync(this DiscordMessage message, Func predicate, CancellationToken token) - => GetInteractivity(message).WaitForButtonAsync(message, predicate, token); - - /// - /// Waits for any dropdown to be interacted with. - /// - /// The message to wait for. - /// A filter predicate. - /// Override the timeout period specified in . - /// Thrown when the message doesn't contain any dropdowns - public static Task> WaitForSelectAsync(this DiscordMessage message, Func predicate, TimeSpan? timeoutOverride = null) - => GetInteractivity(message).WaitForSelectAsync(message, predicate, timeoutOverride); - - /// - /// Waits for any dropdown to be interacted with. - /// - /// The message to wait for. - /// A filter predicate. - /// A token that can be used to cancel interactivity. Pass to wait indefinitely. - /// Thrown when the message doesn't contain any dropdowns - public static Task> WaitForSelectAsync(this DiscordMessage message, Func predicate, CancellationToken token) - => GetInteractivity(message).WaitForSelectAsync(message, predicate, token); - - /// - /// Waits for a dropdown to be interacted with. - /// - /// The message to wait on. - /// The Id of the dropdown to wait for. - /// Overrides the timeout set in - public static Task> WaitForSelectAsync(this DiscordMessage message, string id, TimeSpan? timeoutOverride = null) - => GetInteractivity(message).WaitForSelectAsync(message, id, timeoutOverride); - - /// - /// Waits for a dropdown to be interacted with. - /// - /// The message to wait on. - /// The Id of the dropdown to wait for. - /// A custom cancellation token that can be cancelled at any point. - public static Task> WaitForSelectAsync(this DiscordMessage message, string id, CancellationToken token) - => GetInteractivity(message).WaitForSelectAsync(message, id, token); - - /// - /// Waits for a dropdown to be interacted with by the specified user. - /// - /// The message to wait on. - /// The user to wait for. - /// The Id of the dropdown to wait for. - /// - public static Task> WaitForSelectAsync(this DiscordMessage message, DiscordUser user, string id, TimeSpan? timeoutOverride = null) - => GetInteractivity(message).WaitForSelectAsync(message, user, id, timeoutOverride); - - /// - /// Waits for a dropdown to be interacted with by the specified user. - /// - /// The message to wait on. - /// The user to wait for. - /// The Id of the dropdown to wait for. - /// A custom cancellation token that can be cancelled at any point. - - public static Task> WaitForSelectAsync(this DiscordMessage message, DiscordUser user, string id, CancellationToken token) - => GetInteractivity(message).WaitForSelectAsync(message, user, id, token); - - /// - /// Waits for a reaction on this message from a specific user. - /// - /// Target message. - /// The target user. - /// Overrides the timeout set in - /// Thrown if interactivity is not enabled for the client associated with the message. - public static Task> WaitForReactionAsync(this DiscordMessage message, DiscordUser user, TimeSpan? timeoutOverride = null) - => GetInteractivity(message).WaitForReactionAsync(message, user, timeoutOverride); - - /// - /// Waits for a specific reaction on this message from the specified user. - /// - /// Target message. - /// The target user. - /// The target emoji. - /// Overrides the timeout set in - /// Thrown if interactivity is not enabled for the client associated with the message. - public static Task> WaitForReactionAsync(this DiscordMessage message, DiscordUser user, DiscordEmoji emoji, TimeSpan? timeoutOverride = null) - => GetInteractivity(message).WaitForReactionAsync(e => e.Emoji == emoji, message, user, timeoutOverride); - - /// - /// Collects all reactions on this message within the timeout duration. - /// - /// The message to collect reactions from. - /// Overrides the timeout set in - /// Thrown if interactivity is not enabled for the client associated with the message. - public static Task> CollectReactionsAsync(this DiscordMessage message, TimeSpan? timeoutOverride = null) - => GetInteractivity(message).CollectReactionsAsync(message, timeoutOverride); - - /// - /// Begins a poll using this message. - /// - /// Target message. - /// Options for this poll. - /// Overrides the action set in - /// Overrides the timeout set in - /// Thrown if interactivity is not enabled for the client associated with the message. - public static Task> DoPollAsync(this DiscordMessage message, IEnumerable emojis, PollBehaviour? behaviorOverride = null, TimeSpan? timeoutOverride = null) - => GetInteractivity(message).DoPollAsync(message, emojis, behaviorOverride, timeoutOverride); - - /// - /// Retrieves an interactivity instance from a message instance. - /// - internal static InteractivityExtension GetInteractivity(DiscordMessage message) - { - DiscordClient client = (DiscordClient)message.Discord; - InteractivityExtension interactivity = client.GetInteractivity(); - - return interactivity ?? throw new InvalidOperationException($"Interactivity is not enabled for this {(client.isShard ? "shard" : "client")}."); - } -} +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Threading; +using System.Threading.Tasks; +using DSharpPlus.Entities; +using DSharpPlus.EventArgs; +using DSharpPlus.Interactivity.Enums; +using DSharpPlus.Interactivity.EventHandling; + +namespace DSharpPlus.Interactivity.Extensions; + +/// +/// Interactivity extension methods for . +/// +public static class MessageExtensions +{ + /// + /// Waits for the next message that has the same author and channel as this message. + /// + /// Original message. + /// Overrides the timeout set in + public static Task> GetNextMessageAsync(this DiscordMessage message, TimeSpan? timeoutOverride = null) + => message.Channel.GetNextMessageAsync(message.Author, timeoutOverride); + + /// + /// Waits for the next message with the same author and channel as this message, which also satisfies a predicate. + /// + /// Original message. + /// A predicate that should return if a message matches. + /// Overrides the timeout set in + public static Task> GetNextMessageAsync(this DiscordMessage message, Func predicate, TimeSpan? timeoutOverride = null) + => message.Channel.GetNextMessageAsync(msg => msg.Author.Id == message.Author.Id && message.ChannelId == msg.ChannelId && predicate(msg), timeoutOverride); + + /// + /// Waits for any button to be pressed on the specified message. + /// + /// The message to wait on. + public static Task> WaitForButtonAsync(this DiscordMessage message) + => GetInteractivity(message).WaitForButtonAsync(message); + + /// + /// Waits for any button to be pressed on the specified message. + /// + /// The message to wait on. + /// Overrides the timeout set in + public static Task> WaitForButtonAsync(this DiscordMessage message, TimeSpan? timeoutOverride = null) + => GetInteractivity(message).WaitForButtonAsync(message, timeoutOverride); + + /// + /// Waits for any button to be pressed on the specified message. + /// + /// The message to wait on. + /// A custom cancellation token that can be cancelled at any point. + public static Task> WaitForButtonAsync(this DiscordMessage message, CancellationToken token) + => GetInteractivity(message).WaitForButtonAsync(message, token); + + /// + /// Waits for a button with the specified Id to be pressed on the specified message. + /// + /// The message to wait on. + /// The Id of the button to wait for. + /// Overrides the timeout set in + public static Task> WaitForButtonAsync(this DiscordMessage message, string id, TimeSpan? timeoutOverride = null) + => GetInteractivity(message).WaitForButtonAsync(message, id, timeoutOverride); + + /// + /// Waits for a button with the specified Id to be pressed on the specified message. + /// + /// The message to wait on. + /// The Id of the button to wait for. + /// A custom cancellation token that can be cancelled at any point. + public static Task> WaitForButtonAsync(this DiscordMessage message, string id, CancellationToken token) + => GetInteractivity(message).WaitForButtonAsync(message, id, token); + + /// + /// Waits for any button to be pressed on the specified message by the specified user. + /// + /// The message to wait on. + /// The user to wait for button input from. + /// Overrides the timeout set in + public static Task> WaitForButtonAsync(this DiscordMessage message, DiscordUser user, TimeSpan? timeoutOverride = null) + => GetInteractivity(message).WaitForButtonAsync(message, user, timeoutOverride); + + /// + /// Waits for any button to be pressed on the specified message by the specified user. + /// + /// The message to wait on. + /// The user to wait for button input from. + /// A custom cancellation token that can be cancelled at any point. + public static Task> WaitForButtonAsync(this DiscordMessage message, DiscordUser user, CancellationToken token) + => GetInteractivity(message).WaitForButtonAsync(message, user, token); + + /// + /// Waits for any button to be interacted with. + /// + /// The message to wait on. + /// The predicate to filter interactions by. + /// Override the timeout specified in + public static Task> WaitForButtonAsync(this DiscordMessage message, Func predicate, TimeSpan? timeoutOverride = null) + => GetInteractivity(message).WaitForButtonAsync(message, predicate, timeoutOverride); + + /// + /// Waits for any button to be interacted with. + /// + /// The message to wait on. + /// The predicate to filter interactions by. + /// A token to cancel interactivity with at any time. Pass to wait indefinitely. + public static Task> WaitForButtonAsync(this DiscordMessage message, Func predicate, CancellationToken token) + => GetInteractivity(message).WaitForButtonAsync(message, predicate, token); + + /// + /// Waits for any dropdown to be interacted with. + /// + /// The message to wait for. + /// A filter predicate. + /// Override the timeout period specified in . + /// Thrown when the message doesn't contain any dropdowns + public static Task> WaitForSelectAsync(this DiscordMessage message, Func predicate, TimeSpan? timeoutOverride = null) + => GetInteractivity(message).WaitForSelectAsync(message, predicate, timeoutOverride); + + /// + /// Waits for any dropdown to be interacted with. + /// + /// The message to wait for. + /// A filter predicate. + /// A token that can be used to cancel interactivity. Pass to wait indefinitely. + /// Thrown when the message doesn't contain any dropdowns + public static Task> WaitForSelectAsync(this DiscordMessage message, Func predicate, CancellationToken token) + => GetInteractivity(message).WaitForSelectAsync(message, predicate, token); + + /// + /// Waits for a dropdown to be interacted with. + /// + /// The message to wait on. + /// The Id of the dropdown to wait for. + /// Overrides the timeout set in + public static Task> WaitForSelectAsync(this DiscordMessage message, string id, TimeSpan? timeoutOverride = null) + => GetInteractivity(message).WaitForSelectAsync(message, id, timeoutOverride); + + /// + /// Waits for a dropdown to be interacted with. + /// + /// The message to wait on. + /// The Id of the dropdown to wait for. + /// A custom cancellation token that can be cancelled at any point. + public static Task> WaitForSelectAsync(this DiscordMessage message, string id, CancellationToken token) + => GetInteractivity(message).WaitForSelectAsync(message, id, token); + + /// + /// Waits for a dropdown to be interacted with by the specified user. + /// + /// The message to wait on. + /// The user to wait for. + /// The Id of the dropdown to wait for. + /// + public static Task> WaitForSelectAsync(this DiscordMessage message, DiscordUser user, string id, TimeSpan? timeoutOverride = null) + => GetInteractivity(message).WaitForSelectAsync(message, user, id, timeoutOverride); + + /// + /// Waits for a dropdown to be interacted with by the specified user. + /// + /// The message to wait on. + /// The user to wait for. + /// The Id of the dropdown to wait for. + /// A custom cancellation token that can be cancelled at any point. + + public static Task> WaitForSelectAsync(this DiscordMessage message, DiscordUser user, string id, CancellationToken token) + => GetInteractivity(message).WaitForSelectAsync(message, user, id, token); + + /// + /// Waits for a reaction on this message from a specific user. + /// + /// Target message. + /// The target user. + /// Overrides the timeout set in + /// Thrown if interactivity is not enabled for the client associated with the message. + public static Task> WaitForReactionAsync(this DiscordMessage message, DiscordUser user, TimeSpan? timeoutOverride = null) + => GetInteractivity(message).WaitForReactionAsync(message, user, timeoutOverride); + + /// + /// Waits for a specific reaction on this message from the specified user. + /// + /// Target message. + /// The target user. + /// The target emoji. + /// Overrides the timeout set in + /// Thrown if interactivity is not enabled for the client associated with the message. + public static Task> WaitForReactionAsync(this DiscordMessage message, DiscordUser user, DiscordEmoji emoji, TimeSpan? timeoutOverride = null) + => GetInteractivity(message).WaitForReactionAsync(e => e.Emoji == emoji, message, user, timeoutOverride); + + /// + /// Collects all reactions on this message within the timeout duration. + /// + /// The message to collect reactions from. + /// Overrides the timeout set in + /// Thrown if interactivity is not enabled for the client associated with the message. + public static Task> CollectReactionsAsync(this DiscordMessage message, TimeSpan? timeoutOverride = null) + => GetInteractivity(message).CollectReactionsAsync(message, timeoutOverride); + + /// + /// Begins a poll using this message. + /// + /// Target message. + /// Options for this poll. + /// Overrides the action set in + /// Overrides the timeout set in + /// Thrown if interactivity is not enabled for the client associated with the message. + public static Task> DoPollAsync(this DiscordMessage message, IEnumerable emojis, PollBehaviour? behaviorOverride = null, TimeSpan? timeoutOverride = null) + => GetInteractivity(message).DoPollAsync(message, emojis, behaviorOverride, timeoutOverride); + + /// + /// Retrieves an interactivity instance from a message instance. + /// + internal static InteractivityExtension GetInteractivity(DiscordMessage message) + { + DiscordClient client = (DiscordClient)message.Discord; + InteractivityExtension interactivity = client.GetInteractivity(); + + return interactivity ?? throw new InvalidOperationException($"Interactivity is not enabled for this {(client.isShard ? "shard" : "client")}."); + } +} diff --git a/DSharpPlus.Interactivity/InteractivityConfiguration.cs b/DSharpPlus.Interactivity/InteractivityConfiguration.cs index 52f64c49ef..930a0d0689 100644 --- a/DSharpPlus.Interactivity/InteractivityConfiguration.cs +++ b/DSharpPlus.Interactivity/InteractivityConfiguration.cs @@ -1,95 +1,95 @@ -using System; -using DSharpPlus.EventArgs; -using DSharpPlus.Interactivity.Enums; -using DSharpPlus.Interactivity.EventHandling; - -namespace DSharpPlus.Interactivity; - -/// -/// Configuration class for your Interactivity extension -/// -public sealed class InteractivityConfiguration -{ - /// - /// Sets the default interactivity action timeout. - /// Defaults to 1 minute. - /// - public TimeSpan Timeout { internal get; set; } = TimeSpan.FromMinutes(1); - - /// - /// What to do after the poll ends - /// - public PollBehaviour PollBehaviour { internal get; set; } = PollBehaviour.DeleteEmojis; - - /// - /// Emojis to use for pagination - /// - public PaginationEmojis PaginationEmojis { internal get; set; } = new(); - - /// - /// Buttons to use for pagination. - /// - public PaginationButtons PaginationButtons { internal get; set; } = new(); - - /// - /// How to handle buttons after pagination ends. - /// - public ButtonPaginationBehavior ButtonBehavior { internal get; set; } = new(); - - /// - /// How to handle pagination. Defaults to WrapAround. - /// - public PaginationBehaviour PaginationBehaviour { internal get; set; } = PaginationBehaviour.WrapAround; - - /// - /// How to handle pagination deletion. Defaults to DeleteEmojis. - /// - public PaginationDeletion PaginationDeletion { internal get; set; } = PaginationDeletion.DeleteEmojis; - - /// - /// How to handle invalid [component] interactions. Defaults to - /// - public InteractionResponseBehavior ResponseBehavior { internal get; set; } = InteractionResponseBehavior.Ignore; - - /// - /// Provides a string factory to generate a response when processing invalid interactions. This is ignored if is not - /// - /// - /// An invalid interaction in this case is considered as an interaction on a component where the invoking user does not match the specified user to wait for. - /// - public Func ResponseMessageFactory - { - internal get; - set; - } = (_, _) => "This message is not meant for you!"; - - /// - /// The message to send to the user when processing invalid interactions. Ignored if is not set to . - /// - public string ResponseMessage { internal get; set; } - - /// - /// Creates a new instance of . - /// - public InteractivityConfiguration() - { - } - - /// - /// Creates a new instance of , copying the properties of another configuration. - /// - /// Configuration the properties of which are to be copied. - public InteractivityConfiguration(InteractivityConfiguration other) - { - this.PaginationButtons = other.PaginationButtons; - this.ButtonBehavior = other.ButtonBehavior; - this.PaginationBehaviour = other.PaginationBehaviour; - this.PaginationDeletion = other.PaginationDeletion; - this.ResponseBehavior = other.ResponseBehavior; - this.PaginationEmojis = other.PaginationEmojis; - this.ResponseMessageFactory = other.ResponseMessageFactory; - this.ResponseMessage = other.ResponseMessage; - this.PollBehaviour = other.PollBehaviour; - this.Timeout = other.Timeout; - } -} +using System; +using DSharpPlus.EventArgs; +using DSharpPlus.Interactivity.Enums; +using DSharpPlus.Interactivity.EventHandling; + +namespace DSharpPlus.Interactivity; + +/// +/// Configuration class for your Interactivity extension +/// +public sealed class InteractivityConfiguration +{ + /// + /// Sets the default interactivity action timeout. + /// Defaults to 1 minute. + /// + public TimeSpan Timeout { internal get; set; } = TimeSpan.FromMinutes(1); + + /// + /// What to do after the poll ends + /// + public PollBehaviour PollBehaviour { internal get; set; } = PollBehaviour.DeleteEmojis; + + /// + /// Emojis to use for pagination + /// + public PaginationEmojis PaginationEmojis { internal get; set; } = new(); + + /// + /// Buttons to use for pagination. + /// + public PaginationButtons PaginationButtons { internal get; set; } = new(); + + /// + /// How to handle buttons after pagination ends. + /// + public ButtonPaginationBehavior ButtonBehavior { internal get; set; } = new(); + + /// + /// How to handle pagination. Defaults to WrapAround. + /// + public PaginationBehaviour PaginationBehaviour { internal get; set; } = PaginationBehaviour.WrapAround; + + /// + /// How to handle pagination deletion. Defaults to DeleteEmojis. + /// + public PaginationDeletion PaginationDeletion { internal get; set; } = PaginationDeletion.DeleteEmojis; + + /// + /// How to handle invalid [component] interactions. Defaults to + /// + public InteractionResponseBehavior ResponseBehavior { internal get; set; } = InteractionResponseBehavior.Ignore; + + /// + /// Provides a string factory to generate a response when processing invalid interactions. This is ignored if is not + /// + /// + /// An invalid interaction in this case is considered as an interaction on a component where the invoking user does not match the specified user to wait for. + /// + public Func ResponseMessageFactory + { + internal get; + set; + } = (_, _) => "This message is not meant for you!"; + + /// + /// The message to send to the user when processing invalid interactions. Ignored if is not set to . + /// + public string ResponseMessage { internal get; set; } + + /// + /// Creates a new instance of . + /// + public InteractivityConfiguration() + { + } + + /// + /// Creates a new instance of , copying the properties of another configuration. + /// + /// Configuration the properties of which are to be copied. + public InteractivityConfiguration(InteractivityConfiguration other) + { + this.PaginationButtons = other.PaginationButtons; + this.ButtonBehavior = other.ButtonBehavior; + this.PaginationBehaviour = other.PaginationBehaviour; + this.PaginationDeletion = other.PaginationDeletion; + this.ResponseBehavior = other.ResponseBehavior; + this.PaginationEmojis = other.PaginationEmojis; + this.ResponseMessageFactory = other.ResponseMessageFactory; + this.ResponseMessage = other.ResponseMessage; + this.PollBehaviour = other.PollBehaviour; + this.Timeout = other.Timeout; + } +} diff --git a/DSharpPlus.Interactivity/InteractivityEvents.cs b/DSharpPlus.Interactivity/InteractivityEvents.cs index e63fad3e07..6ae6205d94 100644 --- a/DSharpPlus.Interactivity/InteractivityEvents.cs +++ b/DSharpPlus.Interactivity/InteractivityEvents.cs @@ -1,34 +1,34 @@ -using Microsoft.Extensions.Logging; - -namespace DSharpPlus.Interactivity; - -/// -/// Contains well-defined event IDs used by the Interactivity extension. -/// -public static class InteractivityEvents -{ - /// - /// Miscellaneous events, that do not fit in any other category. - /// - public static EventId Misc { get; } = new EventId(500, "Interactivity"); - - /// - /// Events pertaining to errors that happen during waiting for events. - /// - public static EventId InteractivityWaitError { get; } = new EventId(501, nameof(InteractivityWaitError)); - - /// - /// Events pertaining to pagination. - /// - public static EventId InteractivityPaginationError { get; } = new EventId(502, nameof(InteractivityPaginationError)); - - /// - /// Events pertaining to polling. - /// - public static EventId InteractivityPollError { get; } = new EventId(503, nameof(InteractivityPollError)); - - /// - /// Events pertaining to event collection. - /// - public static EventId InteractivityCollectorError { get; } = new EventId(504, nameof(InteractivityCollectorError)); -} +using Microsoft.Extensions.Logging; + +namespace DSharpPlus.Interactivity; + +/// +/// Contains well-defined event IDs used by the Interactivity extension. +/// +public static class InteractivityEvents +{ + /// + /// Miscellaneous events, that do not fit in any other category. + /// + public static EventId Misc { get; } = new EventId(500, "Interactivity"); + + /// + /// Events pertaining to errors that happen during waiting for events. + /// + public static EventId InteractivityWaitError { get; } = new EventId(501, nameof(InteractivityWaitError)); + + /// + /// Events pertaining to pagination. + /// + public static EventId InteractivityPaginationError { get; } = new EventId(502, nameof(InteractivityPaginationError)); + + /// + /// Events pertaining to polling. + /// + public static EventId InteractivityPollError { get; } = new EventId(503, nameof(InteractivityPollError)); + + /// + /// Events pertaining to event collection. + /// + public static EventId InteractivityCollectorError { get; } = new EventId(504, nameof(InteractivityCollectorError)); +} diff --git a/DSharpPlus.Interactivity/InteractivityExtension.cs b/DSharpPlus.Interactivity/InteractivityExtension.cs index 8ba6779e2c..58f4e25ad6 100644 --- a/DSharpPlus.Interactivity/InteractivityExtension.cs +++ b/DSharpPlus.Interactivity/InteractivityExtension.cs @@ -1,1174 +1,1174 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using DSharpPlus.AsyncEvents; -using DSharpPlus.Entities; -using DSharpPlus.EventArgs; -using DSharpPlus.Interactivity.Enums; -using DSharpPlus.Interactivity.EventHandling; - -namespace DSharpPlus.Interactivity; - -/// -/// Extension class for DSharpPlus.Interactivity -/// -public class InteractivityExtension : IDisposable -{ - internal readonly ConcurrentDictionary eventDistributor = []; - internal IClientErrorHandler errorHandler; - -#pragma warning disable IDE1006 // Naming Styles - internal InteractivityConfiguration Config { get; } - public DiscordClient Client { get; private set; } - - private EventWaiter MessageCreatedWaiter; - - private EventWaiter MessageReactionAddWaiter; - - private EventWaiter TypingStartWaiter; - - private EventWaiter ComponentInteractionWaiter; - - internal ComponentEventWaiter ComponentEventWaiter; - - internal ModalEventWaiter ModalEventWaiter; - - internal ReactionCollector ReactionCollector; - - internal Poller Poller; - - internal Paginator Paginator; - internal ComponentPaginator compPaginator; - -#pragma warning restore IDE1006 // Naming Styles - - internal InteractivityExtension(InteractivityConfiguration cfg) => this.Config = new InteractivityConfiguration(cfg); - - public void Setup(DiscordClient client) - { - this.Client = client; - this.MessageCreatedWaiter = new EventWaiter(this); - this.MessageReactionAddWaiter = new EventWaiter(this); - this.ComponentInteractionWaiter = new EventWaiter(this); - this.TypingStartWaiter = new EventWaiter(this); - this.Poller = new Poller(this.Client); - this.ReactionCollector = new ReactionCollector(this); - this.Paginator = new Paginator(this.Client); - this.compPaginator = new(this.Client, this.Config); - this.ComponentEventWaiter = new(this.Client, this.Config); - this.ModalEventWaiter = new(this.Client); - this.errorHandler = new DefaultClientErrorHandler(this.Client.Logger); - } - - /// - /// Makes a poll and returns poll results. - /// - /// Message to create poll on. - /// Emojis to use for this poll. - /// What to do when the poll ends. - /// override timeout period. - /// - public async Task> DoPollAsync(DiscordMessage m, IEnumerable emojis, PollBehaviour? behaviour = default, TimeSpan? timeout = null) - { - if (!Utilities.HasReactionIntents(this.Client.Intents)) - { - throw new InvalidOperationException("No reaction intents are enabled."); - } - - if (!emojis.Any()) - { - throw new ArgumentException("You need to provide at least one emoji for a poll!"); - } - - foreach (DiscordEmoji em in emojis) - { - await m.CreateReactionAsync(em); - } - - ReadOnlyCollection res = await this.Poller.DoPollAsync(new PollRequest(m, timeout ?? this.Config.Timeout, emojis)); - - PollBehaviour pollbehaviour = behaviour ?? this.Config.PollBehaviour; - DiscordMember thismember = await m.Channel.Guild.GetMemberAsync(this.Client.CurrentUser.Id); - - if (pollbehaviour == PollBehaviour.DeleteEmojis && m.Channel.PermissionsFor(thismember).HasPermission(DiscordPermission.ManageMessages)) - { - await m.DeleteAllReactionsAsync(); - } - - return new ReadOnlyCollection(res.ToList()); - } - - /// - /// Waits for a modal with the specified id to be submitted. - /// - /// The id of the modal to wait for. Should be unique to avoid issues. - /// Override the timeout period in . - /// A with a modal if the interactivity did not time out. - public Task> WaitForModalAsync(string modal_id, TimeSpan? timeoutOverride = null) - => WaitForModalAsync(modal_id, GetCancellationToken(timeoutOverride)); - - /// - /// Waits for a modal with the specified id to be submitted. - /// - /// The id of the modal to wait for. Should be unique to avoid issues. - /// A custom cancellation token that can be cancelled at any point. - /// A with a modal if the interactivity did not time out. - public async Task> WaitForModalAsync(string modal_id, CancellationToken token) - { - if (string.IsNullOrEmpty(modal_id) || modal_id.Length > 100) - { - throw new ArgumentException("Custom ID must be between 1 and 100 characters."); - } - - ModalMatchRequest matchRequest = new(modal_id, - c => c.Interaction.Data.CustomId == modal_id, cancellation: token); - ModalSubmittedEventArgs? result = await this.ModalEventWaiter.WaitForMatchAsync(matchRequest); - - return new(result is null, result); - } - - /// - /// Waits for a modal with the specified custom id to be submitted by the given user. - /// - /// The id of the modal to wait for. Should be unique to avoid issues. - /// The user to wait for the modal from. - /// Override the timeout period in . - /// A with a modal if the interactivity did not time out. - public Task> WaitForModalAsync(string modal_id, DiscordUser user, TimeSpan? timeoutOverride = null) - => WaitForModalAsync(modal_id, user, GetCancellationToken(timeoutOverride)); - - /// - /// Waits for a modal with the specified custom id to be submitted by the given user. - /// - /// The id of the modal to wait for. Should be unique to avoid issues. - /// The user to wait for the modal from. - /// A custom cancellation token that can be cancelled at any point. - /// A with a modal if the interactivity did not time out. - public async Task> WaitForModalAsync(string modal_id, DiscordUser user, CancellationToken token) - { - if (string.IsNullOrEmpty(modal_id) || modal_id.Length > 100) - { - throw new ArgumentException("Custom ID must be between 1 and 100 characters."); - } - - ModalMatchRequest matchRequest = new(modal_id, - c => c.Interaction.Data.CustomId == modal_id && - c.Interaction.User.Id == user.Id, cancellation: token); - ModalSubmittedEventArgs? result = await this.ModalEventWaiter.WaitForMatchAsync(matchRequest); - - return new(result is null, result); - } - - /// - /// Waits for any button in the specified collection to be pressed. - /// - /// The message to wait on. - /// A collection of buttons to listen for. - /// Override the timeout period in . - /// A with the result of button that was pressed, if any. - /// Thrown when attempting to wait for a message that is not authored by the current user. - /// Thrown when the message does not contain a button with the specified Id, or any buttons at all. - public Task> WaitForButtonAsync(DiscordMessage message, IEnumerable buttons, TimeSpan? timeoutOverride = null) - => WaitForButtonAsync(message, buttons, GetCancellationToken(timeoutOverride)); - - /// - /// Waits for any button in the specified collection to be pressed. - /// - /// The message to wait on. - /// A collection of buttons to listen for. - /// A custom cancellation token that can be cancelled at any point. - /// A with the result of button that was pressed, if any. - /// Thrown when attempting to wait for a message that is not authored by the current user. - /// Thrown when the message does not contain a button with the specified Id, or any buttons at all. - public async Task> WaitForButtonAsync(DiscordMessage message, IEnumerable buttons, CancellationToken token) - { - if (message.Author != this.Client.CurrentUser) - { - throw new InvalidOperationException("Interaction events are only sent to the application that created them."); - } - - if (!buttons.Any()) - { - throw new ArgumentException("You must specify at least one button to listen for."); - } - - if (message.Components.Count == 0) - { - throw new ArgumentException("Provided message does not contain any components."); - } - - if (message.FilterComponents().Count == 0) - { - throw new ArgumentException("Provided message does not contain any button components."); - } - - ComponentInteractionCreatedEventArgs? res = await this.ComponentEventWaiter - .WaitForMatchAsync(new(message, - c => - c.Interaction.Data.ComponentType == DiscordComponentType.Button && - buttons.Any(b => b.CustomId == c.Id), token)); - - return new(res is null, res); - } - - /// - /// Waits for any button on the specified message to be pressed. - /// - /// The message to wait for the button on. - /// Override the timeout period specified in . - /// A with the result of button that was pressed, if any. - /// Thrown when attempting to wait for a message that is not authored by the current user. - /// Thrown when the message does not contain a button with the specified Id, or any buttons at all. - public Task> WaitForButtonAsync(DiscordMessage message, TimeSpan? timeoutOverride = null) - => WaitForButtonAsync(message, GetCancellationToken(timeoutOverride)); - - /// - /// Waits for any button on the specified message to be pressed. - /// - /// The message to wait for the button on. - /// A custom cancellation token that can be cancelled at any point. - /// A with the result of button that was pressed, if any. - /// Thrown when attempting to wait for a message that is not authored by the current user. - /// Thrown when the message does not contain a button with the specified Id, or any buttons at all. - public async Task> WaitForButtonAsync(DiscordMessage message, CancellationToken token) - { - if (message.Author != this.Client.CurrentUser) - { - throw new InvalidOperationException("Interaction events are only sent to the application that created them."); - } - - if (message.Components.Count == 0) - { - throw new ArgumentException("Provided message does not contain any components."); - } - - if (message.FilterComponents().Count == 0) - { - throw new ArgumentException("Provided message does not contain any button components."); - } - - IEnumerable ids = message.FilterComponents().Select(c => c.CustomId); - - ComponentInteractionCreatedEventArgs? result = - await - this.ComponentEventWaiter - .WaitForMatchAsync(new(message, c => c.Interaction.Data.ComponentType == DiscordComponentType.Button && ids.Contains(c.Id), token)) - ; - - return new(result is null, result); - } - - /// - /// Waits for any button on the specified message to be pressed by the specified user. - /// - /// The message to wait for the button on. - /// The user to wait for the button press from. - /// Override the timeout period specified in . - /// A with the result of button that was pressed, if any. - /// Thrown when attempting to wait for a message that is not authored by the current user. - /// Thrown when the message does not contain a button with the specified Id, or any buttons at all. - public Task> WaitForButtonAsync(DiscordMessage message, DiscordUser user, TimeSpan? timeoutOverride = null) - => WaitForButtonAsync(message, user, GetCancellationToken(timeoutOverride)); - - /// - /// Waits for any button on the specified message to be pressed by the specified user. - /// - /// The message to wait for the button on. - /// The user to wait for the button press from. - /// A custom cancellation token that can be cancelled at any point. - /// A with the result of button that was pressed, if any. - /// Thrown when attempting to wait for a message that is not authored by the current user. - /// Thrown when the message does not contain a button with the specified Id, or any buttons at all. - public async Task> WaitForButtonAsync(DiscordMessage message, DiscordUser user, CancellationToken token) - { - if (message.Author != this.Client.CurrentUser) - { - throw new InvalidOperationException("Interaction events are only sent to the application that created them."); - } - - if (message.Components.Count == 0) - { - throw new ArgumentException("Provided message does not contain any components."); - } - - if (message.FilterComponents().Count == 0) - { - throw new ArgumentException("Provided message does not contain any button components."); - } - - ComponentInteractionCreatedEventArgs? result = await - this.ComponentEventWaiter - .WaitForMatchAsync(new(message, (c) => c.Interaction.Data.ComponentType is DiscordComponentType.Button && c.User == user, token)) - ; - - return new(result is null, result); - - } - - /// - /// Waits for a button with the specified Id to be pressed. - /// - /// The message to wait for the button on. - /// The Id of the button to wait for. - /// Override the timeout period specified in . - /// A with the result of the operation. - /// Thrown when attempting to wait for a message that is not authored by the current user. - /// Thrown when the message does not contain a button with the specified Id, or any buttons at all. - public Task> WaitForButtonAsync(DiscordMessage message, string id, TimeSpan? timeoutOverride = null) - => WaitForButtonAsync(message, id, GetCancellationToken(timeoutOverride)); - - /// - /// Waits for a button with the specified Id to be pressed. - /// - /// The message to wait for the button on. - /// The Id of the button to wait for. - /// Cancellation token. - /// A with the result of the operation. - /// Thrown when attempting to wait for a message that is not authored by the current user. - /// Thrown when the message does not contain a button with the specified Id, or any buttons at all. - public async Task> WaitForButtonAsync(DiscordMessage message, string id, CancellationToken token) - { - if (message.Author != this.Client.CurrentUser) - { - throw new InvalidOperationException("Interaction events are only sent to the application that created them."); - } - - if (message.Components.Count == 0) - { - throw new ArgumentException("Provided message does not contain any components."); - } - - if (message.FilterComponents().Count == 0) - { - throw new ArgumentException("Provided message does not contain any button components."); - } - - if (!message.FilterComponents().Any(c => c.CustomId == id)) - { - throw new ArgumentException($"Provided message does not contain button with Id of '{id}'."); - } - - ComponentInteractionCreatedEventArgs? result = await - this.ComponentEventWaiter - .WaitForMatchAsync(new(message, (c) => c.Interaction.Data.ComponentType is DiscordComponentType.Button && c.Id == id, token)) - ; - - return new(result is null, result); - } - - /// - /// Waits for any button to be interacted with. - /// - /// The message to wait on. - /// The predicate to filter interactions by. - /// Override the timeout specified in - public Task> WaitForButtonAsync(DiscordMessage message, Func predicate, TimeSpan? timeoutOverride = null) - => WaitForButtonAsync(message, predicate, GetCancellationToken(timeoutOverride)); - - /// - /// Waits for any button to be interacted with. - /// - /// The message to wait on. - /// The predicate to filter interactions by. - /// A token to cancel interactivity with at any time. Pass to wait indefinitely. - public async Task> WaitForButtonAsync(DiscordMessage message, Func predicate, CancellationToken token) - { - if (message.Author != this.Client.CurrentUser) - { - throw new InvalidOperationException("Interaction events are only sent to the application that created them."); - } - - if (message.Components.Count == 0) - { - throw new ArgumentException("Provided message does not contain any components."); - } - - if (message.FilterComponents().Count == 0) - { - throw new ArgumentException("Provided message does not contain any button components."); - } - - ComponentInteractionCreatedEventArgs? result = await - this.ComponentEventWaiter - .WaitForMatchAsync(new(message, c => c.Interaction.Data.ComponentType is DiscordComponentType.Button && predicate(c), token)) - ; - - return new(result is null, result); - } - - /// - /// Waits for any dropdown to be interacted with. - /// - /// The message to wait for. - /// A filter predicate. - /// Override the timeout period specified in . - /// Thrown when the message doesn't contain any dropdowns - public Task> WaitForSelectAsync(DiscordMessage message, Func predicate, TimeSpan? timeoutOverride = null) - => WaitForSelectAsync(message, predicate, GetCancellationToken(timeoutOverride)); - - /// - /// Waits for any dropdown to be interacted with. - /// - /// The message to wait for. - /// A filter predicate. - /// A token that can be used to cancel interactivity. Pass to wait indefinitely. - /// Thrown when the message doesn't contain any dropdowns - public async Task> WaitForSelectAsync(DiscordMessage message, Func predicate, CancellationToken token) - { - if (message.Author != this.Client.CurrentUser) - { - throw new InvalidOperationException("Interaction events are only sent to the application that created them."); - } - - if (message.Components.Count == 0) - { - throw new ArgumentException("Provided message does not contain any components."); - } - - if (!message.FilterComponents().Any(IsSelect)) - { - throw new ArgumentException("Provided message does not contain any select components."); - } - - ComponentInteractionCreatedEventArgs? result = await - this.ComponentEventWaiter - .WaitForMatchAsync(new(message, c => IsSelect(c.Interaction.Data.ComponentType) && predicate(c), token)) - ; - - return new(result is null, result); - } - - /// - /// Waits for a dropdown to be interacted with. - /// - /// This is here for backwards-compatibility and will internally create a cancellation token. - /// The message to wait on. - /// The Id of the dropdown to wait on. - /// Override the timeout period specified in . - /// Thrown when the message does not have any dropdowns or any dropdown with the specified Id. - public Task> WaitForSelectAsync(DiscordMessage message, string id, TimeSpan? timeoutOverride = null) - => WaitForSelectAsync(message, id, GetCancellationToken(timeoutOverride)); - - /// - /// Waits for a dropdown to be interacted with. - /// - /// The message to wait on. - /// The Id of the dropdown to wait on. - /// A custom cancellation token that can be cancelled at any point. - /// Thrown when the message does not have any dropdowns or any dropdown with the specified Id. - public async Task> WaitForSelectAsync(DiscordMessage message, string id, CancellationToken token) - { - if (message.Author != this.Client.CurrentUser) - { - throw new InvalidOperationException("Interaction events are only sent to the application that created them."); - } - - if (message.Components.Count == 0) - { - throw new ArgumentException("Provided message does not contain any components."); - } - - if (!message.FilterComponents().Any(IsSelect)) - { - throw new ArgumentException("Provided message does not contain any select components."); - } - - if (message.FilterComponents().Where(IsSelect).All(c => c.CustomId != id)) - { - throw new ArgumentException($"Provided message does not contain select component with Id of '{id}'."); - } - - ComponentInteractionCreatedEventArgs? result = await - this.ComponentEventWaiter - .WaitForMatchAsync(new(message, (c) => IsSelect(c.Interaction.Data.ComponentType) && c.Id == id, token)) - ; - - return new(result is null, result); - } - - private bool IsSelect(DiscordComponent component) - => IsSelect(component.Type); - - private static bool IsSelect(DiscordComponentType type) - => type is - DiscordComponentType.StringSelect or - DiscordComponentType.UserSelect or - DiscordComponentType.RoleSelect or - DiscordComponentType.MentionableSelect or - DiscordComponentType.ChannelSelect; - - /// - /// Waits for a dropdown to be interacted with by a specific user. - /// - /// The message to wait on. - /// The user to wait on. - /// The Id of the dropdown to wait on. - /// Override the timeout period specified in . - /// Thrown when the message does not have any dropdowns or any dropdown with the specified Id. - public Task> WaitForSelectAsync(DiscordMessage message, DiscordUser user, string id, TimeSpan? timeoutOverride = null) - => WaitForSelectAsync(message, user, id, GetCancellationToken(timeoutOverride)); - - /// - /// Waits for a dropdown to be interacted with by a specific user. - /// - /// The message to wait on. - /// The user to wait on. - /// The Id of the dropdown to wait on. - /// A custom cancellation token that can be cancelled at any point. - /// Thrown when the message does not have any dropdowns or any dropdown with the specified Id. - public async Task> WaitForSelectAsync(DiscordMessage message, DiscordUser user, string id, CancellationToken token) - { - if (message.Author != this.Client.CurrentUser) - { - throw new InvalidOperationException("Interaction events are only sent to the application that created them."); - } - - if (message.Components.Count == 0) - { - throw new ArgumentException("Provided message does not contain any components."); - } - - if (!message.FilterComponents().Any(IsSelect)) - { - throw new ArgumentException("Provided message does not contain any select components."); - } - - if (message.FilterComponents().Where(IsSelect).All(c => c.CustomId != id)) - { - throw new ArgumentException($"Provided message does not contain select component with Id of '{id}'."); - } - - ComponentInteractionCreatedEventArgs? result = await - this.ComponentEventWaiter - .WaitForMatchAsync(new(message, (c) => c.Id == id && c.User == user, token)); - - return new(result is null, result); - } - - /// - /// Waits for a specific message. - /// - /// Predicate to match. - /// override timeout period. - /// - public async Task> WaitForMessageAsync(Func predicate, - TimeSpan? timeoutoverride = null) - { - if (!Utilities.HasMessageIntents(this.Client.Intents)) - { - throw new InvalidOperationException("No message intents are enabled."); - } - - TimeSpan timeout = timeoutoverride ?? this.Config.Timeout; - MessageCreatedEventArgs? returns = await this.MessageCreatedWaiter.WaitForMatchAsync(new MatchRequest(x => predicate(x.Message), timeout)); - - return new InteractivityResult(returns == null, returns?.Message); - } - - /// - /// Wait for a specific reaction. - /// - /// Predicate to match. - /// override timeout period. - /// - public async Task> WaitForReactionAsync(Func predicate, - TimeSpan? timeoutoverride = null) - { - if (!Utilities.HasReactionIntents(this.Client.Intents)) - { - throw new InvalidOperationException("No reaction intents are enabled."); - } - - TimeSpan timeout = timeoutoverride ?? this.Config.Timeout; - MessageReactionAddedEventArgs? returns = await this.MessageReactionAddWaiter.WaitForMatchAsync(new MatchRequest(predicate, timeout)); - - return new InteractivityResult(returns == null, returns); - } - - /// - /// Wait for a specific reaction. - /// For this Event you need the intent specified in - /// - /// Message reaction was added to. - /// User that made the reaction. - /// override timeout period. - /// - public async Task> WaitForReactionAsync(DiscordMessage message, DiscordUser user, - TimeSpan? timeoutoverride = null) - => await WaitForReactionAsync(x => x.User.Id == user.Id && x.Message.Id == message.Id, timeoutoverride); - - /// - /// Waits for a specific reaction. - /// For this Event you need the intent specified in - /// - /// Predicate to match. - /// Message reaction was added to. - /// User that made the reaction. - /// override timeout period. - /// - public async Task> WaitForReactionAsync(Func predicate, - DiscordMessage message, DiscordUser user, TimeSpan? timeoutoverride = null) - => await WaitForReactionAsync(x => predicate(x) && x.User.Id == user.Id && x.Message.Id == message.Id, timeoutoverride); - - /// - /// Waits for a specific reaction. - /// For this Event you need the intent specified in - /// - /// predicate to match. - /// User that made the reaction. - /// Override timeout period. - /// - public async Task> WaitForReactionAsync(Func predicate, - DiscordUser user, TimeSpan? timeoutoverride = null) - => await WaitForReactionAsync(x => predicate(x) && x.User.Id == user.Id, timeoutoverride); - - /// - /// Waits for a user to start typing. - /// - /// User that starts typing. - /// Channel the user is typing in. - /// Override timeout period. - /// - public async Task> WaitForUserTypingAsync(DiscordUser user, - DiscordChannel channel, TimeSpan? timeoutoverride = null) - { - if (!Utilities.HasTypingIntents(this.Client.Intents)) - { - throw new InvalidOperationException("No typing intents are enabled."); - } - - TimeSpan timeout = timeoutoverride ?? this.Config.Timeout; - TypingStartedEventArgs? returns = await this.TypingStartWaiter.WaitForMatchAsync( - new MatchRequest(x => x.User.Id == user.Id && x.Channel.Id == channel.Id, timeout)) - ; - - return new InteractivityResult(returns == null, returns); - } - - /// - /// Waits for a user to start typing. - /// - /// User that starts typing. - /// Override timeout period. - /// - public async Task> WaitForUserTypingAsync(DiscordUser user, TimeSpan? timeoutoverride = null) - { - if (!Utilities.HasTypingIntents(this.Client.Intents)) - { - throw new InvalidOperationException("No typing intents are enabled."); - } - - TimeSpan timeout = timeoutoverride ?? this.Config.Timeout; - TypingStartedEventArgs? returns = await this.TypingStartWaiter.WaitForMatchAsync( - new MatchRequest(x => x.User.Id == user.Id, timeout)) - ; - - return new InteractivityResult(returns == null, returns); - } - - /// - /// Waits for any user to start typing. - /// - /// Channel to type in. - /// Override timeout period. - /// - public async Task> WaitForTypingAsync(DiscordChannel channel, TimeSpan? timeoutoverride = null) - { - if (!Utilities.HasTypingIntents(this.Client.Intents)) - { - throw new InvalidOperationException("No typing intents are enabled."); - } - - TimeSpan timeout = timeoutoverride ?? this.Config.Timeout; - TypingStartedEventArgs? returns = await this.TypingStartWaiter.WaitForMatchAsync( - new MatchRequest(x => x.Channel.Id == channel.Id, timeout)) - ; - - return new InteractivityResult(returns == null, returns); - } - - /// - /// Collects reactions on a specific message. - /// - /// Message to collect reactions on. - /// Override timeout period. - /// - public async Task> CollectReactionsAsync(DiscordMessage m, TimeSpan? timeoutoverride = null) - { - if (!Utilities.HasReactionIntents(this.Client.Intents)) - { - throw new InvalidOperationException("No reaction intents are enabled."); - } - - TimeSpan timeout = timeoutoverride ?? this.Config.Timeout; - ReadOnlyCollection collection = await this.ReactionCollector.CollectAsync(new ReactionCollectRequest(m, timeout)); - - return collection; - } - - /// - /// Waits for specific event args to be received. Make sure the appropriate are registered, if needed. - /// - /// - /// - /// - /// - public async Task> WaitForEventArgsAsync(Func predicate, TimeSpan? timeoutoverride = null) where T : AsyncEventArgs - { - TimeSpan timeout = timeoutoverride ?? this.Config.Timeout; - - using EventWaiter waiter = new(this); - T? res = await waiter.WaitForMatchAsync(new MatchRequest(predicate, timeout)); - return new InteractivityResult(res == null, res); - } - - public async Task> CollectEventArgsAsync(Func predicate, TimeSpan? timeoutoverride = null) where T : AsyncEventArgs - { - TimeSpan timeout = timeoutoverride ?? this.Config.Timeout; - - using EventWaiter waiter = new(this); - ReadOnlyCollection res = await waiter.CollectMatchesAsync(new CollectRequest(predicate, timeout)); - return res; - } - - /// - /// Sends a paginated message with buttons. - /// - /// The channel to send it on. - /// User to give control. - /// The pages. - /// Pagination buttons (pass null to use buttons defined in ). - /// Pagination behaviour. - /// Deletion behaviour - /// A custom cancellation token that can be cancelled at any point. - // Ideally this would take a [list of] builder(s), but there's complications with muddying APIs further than we already do. - public async Task SendPaginatedMessageAsync( - DiscordChannel channel, DiscordUser user, IEnumerable pages, PaginationButtons buttons, - PaginationBehaviour? behaviour = default, ButtonPaginationBehavior? deletion = default, CancellationToken token = default) - { - PaginationBehaviour bhv = behaviour ?? this.Config.PaginationBehaviour; - ButtonPaginationBehavior del = deletion ?? this.Config.ButtonBehavior; - PaginationButtons bts = buttons ?? this.Config.PaginationButtons; - - bts = new(bts); - - Page[] pageArray = pages.ToArray(); - - if (pageArray.Length == 1) - { - bts.SkipLeft.Disable(); - bts.Left.Disable(); - bts.Right.Disable(); - bts.SkipRight.Disable(); - } - - if (bhv is PaginationBehaviour.Ignore) - { - bts.SkipLeft.Disable(); - bts.Left.Disable(); - - if (pageArray.Length == 2) - { - bts.SkipRight.Disable(); - } - } - - DiscordMessageBuilder builder = new DiscordMessageBuilder() - .WithContent(pageArray[0].Content) - .AddEmbed(pageArray[0].Embed) - .AddActionRowComponent(bts.ButtonArray); - - if (pageArray[0].Components is [..] pac) - { - foreach (DiscordActionRowComponent actionRow in pac) - { - builder.AddActionRowComponent(actionRow); - } - } - - DiscordMessage message = await builder.SendAsync(channel); - - ButtonPaginationRequest req = new(message, user, bhv, del, bts, pageArray, token == default ? GetCancellationToken() : token); - - await this.compPaginator.DoPaginationAsync(req); - } - - /// - /// Sends a paginated message with buttons. - /// - /// The channel to send it on. - /// User to give control. - /// The pages. - /// Pagination buttons (pass null to use buttons defined in ). - /// Pagination behaviour. - /// Deletion behaviour - /// Override timeout period. - public Task SendPaginatedMessageAsync( - DiscordChannel channel, DiscordUser user, IEnumerable pages, PaginationButtons buttons, TimeSpan? timeoutoverride, - PaginationBehaviour? behaviour = default, ButtonPaginationBehavior? deletion = default) - => SendPaginatedMessageAsync(channel, user, pages, buttons, behaviour, deletion, GetCancellationToken(timeoutoverride)); - - /// - /// This is the "default" overload for SendPaginatedMessageAsync, and will use buttons. Feel free to specify default(PaginationEmojis) to use reactions and emojis specified in , instead. - public Task SendPaginatedMessageAsync(DiscordChannel channel, DiscordUser user, IEnumerable pages, PaginationBehaviour? behaviour = default, ButtonPaginationBehavior? deletion = default, CancellationToken token = default) - => SendPaginatedMessageAsync(channel, user, pages, default, behaviour, deletion, token); - - /// - /// This is the "default" overload for SendPaginatedMessageAsync, and will use buttons. Feel free to specify default(PaginationEmojis) to use reactions and emojis specified in , instead. - public Task SendPaginatedMessageAsync(DiscordChannel channel, DiscordUser user, IEnumerable pages, TimeSpan? timeoutoverride, PaginationBehaviour? behaviour = default, ButtonPaginationBehavior? deletion = default) - => SendPaginatedMessageAsync(channel, user, pages, default, timeoutoverride, behaviour, deletion); - - /// - /// Sends a paginated message. - /// For this Event you need the intent specified in - /// - /// Channel to send paginated message in. - /// User to give control. - /// Pages. - /// Pagination emojis. - /// Pagination behaviour (when hitting max and min indices). - /// Deletion behaviour. - /// Override timeout period. - public async Task SendPaginatedMessageAsync(DiscordChannel channel, DiscordUser user, IEnumerable pages, PaginationEmojis emojis, - PaginationBehaviour? behaviour = default, PaginationDeletion? deletion = default, TimeSpan? timeoutoverride = null) - { - Page[] pageArray = pages.ToArray(); - Page firstPage = pageArray.First(); - DiscordMessageBuilder builder = new DiscordMessageBuilder() - .WithContent(firstPage.Content) - .AddEmbed(firstPage.Embed); - DiscordMessage m = await builder.SendAsync(channel); - - TimeSpan timeout = timeoutoverride ?? this.Config.Timeout; - - PaginationBehaviour bhv = behaviour ?? this.Config.PaginationBehaviour; - PaginationDeletion del = deletion ?? this.Config.PaginationDeletion; - PaginationEmojis ems = emojis ?? this.Config.PaginationEmojis; - - PaginationRequest prequest = new(m, user, bhv, del, ems, timeout, pageArray); - - await this.Paginator.DoPaginationAsync(prequest); - } - - /// - /// Sends a paginated message in response to an interaction. - /// - /// Pass the interaction directly. Interactivity will ACK it. - /// - /// - /// The interaction to create a response to. - /// Whether the response should be ephemeral. - /// The user to listen for button presses from. - /// The pages to paginate. - /// Optional: custom buttons - /// Pagination behaviour. - /// Deletion behaviour - /// Whether to disable or remove the buttons if there is only one page - /// Disabled buttons - /// A custom cancellation token that can be cancelled at any point. - public async Task SendPaginatedResponseAsync - ( - DiscordInteraction interaction, - bool ephemeral, - DiscordUser user, - IEnumerable pages, - PaginationButtons buttons = null, - PaginationBehaviour? behaviour = default, - ButtonPaginationBehavior? deletion = default, - ButtonDisableBehavior disableBehavior = ButtonDisableBehavior.Disable, - List disabledButtons = null, - CancellationToken token = default - ) - { - PaginationBehaviour bhv = behaviour ?? this.Config.PaginationBehaviour; - ButtonPaginationBehavior del = deletion ?? this.Config.ButtonBehavior; - PaginationButtons bts = buttons ?? this.Config.PaginationButtons; - disabledButtons ??= []; - Page[] pageArray = pages.ToArray(); - - bts = new PaginationButtons(bts); // Copy // - - if (pageArray.Length == 1) - { - if (disableBehavior == ButtonDisableBehavior.Disable) - { - bts.SkipLeft.Disable(); - bts.Left.Disable(); - bts.Right.Disable(); - bts.SkipRight.Disable(); - } - else - { - disabledButtons - .AddRange(new[] { PaginationButtonType.Left, PaginationButtonType.Right, PaginationButtonType.SkipLeft, PaginationButtonType.SkipRight }); - } - } - - if (bhv is PaginationBehaviour.Ignore) - { - if (disableBehavior == ButtonDisableBehavior.Disable) - { - bts.SkipLeft.Disable(); - bts.Left.Disable(); - } - else - { - disabledButtons.AddRange(new[] { PaginationButtonType.SkipLeft, PaginationButtonType.Left }); - } - - if (pageArray.Length == 2) - { - if (disableBehavior == ButtonDisableBehavior.Disable) - { - bts.SkipRight.Disable(); - } - else - { - disabledButtons.AddRange(new[] { PaginationButtonType.SkipRight }); - } - - } - - } - - DiscordMessage message; - DiscordButtonComponent[] buttonArray = bts.ButtonArray; - if (disabledButtons.Count != 0) - { - List buttonList = [.. buttonArray]; - if (disabledButtons.Contains(PaginationButtonType.Left)) - { - buttonList.Remove(bts.Left); - } - if (disabledButtons.Contains(PaginationButtonType.Right)) - { - buttonList.Remove(bts.Right); - } - if (disabledButtons.Contains(PaginationButtonType.SkipLeft)) - { - buttonList.Remove(bts.SkipLeft); - } - if (disabledButtons.Contains(PaginationButtonType.SkipRight)) - { - buttonList.Remove(bts.SkipRight); - } - if (disabledButtons.Contains(PaginationButtonType.Stop)) - { - buttonList.Remove(bts.Stop); - } - - buttonArray = [.. buttonList]; - } - - - - if (interaction.ResponseState != DiscordInteractionResponseState.Unacknowledged) - { - DiscordWebhookBuilder builder = new DiscordWebhookBuilder() - .WithContent(pageArray[0].Content) - .AddEmbed(pageArray[0].Embed) - .AddActionRowComponent(buttonArray); - - if (pageArray[0].Components is [..] pageArrayComponents) - { - foreach (DiscordActionRowComponent actionRow in pageArrayComponents) - { - builder.AddActionRowComponent(actionRow); - } - } - - message = await interaction.EditOriginalResponseAsync(builder); - } - else - { - DiscordInteractionResponseBuilder builder = new DiscordInteractionResponseBuilder() - .WithContent(pageArray[0].Content) - .AddEmbed(pageArray[0].Embed) - .AsEphemeral(ephemeral) - .AddActionRowComponent(buttonArray); - - if (pageArray[0].Components is [..] pageArrayComponents) - { - foreach (DiscordActionRowComponent actionRow in pageArrayComponents) - { - builder.AddActionRowComponent(actionRow); - } - } - - await interaction.CreateResponseAsync(DiscordInteractionResponseType.ChannelMessageWithSource, builder); - message = await interaction.GetOriginalResponseAsync(); - } - - InteractionPaginationRequest req = new(interaction, message, user, bhv, del, bts, pageArray, token); - - await this.compPaginator.DoPaginationAsync(req); - } - - /// - /// Waits for a custom pagination request to finish. - /// This does NOT handle removing emojis after finishing for you. - /// - /// - /// - public async Task WaitForCustomPaginationAsync(IPaginationRequest request) => await this.Paginator.DoPaginationAsync(request); - - /// - /// Waits for custom button-based pagination request to finish. - ///
- /// This does not invoke . - ///
- /// The request to wait for. - public async Task WaitForCustomComponentPaginationAsync(IPaginationRequest request) => await this.compPaginator.DoPaginationAsync(request); - - /// - /// Generates pages from a string, and puts them in message content. - /// - /// Input string. - /// How to split input string. - /// - public static IEnumerable GeneratePagesInContent(string input, SplitType splittype = SplitType.Character) - { - if (string.IsNullOrEmpty(input)) - { - throw new ArgumentException("You must provide a string that is not null or empty!"); - } - - List result = []; - List split; - - switch (splittype) - { - default: - case SplitType.Character: - split = [.. SplitString(input, 500)]; - break; - case SplitType.Line: - string[] subsplit = input.Split('\n'); - - split = []; - string s = ""; - - for (int i = 0; i < subsplit.Length; i++) - { - s += subsplit[i]; - if (i >= 15 && i % 15 == 0) - { - split.Add(s); - s = ""; - } - } - if (s != "" && split.All(x => x != s)) - { - split.Add(s); - } - - break; - } - - int page = 1; - foreach (string s in split) - { - result.Add(new Page($"Page {page}:\n{s}")); - page++; - } - - return result; - } - - /// - /// Generates pages from a string, and puts them in message embeds. - /// - /// Input string. - /// How to split input string. - /// Base embed for output embeds. - /// - public static IEnumerable GeneratePagesInEmbed(string input, SplitType splittype = SplitType.Character, DiscordEmbedBuilder embedbase = null) - { - if (string.IsNullOrEmpty(input)) - { - throw new ArgumentException("You must provide a string that is not null or empty!"); - } - - DiscordEmbedBuilder embed = embedbase ?? new DiscordEmbedBuilder(); - - List result = []; - List split; - - switch (splittype) - { - default: - case SplitType.Character: - split = [.. SplitString(input, 500)]; - break; - case SplitType.Line: - string[] subsplit = input.Split('\n'); - - split = []; - string s = ""; - - for (int i = 0; i < subsplit.Length; i++) - { - s += $"{subsplit[i]}\n"; - if (i % 15 == 0 && i != 0) - { - split.Add(s); - s = ""; - } - } - if (s != "" && split.All(x => x != s)) - { - split.Add(s); - } - - break; - } - - int page = 1; - foreach (string s in split) - { - result.Add(new Page("", new DiscordEmbedBuilder(embed).WithDescription(s).WithFooter($"Page {page}/{split.Count}"))); - page++; - } - - return result; - } - - private static List SplitString(string str, int chunkSize) - { - List res = []; - int len = str.Length; - int i = 0; - - while (i < len) - { - int size = Math.Min(len - i, chunkSize); - res.Add(str.Substring(i, size)); - i += size; - } - - return res; - } - - private CancellationToken GetCancellationToken(TimeSpan? timeout = null) => new CancellationTokenSource(timeout ?? this.Config.Timeout).Token; - - public void Dispose() - { - this.ComponentEventWaiter?.Dispose(); - this.ModalEventWaiter?.Dispose(); - this.ReactionCollector?.Dispose(); - this.ComponentInteractionWaiter?.Dispose(); - this.MessageCreatedWaiter?.Dispose(); - this.MessageReactionAddWaiter?.Dispose(); - this.Paginator?.Dispose(); - this.Poller?.Dispose(); - this.TypingStartWaiter?.Dispose(); - this.compPaginator?.Dispose(); - - // Satisfy rule CA1816. Can be removed if this class is sealed. - GC.SuppressFinalize(this); - } -} +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using DSharpPlus.AsyncEvents; +using DSharpPlus.Entities; +using DSharpPlus.EventArgs; +using DSharpPlus.Interactivity.Enums; +using DSharpPlus.Interactivity.EventHandling; + +namespace DSharpPlus.Interactivity; + +/// +/// Extension class for DSharpPlus.Interactivity +/// +public class InteractivityExtension : IDisposable +{ + internal readonly ConcurrentDictionary eventDistributor = []; + internal IClientErrorHandler errorHandler; + +#pragma warning disable IDE1006 // Naming Styles + internal InteractivityConfiguration Config { get; } + public DiscordClient Client { get; private set; } + + private EventWaiter MessageCreatedWaiter; + + private EventWaiter MessageReactionAddWaiter; + + private EventWaiter TypingStartWaiter; + + private EventWaiter ComponentInteractionWaiter; + + internal ComponentEventWaiter ComponentEventWaiter; + + internal ModalEventWaiter ModalEventWaiter; + + internal ReactionCollector ReactionCollector; + + internal Poller Poller; + + internal Paginator Paginator; + internal ComponentPaginator compPaginator; + +#pragma warning restore IDE1006 // Naming Styles + + internal InteractivityExtension(InteractivityConfiguration cfg) => this.Config = new InteractivityConfiguration(cfg); + + public void Setup(DiscordClient client) + { + this.Client = client; + this.MessageCreatedWaiter = new EventWaiter(this); + this.MessageReactionAddWaiter = new EventWaiter(this); + this.ComponentInteractionWaiter = new EventWaiter(this); + this.TypingStartWaiter = new EventWaiter(this); + this.Poller = new Poller(this.Client); + this.ReactionCollector = new ReactionCollector(this); + this.Paginator = new Paginator(this.Client); + this.compPaginator = new(this.Client, this.Config); + this.ComponentEventWaiter = new(this.Client, this.Config); + this.ModalEventWaiter = new(this.Client); + this.errorHandler = new DefaultClientErrorHandler(this.Client.Logger); + } + + /// + /// Makes a poll and returns poll results. + /// + /// Message to create poll on. + /// Emojis to use for this poll. + /// What to do when the poll ends. + /// override timeout period. + /// + public async Task> DoPollAsync(DiscordMessage m, IEnumerable emojis, PollBehaviour? behaviour = default, TimeSpan? timeout = null) + { + if (!Utilities.HasReactionIntents(this.Client.Intents)) + { + throw new InvalidOperationException("No reaction intents are enabled."); + } + + if (!emojis.Any()) + { + throw new ArgumentException("You need to provide at least one emoji for a poll!"); + } + + foreach (DiscordEmoji em in emojis) + { + await m.CreateReactionAsync(em); + } + + ReadOnlyCollection res = await this.Poller.DoPollAsync(new PollRequest(m, timeout ?? this.Config.Timeout, emojis)); + + PollBehaviour pollbehaviour = behaviour ?? this.Config.PollBehaviour; + DiscordMember thismember = await m.Channel.Guild.GetMemberAsync(this.Client.CurrentUser.Id); + + if (pollbehaviour == PollBehaviour.DeleteEmojis && m.Channel.PermissionsFor(thismember).HasPermission(DiscordPermission.ManageMessages)) + { + await m.DeleteAllReactionsAsync(); + } + + return new ReadOnlyCollection(res.ToList()); + } + + /// + /// Waits for a modal with the specified id to be submitted. + /// + /// The id of the modal to wait for. Should be unique to avoid issues. + /// Override the timeout period in . + /// A with a modal if the interactivity did not time out. + public Task> WaitForModalAsync(string modal_id, TimeSpan? timeoutOverride = null) + => WaitForModalAsync(modal_id, GetCancellationToken(timeoutOverride)); + + /// + /// Waits for a modal with the specified id to be submitted. + /// + /// The id of the modal to wait for. Should be unique to avoid issues. + /// A custom cancellation token that can be cancelled at any point. + /// A with a modal if the interactivity did not time out. + public async Task> WaitForModalAsync(string modal_id, CancellationToken token) + { + if (string.IsNullOrEmpty(modal_id) || modal_id.Length > 100) + { + throw new ArgumentException("Custom ID must be between 1 and 100 characters."); + } + + ModalMatchRequest matchRequest = new(modal_id, + c => c.Interaction.Data.CustomId == modal_id, cancellation: token); + ModalSubmittedEventArgs? result = await this.ModalEventWaiter.WaitForMatchAsync(matchRequest); + + return new(result is null, result); + } + + /// + /// Waits for a modal with the specified custom id to be submitted by the given user. + /// + /// The id of the modal to wait for. Should be unique to avoid issues. + /// The user to wait for the modal from. + /// Override the timeout period in . + /// A with a modal if the interactivity did not time out. + public Task> WaitForModalAsync(string modal_id, DiscordUser user, TimeSpan? timeoutOverride = null) + => WaitForModalAsync(modal_id, user, GetCancellationToken(timeoutOverride)); + + /// + /// Waits for a modal with the specified custom id to be submitted by the given user. + /// + /// The id of the modal to wait for. Should be unique to avoid issues. + /// The user to wait for the modal from. + /// A custom cancellation token that can be cancelled at any point. + /// A with a modal if the interactivity did not time out. + public async Task> WaitForModalAsync(string modal_id, DiscordUser user, CancellationToken token) + { + if (string.IsNullOrEmpty(modal_id) || modal_id.Length > 100) + { + throw new ArgumentException("Custom ID must be between 1 and 100 characters."); + } + + ModalMatchRequest matchRequest = new(modal_id, + c => c.Interaction.Data.CustomId == modal_id && + c.Interaction.User.Id == user.Id, cancellation: token); + ModalSubmittedEventArgs? result = await this.ModalEventWaiter.WaitForMatchAsync(matchRequest); + + return new(result is null, result); + } + + /// + /// Waits for any button in the specified collection to be pressed. + /// + /// The message to wait on. + /// A collection of buttons to listen for. + /// Override the timeout period in . + /// A with the result of button that was pressed, if any. + /// Thrown when attempting to wait for a message that is not authored by the current user. + /// Thrown when the message does not contain a button with the specified Id, or any buttons at all. + public Task> WaitForButtonAsync(DiscordMessage message, IEnumerable buttons, TimeSpan? timeoutOverride = null) + => WaitForButtonAsync(message, buttons, GetCancellationToken(timeoutOverride)); + + /// + /// Waits for any button in the specified collection to be pressed. + /// + /// The message to wait on. + /// A collection of buttons to listen for. + /// A custom cancellation token that can be cancelled at any point. + /// A with the result of button that was pressed, if any. + /// Thrown when attempting to wait for a message that is not authored by the current user. + /// Thrown when the message does not contain a button with the specified Id, or any buttons at all. + public async Task> WaitForButtonAsync(DiscordMessage message, IEnumerable buttons, CancellationToken token) + { + if (message.Author != this.Client.CurrentUser) + { + throw new InvalidOperationException("Interaction events are only sent to the application that created them."); + } + + if (!buttons.Any()) + { + throw new ArgumentException("You must specify at least one button to listen for."); + } + + if (message.Components.Count == 0) + { + throw new ArgumentException("Provided message does not contain any components."); + } + + if (message.FilterComponents().Count == 0) + { + throw new ArgumentException("Provided message does not contain any button components."); + } + + ComponentInteractionCreatedEventArgs? res = await this.ComponentEventWaiter + .WaitForMatchAsync(new(message, + c => + c.Interaction.Data.ComponentType == DiscordComponentType.Button && + buttons.Any(b => b.CustomId == c.Id), token)); + + return new(res is null, res); + } + + /// + /// Waits for any button on the specified message to be pressed. + /// + /// The message to wait for the button on. + /// Override the timeout period specified in . + /// A with the result of button that was pressed, if any. + /// Thrown when attempting to wait for a message that is not authored by the current user. + /// Thrown when the message does not contain a button with the specified Id, or any buttons at all. + public Task> WaitForButtonAsync(DiscordMessage message, TimeSpan? timeoutOverride = null) + => WaitForButtonAsync(message, GetCancellationToken(timeoutOverride)); + + /// + /// Waits for any button on the specified message to be pressed. + /// + /// The message to wait for the button on. + /// A custom cancellation token that can be cancelled at any point. + /// A with the result of button that was pressed, if any. + /// Thrown when attempting to wait for a message that is not authored by the current user. + /// Thrown when the message does not contain a button with the specified Id, or any buttons at all. + public async Task> WaitForButtonAsync(DiscordMessage message, CancellationToken token) + { + if (message.Author != this.Client.CurrentUser) + { + throw new InvalidOperationException("Interaction events are only sent to the application that created them."); + } + + if (message.Components.Count == 0) + { + throw new ArgumentException("Provided message does not contain any components."); + } + + if (message.FilterComponents().Count == 0) + { + throw new ArgumentException("Provided message does not contain any button components."); + } + + IEnumerable ids = message.FilterComponents().Select(c => c.CustomId); + + ComponentInteractionCreatedEventArgs? result = + await + this.ComponentEventWaiter + .WaitForMatchAsync(new(message, c => c.Interaction.Data.ComponentType == DiscordComponentType.Button && ids.Contains(c.Id), token)) + ; + + return new(result is null, result); + } + + /// + /// Waits for any button on the specified message to be pressed by the specified user. + /// + /// The message to wait for the button on. + /// The user to wait for the button press from. + /// Override the timeout period specified in . + /// A with the result of button that was pressed, if any. + /// Thrown when attempting to wait for a message that is not authored by the current user. + /// Thrown when the message does not contain a button with the specified Id, or any buttons at all. + public Task> WaitForButtonAsync(DiscordMessage message, DiscordUser user, TimeSpan? timeoutOverride = null) + => WaitForButtonAsync(message, user, GetCancellationToken(timeoutOverride)); + + /// + /// Waits for any button on the specified message to be pressed by the specified user. + /// + /// The message to wait for the button on. + /// The user to wait for the button press from. + /// A custom cancellation token that can be cancelled at any point. + /// A with the result of button that was pressed, if any. + /// Thrown when attempting to wait for a message that is not authored by the current user. + /// Thrown when the message does not contain a button with the specified Id, or any buttons at all. + public async Task> WaitForButtonAsync(DiscordMessage message, DiscordUser user, CancellationToken token) + { + if (message.Author != this.Client.CurrentUser) + { + throw new InvalidOperationException("Interaction events are only sent to the application that created them."); + } + + if (message.Components.Count == 0) + { + throw new ArgumentException("Provided message does not contain any components."); + } + + if (message.FilterComponents().Count == 0) + { + throw new ArgumentException("Provided message does not contain any button components."); + } + + ComponentInteractionCreatedEventArgs? result = await + this.ComponentEventWaiter + .WaitForMatchAsync(new(message, (c) => c.Interaction.Data.ComponentType is DiscordComponentType.Button && c.User == user, token)) + ; + + return new(result is null, result); + + } + + /// + /// Waits for a button with the specified Id to be pressed. + /// + /// The message to wait for the button on. + /// The Id of the button to wait for. + /// Override the timeout period specified in . + /// A with the result of the operation. + /// Thrown when attempting to wait for a message that is not authored by the current user. + /// Thrown when the message does not contain a button with the specified Id, or any buttons at all. + public Task> WaitForButtonAsync(DiscordMessage message, string id, TimeSpan? timeoutOverride = null) + => WaitForButtonAsync(message, id, GetCancellationToken(timeoutOverride)); + + /// + /// Waits for a button with the specified Id to be pressed. + /// + /// The message to wait for the button on. + /// The Id of the button to wait for. + /// Cancellation token. + /// A with the result of the operation. + /// Thrown when attempting to wait for a message that is not authored by the current user. + /// Thrown when the message does not contain a button with the specified Id, or any buttons at all. + public async Task> WaitForButtonAsync(DiscordMessage message, string id, CancellationToken token) + { + if (message.Author != this.Client.CurrentUser) + { + throw new InvalidOperationException("Interaction events are only sent to the application that created them."); + } + + if (message.Components.Count == 0) + { + throw new ArgumentException("Provided message does not contain any components."); + } + + if (message.FilterComponents().Count == 0) + { + throw new ArgumentException("Provided message does not contain any button components."); + } + + if (!message.FilterComponents().Any(c => c.CustomId == id)) + { + throw new ArgumentException($"Provided message does not contain button with Id of '{id}'."); + } + + ComponentInteractionCreatedEventArgs? result = await + this.ComponentEventWaiter + .WaitForMatchAsync(new(message, (c) => c.Interaction.Data.ComponentType is DiscordComponentType.Button && c.Id == id, token)) + ; + + return new(result is null, result); + } + + /// + /// Waits for any button to be interacted with. + /// + /// The message to wait on. + /// The predicate to filter interactions by. + /// Override the timeout specified in + public Task> WaitForButtonAsync(DiscordMessage message, Func predicate, TimeSpan? timeoutOverride = null) + => WaitForButtonAsync(message, predicate, GetCancellationToken(timeoutOverride)); + + /// + /// Waits for any button to be interacted with. + /// + /// The message to wait on. + /// The predicate to filter interactions by. + /// A token to cancel interactivity with at any time. Pass to wait indefinitely. + public async Task> WaitForButtonAsync(DiscordMessage message, Func predicate, CancellationToken token) + { + if (message.Author != this.Client.CurrentUser) + { + throw new InvalidOperationException("Interaction events are only sent to the application that created them."); + } + + if (message.Components.Count == 0) + { + throw new ArgumentException("Provided message does not contain any components."); + } + + if (message.FilterComponents().Count == 0) + { + throw new ArgumentException("Provided message does not contain any button components."); + } + + ComponentInteractionCreatedEventArgs? result = await + this.ComponentEventWaiter + .WaitForMatchAsync(new(message, c => c.Interaction.Data.ComponentType is DiscordComponentType.Button && predicate(c), token)) + ; + + return new(result is null, result); + } + + /// + /// Waits for any dropdown to be interacted with. + /// + /// The message to wait for. + /// A filter predicate. + /// Override the timeout period specified in . + /// Thrown when the message doesn't contain any dropdowns + public Task> WaitForSelectAsync(DiscordMessage message, Func predicate, TimeSpan? timeoutOverride = null) + => WaitForSelectAsync(message, predicate, GetCancellationToken(timeoutOverride)); + + /// + /// Waits for any dropdown to be interacted with. + /// + /// The message to wait for. + /// A filter predicate. + /// A token that can be used to cancel interactivity. Pass to wait indefinitely. + /// Thrown when the message doesn't contain any dropdowns + public async Task> WaitForSelectAsync(DiscordMessage message, Func predicate, CancellationToken token) + { + if (message.Author != this.Client.CurrentUser) + { + throw new InvalidOperationException("Interaction events are only sent to the application that created them."); + } + + if (message.Components.Count == 0) + { + throw new ArgumentException("Provided message does not contain any components."); + } + + if (!message.FilterComponents().Any(IsSelect)) + { + throw new ArgumentException("Provided message does not contain any select components."); + } + + ComponentInteractionCreatedEventArgs? result = await + this.ComponentEventWaiter + .WaitForMatchAsync(new(message, c => IsSelect(c.Interaction.Data.ComponentType) && predicate(c), token)) + ; + + return new(result is null, result); + } + + /// + /// Waits for a dropdown to be interacted with. + /// + /// This is here for backwards-compatibility and will internally create a cancellation token. + /// The message to wait on. + /// The Id of the dropdown to wait on. + /// Override the timeout period specified in . + /// Thrown when the message does not have any dropdowns or any dropdown with the specified Id. + public Task> WaitForSelectAsync(DiscordMessage message, string id, TimeSpan? timeoutOverride = null) + => WaitForSelectAsync(message, id, GetCancellationToken(timeoutOverride)); + + /// + /// Waits for a dropdown to be interacted with. + /// + /// The message to wait on. + /// The Id of the dropdown to wait on. + /// A custom cancellation token that can be cancelled at any point. + /// Thrown when the message does not have any dropdowns or any dropdown with the specified Id. + public async Task> WaitForSelectAsync(DiscordMessage message, string id, CancellationToken token) + { + if (message.Author != this.Client.CurrentUser) + { + throw new InvalidOperationException("Interaction events are only sent to the application that created them."); + } + + if (message.Components.Count == 0) + { + throw new ArgumentException("Provided message does not contain any components."); + } + + if (!message.FilterComponents().Any(IsSelect)) + { + throw new ArgumentException("Provided message does not contain any select components."); + } + + if (message.FilterComponents().Where(IsSelect).All(c => c.CustomId != id)) + { + throw new ArgumentException($"Provided message does not contain select component with Id of '{id}'."); + } + + ComponentInteractionCreatedEventArgs? result = await + this.ComponentEventWaiter + .WaitForMatchAsync(new(message, (c) => IsSelect(c.Interaction.Data.ComponentType) && c.Id == id, token)) + ; + + return new(result is null, result); + } + + private bool IsSelect(DiscordComponent component) + => IsSelect(component.Type); + + private static bool IsSelect(DiscordComponentType type) + => type is + DiscordComponentType.StringSelect or + DiscordComponentType.UserSelect or + DiscordComponentType.RoleSelect or + DiscordComponentType.MentionableSelect or + DiscordComponentType.ChannelSelect; + + /// + /// Waits for a dropdown to be interacted with by a specific user. + /// + /// The message to wait on. + /// The user to wait on. + /// The Id of the dropdown to wait on. + /// Override the timeout period specified in . + /// Thrown when the message does not have any dropdowns or any dropdown with the specified Id. + public Task> WaitForSelectAsync(DiscordMessage message, DiscordUser user, string id, TimeSpan? timeoutOverride = null) + => WaitForSelectAsync(message, user, id, GetCancellationToken(timeoutOverride)); + + /// + /// Waits for a dropdown to be interacted with by a specific user. + /// + /// The message to wait on. + /// The user to wait on. + /// The Id of the dropdown to wait on. + /// A custom cancellation token that can be cancelled at any point. + /// Thrown when the message does not have any dropdowns or any dropdown with the specified Id. + public async Task> WaitForSelectAsync(DiscordMessage message, DiscordUser user, string id, CancellationToken token) + { + if (message.Author != this.Client.CurrentUser) + { + throw new InvalidOperationException("Interaction events are only sent to the application that created them."); + } + + if (message.Components.Count == 0) + { + throw new ArgumentException("Provided message does not contain any components."); + } + + if (!message.FilterComponents().Any(IsSelect)) + { + throw new ArgumentException("Provided message does not contain any select components."); + } + + if (message.FilterComponents().Where(IsSelect).All(c => c.CustomId != id)) + { + throw new ArgumentException($"Provided message does not contain select component with Id of '{id}'."); + } + + ComponentInteractionCreatedEventArgs? result = await + this.ComponentEventWaiter + .WaitForMatchAsync(new(message, (c) => c.Id == id && c.User == user, token)); + + return new(result is null, result); + } + + /// + /// Waits for a specific message. + /// + /// Predicate to match. + /// override timeout period. + /// + public async Task> WaitForMessageAsync(Func predicate, + TimeSpan? timeoutoverride = null) + { + if (!Utilities.HasMessageIntents(this.Client.Intents)) + { + throw new InvalidOperationException("No message intents are enabled."); + } + + TimeSpan timeout = timeoutoverride ?? this.Config.Timeout; + MessageCreatedEventArgs? returns = await this.MessageCreatedWaiter.WaitForMatchAsync(new MatchRequest(x => predicate(x.Message), timeout)); + + return new InteractivityResult(returns == null, returns?.Message); + } + + /// + /// Wait for a specific reaction. + /// + /// Predicate to match. + /// override timeout period. + /// + public async Task> WaitForReactionAsync(Func predicate, + TimeSpan? timeoutoverride = null) + { + if (!Utilities.HasReactionIntents(this.Client.Intents)) + { + throw new InvalidOperationException("No reaction intents are enabled."); + } + + TimeSpan timeout = timeoutoverride ?? this.Config.Timeout; + MessageReactionAddedEventArgs? returns = await this.MessageReactionAddWaiter.WaitForMatchAsync(new MatchRequest(predicate, timeout)); + + return new InteractivityResult(returns == null, returns); + } + + /// + /// Wait for a specific reaction. + /// For this Event you need the intent specified in + /// + /// Message reaction was added to. + /// User that made the reaction. + /// override timeout period. + /// + public async Task> WaitForReactionAsync(DiscordMessage message, DiscordUser user, + TimeSpan? timeoutoverride = null) + => await WaitForReactionAsync(x => x.User.Id == user.Id && x.Message.Id == message.Id, timeoutoverride); + + /// + /// Waits for a specific reaction. + /// For this Event you need the intent specified in + /// + /// Predicate to match. + /// Message reaction was added to. + /// User that made the reaction. + /// override timeout period. + /// + public async Task> WaitForReactionAsync(Func predicate, + DiscordMessage message, DiscordUser user, TimeSpan? timeoutoverride = null) + => await WaitForReactionAsync(x => predicate(x) && x.User.Id == user.Id && x.Message.Id == message.Id, timeoutoverride); + + /// + /// Waits for a specific reaction. + /// For this Event you need the intent specified in + /// + /// predicate to match. + /// User that made the reaction. + /// Override timeout period. + /// + public async Task> WaitForReactionAsync(Func predicate, + DiscordUser user, TimeSpan? timeoutoverride = null) + => await WaitForReactionAsync(x => predicate(x) && x.User.Id == user.Id, timeoutoverride); + + /// + /// Waits for a user to start typing. + /// + /// User that starts typing. + /// Channel the user is typing in. + /// Override timeout period. + /// + public async Task> WaitForUserTypingAsync(DiscordUser user, + DiscordChannel channel, TimeSpan? timeoutoverride = null) + { + if (!Utilities.HasTypingIntents(this.Client.Intents)) + { + throw new InvalidOperationException("No typing intents are enabled."); + } + + TimeSpan timeout = timeoutoverride ?? this.Config.Timeout; + TypingStartedEventArgs? returns = await this.TypingStartWaiter.WaitForMatchAsync( + new MatchRequest(x => x.User.Id == user.Id && x.Channel.Id == channel.Id, timeout)) + ; + + return new InteractivityResult(returns == null, returns); + } + + /// + /// Waits for a user to start typing. + /// + /// User that starts typing. + /// Override timeout period. + /// + public async Task> WaitForUserTypingAsync(DiscordUser user, TimeSpan? timeoutoverride = null) + { + if (!Utilities.HasTypingIntents(this.Client.Intents)) + { + throw new InvalidOperationException("No typing intents are enabled."); + } + + TimeSpan timeout = timeoutoverride ?? this.Config.Timeout; + TypingStartedEventArgs? returns = await this.TypingStartWaiter.WaitForMatchAsync( + new MatchRequest(x => x.User.Id == user.Id, timeout)) + ; + + return new InteractivityResult(returns == null, returns); + } + + /// + /// Waits for any user to start typing. + /// + /// Channel to type in. + /// Override timeout period. + /// + public async Task> WaitForTypingAsync(DiscordChannel channel, TimeSpan? timeoutoverride = null) + { + if (!Utilities.HasTypingIntents(this.Client.Intents)) + { + throw new InvalidOperationException("No typing intents are enabled."); + } + + TimeSpan timeout = timeoutoverride ?? this.Config.Timeout; + TypingStartedEventArgs? returns = await this.TypingStartWaiter.WaitForMatchAsync( + new MatchRequest(x => x.Channel.Id == channel.Id, timeout)) + ; + + return new InteractivityResult(returns == null, returns); + } + + /// + /// Collects reactions on a specific message. + /// + /// Message to collect reactions on. + /// Override timeout period. + /// + public async Task> CollectReactionsAsync(DiscordMessage m, TimeSpan? timeoutoverride = null) + { + if (!Utilities.HasReactionIntents(this.Client.Intents)) + { + throw new InvalidOperationException("No reaction intents are enabled."); + } + + TimeSpan timeout = timeoutoverride ?? this.Config.Timeout; + ReadOnlyCollection collection = await this.ReactionCollector.CollectAsync(new ReactionCollectRequest(m, timeout)); + + return collection; + } + + /// + /// Waits for specific event args to be received. Make sure the appropriate are registered, if needed. + /// + /// + /// + /// + /// + public async Task> WaitForEventArgsAsync(Func predicate, TimeSpan? timeoutoverride = null) where T : AsyncEventArgs + { + TimeSpan timeout = timeoutoverride ?? this.Config.Timeout; + + using EventWaiter waiter = new(this); + T? res = await waiter.WaitForMatchAsync(new MatchRequest(predicate, timeout)); + return new InteractivityResult(res == null, res); + } + + public async Task> CollectEventArgsAsync(Func predicate, TimeSpan? timeoutoverride = null) where T : AsyncEventArgs + { + TimeSpan timeout = timeoutoverride ?? this.Config.Timeout; + + using EventWaiter waiter = new(this); + ReadOnlyCollection res = await waiter.CollectMatchesAsync(new CollectRequest(predicate, timeout)); + return res; + } + + /// + /// Sends a paginated message with buttons. + /// + /// The channel to send it on. + /// User to give control. + /// The pages. + /// Pagination buttons (pass null to use buttons defined in ). + /// Pagination behaviour. + /// Deletion behaviour + /// A custom cancellation token that can be cancelled at any point. + // Ideally this would take a [list of] builder(s), but there's complications with muddying APIs further than we already do. + public async Task SendPaginatedMessageAsync( + DiscordChannel channel, DiscordUser user, IEnumerable pages, PaginationButtons buttons, + PaginationBehaviour? behaviour = default, ButtonPaginationBehavior? deletion = default, CancellationToken token = default) + { + PaginationBehaviour bhv = behaviour ?? this.Config.PaginationBehaviour; + ButtonPaginationBehavior del = deletion ?? this.Config.ButtonBehavior; + PaginationButtons bts = buttons ?? this.Config.PaginationButtons; + + bts = new(bts); + + Page[] pageArray = pages.ToArray(); + + if (pageArray.Length == 1) + { + bts.SkipLeft.Disable(); + bts.Left.Disable(); + bts.Right.Disable(); + bts.SkipRight.Disable(); + } + + if (bhv is PaginationBehaviour.Ignore) + { + bts.SkipLeft.Disable(); + bts.Left.Disable(); + + if (pageArray.Length == 2) + { + bts.SkipRight.Disable(); + } + } + + DiscordMessageBuilder builder = new DiscordMessageBuilder() + .WithContent(pageArray[0].Content) + .AddEmbed(pageArray[0].Embed) + .AddActionRowComponent(bts.ButtonArray); + + if (pageArray[0].Components is [..] pac) + { + foreach (DiscordActionRowComponent actionRow in pac) + { + builder.AddActionRowComponent(actionRow); + } + } + + DiscordMessage message = await builder.SendAsync(channel); + + ButtonPaginationRequest req = new(message, user, bhv, del, bts, pageArray, token == default ? GetCancellationToken() : token); + + await this.compPaginator.DoPaginationAsync(req); + } + + /// + /// Sends a paginated message with buttons. + /// + /// The channel to send it on. + /// User to give control. + /// The pages. + /// Pagination buttons (pass null to use buttons defined in ). + /// Pagination behaviour. + /// Deletion behaviour + /// Override timeout period. + public Task SendPaginatedMessageAsync( + DiscordChannel channel, DiscordUser user, IEnumerable pages, PaginationButtons buttons, TimeSpan? timeoutoverride, + PaginationBehaviour? behaviour = default, ButtonPaginationBehavior? deletion = default) + => SendPaginatedMessageAsync(channel, user, pages, buttons, behaviour, deletion, GetCancellationToken(timeoutoverride)); + + /// + /// This is the "default" overload for SendPaginatedMessageAsync, and will use buttons. Feel free to specify default(PaginationEmojis) to use reactions and emojis specified in , instead. + public Task SendPaginatedMessageAsync(DiscordChannel channel, DiscordUser user, IEnumerable pages, PaginationBehaviour? behaviour = default, ButtonPaginationBehavior? deletion = default, CancellationToken token = default) + => SendPaginatedMessageAsync(channel, user, pages, default, behaviour, deletion, token); + + /// + /// This is the "default" overload for SendPaginatedMessageAsync, and will use buttons. Feel free to specify default(PaginationEmojis) to use reactions and emojis specified in , instead. + public Task SendPaginatedMessageAsync(DiscordChannel channel, DiscordUser user, IEnumerable pages, TimeSpan? timeoutoverride, PaginationBehaviour? behaviour = default, ButtonPaginationBehavior? deletion = default) + => SendPaginatedMessageAsync(channel, user, pages, default, timeoutoverride, behaviour, deletion); + + /// + /// Sends a paginated message. + /// For this Event you need the intent specified in + /// + /// Channel to send paginated message in. + /// User to give control. + /// Pages. + /// Pagination emojis. + /// Pagination behaviour (when hitting max and min indices). + /// Deletion behaviour. + /// Override timeout period. + public async Task SendPaginatedMessageAsync(DiscordChannel channel, DiscordUser user, IEnumerable pages, PaginationEmojis emojis, + PaginationBehaviour? behaviour = default, PaginationDeletion? deletion = default, TimeSpan? timeoutoverride = null) + { + Page[] pageArray = pages.ToArray(); + Page firstPage = pageArray.First(); + DiscordMessageBuilder builder = new DiscordMessageBuilder() + .WithContent(firstPage.Content) + .AddEmbed(firstPage.Embed); + DiscordMessage m = await builder.SendAsync(channel); + + TimeSpan timeout = timeoutoverride ?? this.Config.Timeout; + + PaginationBehaviour bhv = behaviour ?? this.Config.PaginationBehaviour; + PaginationDeletion del = deletion ?? this.Config.PaginationDeletion; + PaginationEmojis ems = emojis ?? this.Config.PaginationEmojis; + + PaginationRequest prequest = new(m, user, bhv, del, ems, timeout, pageArray); + + await this.Paginator.DoPaginationAsync(prequest); + } + + /// + /// Sends a paginated message in response to an interaction. + /// + /// Pass the interaction directly. Interactivity will ACK it. + /// + /// + /// The interaction to create a response to. + /// Whether the response should be ephemeral. + /// The user to listen for button presses from. + /// The pages to paginate. + /// Optional: custom buttons + /// Pagination behaviour. + /// Deletion behaviour + /// Whether to disable or remove the buttons if there is only one page + /// Disabled buttons + /// A custom cancellation token that can be cancelled at any point. + public async Task SendPaginatedResponseAsync + ( + DiscordInteraction interaction, + bool ephemeral, + DiscordUser user, + IEnumerable pages, + PaginationButtons buttons = null, + PaginationBehaviour? behaviour = default, + ButtonPaginationBehavior? deletion = default, + ButtonDisableBehavior disableBehavior = ButtonDisableBehavior.Disable, + List disabledButtons = null, + CancellationToken token = default + ) + { + PaginationBehaviour bhv = behaviour ?? this.Config.PaginationBehaviour; + ButtonPaginationBehavior del = deletion ?? this.Config.ButtonBehavior; + PaginationButtons bts = buttons ?? this.Config.PaginationButtons; + disabledButtons ??= []; + Page[] pageArray = pages.ToArray(); + + bts = new PaginationButtons(bts); // Copy // + + if (pageArray.Length == 1) + { + if (disableBehavior == ButtonDisableBehavior.Disable) + { + bts.SkipLeft.Disable(); + bts.Left.Disable(); + bts.Right.Disable(); + bts.SkipRight.Disable(); + } + else + { + disabledButtons + .AddRange(new[] { PaginationButtonType.Left, PaginationButtonType.Right, PaginationButtonType.SkipLeft, PaginationButtonType.SkipRight }); + } + } + + if (bhv is PaginationBehaviour.Ignore) + { + if (disableBehavior == ButtonDisableBehavior.Disable) + { + bts.SkipLeft.Disable(); + bts.Left.Disable(); + } + else + { + disabledButtons.AddRange(new[] { PaginationButtonType.SkipLeft, PaginationButtonType.Left }); + } + + if (pageArray.Length == 2) + { + if (disableBehavior == ButtonDisableBehavior.Disable) + { + bts.SkipRight.Disable(); + } + else + { + disabledButtons.AddRange(new[] { PaginationButtonType.SkipRight }); + } + + } + + } + + DiscordMessage message; + DiscordButtonComponent[] buttonArray = bts.ButtonArray; + if (disabledButtons.Count != 0) + { + List buttonList = [.. buttonArray]; + if (disabledButtons.Contains(PaginationButtonType.Left)) + { + buttonList.Remove(bts.Left); + } + if (disabledButtons.Contains(PaginationButtonType.Right)) + { + buttonList.Remove(bts.Right); + } + if (disabledButtons.Contains(PaginationButtonType.SkipLeft)) + { + buttonList.Remove(bts.SkipLeft); + } + if (disabledButtons.Contains(PaginationButtonType.SkipRight)) + { + buttonList.Remove(bts.SkipRight); + } + if (disabledButtons.Contains(PaginationButtonType.Stop)) + { + buttonList.Remove(bts.Stop); + } + + buttonArray = [.. buttonList]; + } + + + + if (interaction.ResponseState != DiscordInteractionResponseState.Unacknowledged) + { + DiscordWebhookBuilder builder = new DiscordWebhookBuilder() + .WithContent(pageArray[0].Content) + .AddEmbed(pageArray[0].Embed) + .AddActionRowComponent(buttonArray); + + if (pageArray[0].Components is [..] pageArrayComponents) + { + foreach (DiscordActionRowComponent actionRow in pageArrayComponents) + { + builder.AddActionRowComponent(actionRow); + } + } + + message = await interaction.EditOriginalResponseAsync(builder); + } + else + { + DiscordInteractionResponseBuilder builder = new DiscordInteractionResponseBuilder() + .WithContent(pageArray[0].Content) + .AddEmbed(pageArray[0].Embed) + .AsEphemeral(ephemeral) + .AddActionRowComponent(buttonArray); + + if (pageArray[0].Components is [..] pageArrayComponents) + { + foreach (DiscordActionRowComponent actionRow in pageArrayComponents) + { + builder.AddActionRowComponent(actionRow); + } + } + + await interaction.CreateResponseAsync(DiscordInteractionResponseType.ChannelMessageWithSource, builder); + message = await interaction.GetOriginalResponseAsync(); + } + + InteractionPaginationRequest req = new(interaction, message, user, bhv, del, bts, pageArray, token); + + await this.compPaginator.DoPaginationAsync(req); + } + + /// + /// Waits for a custom pagination request to finish. + /// This does NOT handle removing emojis after finishing for you. + /// + /// + /// + public async Task WaitForCustomPaginationAsync(IPaginationRequest request) => await this.Paginator.DoPaginationAsync(request); + + /// + /// Waits for custom button-based pagination request to finish. + ///
+ /// This does not invoke . + ///
+ /// The request to wait for. + public async Task WaitForCustomComponentPaginationAsync(IPaginationRequest request) => await this.compPaginator.DoPaginationAsync(request); + + /// + /// Generates pages from a string, and puts them in message content. + /// + /// Input string. + /// How to split input string. + /// + public static IEnumerable GeneratePagesInContent(string input, SplitType splittype = SplitType.Character) + { + if (string.IsNullOrEmpty(input)) + { + throw new ArgumentException("You must provide a string that is not null or empty!"); + } + + List result = []; + List split; + + switch (splittype) + { + default: + case SplitType.Character: + split = [.. SplitString(input, 500)]; + break; + case SplitType.Line: + string[] subsplit = input.Split('\n'); + + split = []; + string s = ""; + + for (int i = 0; i < subsplit.Length; i++) + { + s += subsplit[i]; + if (i >= 15 && i % 15 == 0) + { + split.Add(s); + s = ""; + } + } + if (s != "" && split.All(x => x != s)) + { + split.Add(s); + } + + break; + } + + int page = 1; + foreach (string s in split) + { + result.Add(new Page($"Page {page}:\n{s}")); + page++; + } + + return result; + } + + /// + /// Generates pages from a string, and puts them in message embeds. + /// + /// Input string. + /// How to split input string. + /// Base embed for output embeds. + /// + public static IEnumerable GeneratePagesInEmbed(string input, SplitType splittype = SplitType.Character, DiscordEmbedBuilder embedbase = null) + { + if (string.IsNullOrEmpty(input)) + { + throw new ArgumentException("You must provide a string that is not null or empty!"); + } + + DiscordEmbedBuilder embed = embedbase ?? new DiscordEmbedBuilder(); + + List result = []; + List split; + + switch (splittype) + { + default: + case SplitType.Character: + split = [.. SplitString(input, 500)]; + break; + case SplitType.Line: + string[] subsplit = input.Split('\n'); + + split = []; + string s = ""; + + for (int i = 0; i < subsplit.Length; i++) + { + s += $"{subsplit[i]}\n"; + if (i % 15 == 0 && i != 0) + { + split.Add(s); + s = ""; + } + } + if (s != "" && split.All(x => x != s)) + { + split.Add(s); + } + + break; + } + + int page = 1; + foreach (string s in split) + { + result.Add(new Page("", new DiscordEmbedBuilder(embed).WithDescription(s).WithFooter($"Page {page}/{split.Count}"))); + page++; + } + + return result; + } + + private static List SplitString(string str, int chunkSize) + { + List res = []; + int len = str.Length; + int i = 0; + + while (i < len) + { + int size = Math.Min(len - i, chunkSize); + res.Add(str.Substring(i, size)); + i += size; + } + + return res; + } + + private CancellationToken GetCancellationToken(TimeSpan? timeout = null) => new CancellationTokenSource(timeout ?? this.Config.Timeout).Token; + + public void Dispose() + { + this.ComponentEventWaiter?.Dispose(); + this.ModalEventWaiter?.Dispose(); + this.ReactionCollector?.Dispose(); + this.ComponentInteractionWaiter?.Dispose(); + this.MessageCreatedWaiter?.Dispose(); + this.MessageReactionAddWaiter?.Dispose(); + this.Paginator?.Dispose(); + this.Poller?.Dispose(); + this.TypingStartWaiter?.Dispose(); + this.compPaginator?.Dispose(); + + // Satisfy rule CA1816. Can be removed if this class is sealed. + GC.SuppressFinalize(this); + } +} diff --git a/DSharpPlus.Interactivity/InteractivityResult.cs b/DSharpPlus.Interactivity/InteractivityResult.cs index 6f0f35d46f..d39b10450c 100644 --- a/DSharpPlus.Interactivity/InteractivityResult.cs +++ b/DSharpPlus.Interactivity/InteractivityResult.cs @@ -1,24 +1,24 @@ -namespace DSharpPlus.Interactivity; - - -/// -/// Interactivity result -/// -/// Type of result -public readonly struct InteractivityResult -{ - /// - /// Whether interactivity was timed out - /// - public bool TimedOut { get; } - /// - /// Result - /// - public T Result { get; } - - internal InteractivityResult(bool timedout, T result) - { - this.TimedOut = timedout; - this.Result = result; - } -} +namespace DSharpPlus.Interactivity; + + +/// +/// Interactivity result +/// +/// Type of result +public readonly struct InteractivityResult +{ + /// + /// Whether interactivity was timed out + /// + public bool TimedOut { get; } + /// + /// Result + /// + public T Result { get; } + + internal InteractivityResult(bool timedout, T result) + { + this.TimedOut = timedout; + this.Result = result; + } +} diff --git a/DSharpPlus.Rest/DiscordRestClient.cs b/DSharpPlus.Rest/DiscordRestClient.cs index f037e0111f..a52734c8e6 100644 --- a/DSharpPlus.Rest/DiscordRestClient.cs +++ b/DSharpPlus.Rest/DiscordRestClient.cs @@ -1,2375 +1,2375 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.IO; -using System.Linq; -using System.Net.Http; -using System.Threading.Tasks; - -using DSharpPlus.Entities; -using DSharpPlus.Exceptions; -using DSharpPlus.Net; -using DSharpPlus.Net.Abstractions; -using DSharpPlus.Net.Models; - -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; - -namespace DSharpPlus; - -public class DiscordRestClient : BaseDiscordClient -{ - /// - /// Gets the dictionary of guilds cached by this client. - /// - public override IReadOnlyDictionary Guilds - => this.guilds; - - internal ConcurrentDictionary guilds = []; - private bool disposedValue; - - public string Token { get; } - - public TokenType TokenType { get; } - - public DiscordRestClient(RestClientOptions options, string token, TokenType tokenType, ILogger? logger = null) : base() - { - string headerTokenType = tokenType == TokenType.Bot ? "Bot" : "Bearer"; - - HttpClient httpClient = new(); - httpClient.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", $"{headerTokenType} {token}"); - - this.ApiClient = new(new - ( - httpClient, - options.Timeout, - logger ?? NullLogger.Instance, - options.MaximumRatelimitRetries, - (int)options.RatelimitRetryDelayFallback.TotalMilliseconds, - (int)options.InitialRequestTimeout.TotalMilliseconds, - options.MaximumConcurrentRestRequests - )); - - this.ApiClient.SetClient(this); - this.Token = token; - this.TokenType = tokenType; - } - - /// - /// Initializes cache - /// - /// - public async Task InitializeCacheAsync() - { - await base.InitializeAsync(); - IReadOnlyList currentUserGuilds = await this.ApiClient.GetCurrentUserGuildsAsync(); - foreach (DiscordGuild guild in currentUserGuilds) - { - this.guilds[guild.Id] = guild; - } - } - - #region Scheduled Guild Events - - /// - /// Creates a new scheduled guild event. - /// - /// The guild to create an event on. - /// The name of the event, up to 100 characters. - /// The description of the event, up to 1000 characters. - /// The channel the event will take place in, if applicable. - /// The type of event. If , a end time must be specified. - /// The image of event. - /// The privacy level of the event. - /// When the event starts. Must be in the future and before the end date, if specified. - /// When the event ends. Required for - /// Where this location takes place. - /// The created event. - public async Task CreateScheduledGuildEventAsync(ulong guildId, string name, string description, ulong? channelId, DiscordScheduledGuildEventType type, DiscordScheduledGuildEventPrivacyLevel privacyLevel, DateTimeOffset start, DateTimeOffset? end, Stream? image = null, string location = null) - => await this.ApiClient.CreateScheduledGuildEventAsync(guildId, name, description, start, type, privacyLevel, new DiscordScheduledGuildEventMetadata(location), end, channelId, image); - - /// - /// Delete a scheduled guild event. - /// - /// The ID the guild the event resides on. - /// The ID of the event to delete. - public async Task DeleteScheduledGuildEventAsync(ulong guildId, ulong eventId) - => await this.ApiClient.DeleteScheduledGuildEventAsync(guildId, eventId); - - /// - /// Gets a specific scheduled guild event. - /// - /// The ID of the guild the event resides on. - /// The ID of the event to get - /// The requested event. - public async Task GetScheduledGuildEventAsync(ulong guildId, ulong eventId) - => await this.ApiClient.GetScheduledGuildEventAsync(guildId, eventId); - - /// - /// Gets all available scheduled guild events. - /// - /// The ID of the guild to query. - /// All active and scheduled events. - public async Task> GetScheduledGuildEventsAsync(ulong guildId) - => await this.ApiClient.GetScheduledGuildEventsAsync(guildId); - - /// - /// Modify a scheduled guild event. - /// - /// The ID of the guild the event resides on. - /// The ID of the event to modify. - /// The action to apply to the event. - /// The modified event. - public async Task ModifyScheduledGuildEventAsync(ulong guildId, ulong eventId, Action mdl) - { - ScheduledGuildEventEditModel model = new(); - mdl(model); - - if (model.Type.HasValue && model.Type.Value is DiscordScheduledGuildEventType.StageInstance or DiscordScheduledGuildEventType.VoiceChannel) - { - if (!model.Channel.HasValue) - { - throw new ArgumentException("Channel must be supplied if the event is a stage instance or voice channel event."); - } - } - - if (model.Type.HasValue && model.Type.Value is DiscordScheduledGuildEventType.External) - { - if (!model.EndTime.HasValue) - { - throw new ArgumentException("End must be supplied if the event is an external event."); - } - - if (!model.Metadata.HasValue || string.IsNullOrEmpty(model.Metadata.Value.Location)) - { - throw new ArgumentException("Location must be supplied if the event is an external event."); - } - - if (model.Channel.HasValue && model.Channel.Value is not null) - { - throw new ArgumentException("Channel must not be supplied if the event is an external event."); - } - } - - // We only have an ID to work off of, so we have no validation as to the current state of the event. - return model.Status.HasValue && model.Status.Value is DiscordScheduledGuildEventStatus.Scheduled - ? throw new ArgumentException("Status cannot be set to scheduled.") - : await this.ApiClient.ModifyScheduledGuildEventAsync( - guildId, eventId, - model.Name, model.Description, - model.Channel.IfPresent(c => c?.Id), - model.StartTime, model.EndTime, - model.Type, model.PrivacyLevel, - model.Metadata, model.Status); - } - - /// - /// Gets the users interested in the guild event. - /// - /// The ID of the guild the event resides on. - /// The ID of the event. - /// How many users to query. - /// Fetch users after this ID. - /// Fetch users before this ID. - /// The users interested in the event. - public async Task> GetScheduledGuildEventUsersAsync(ulong guildId, ulong eventId, int limit = 100, ulong? after = null, ulong? before = null) - { - int remaining = limit; - ulong? last = null; - bool isAfter = after is not null; - - List users = []; - - int lastCount; - do - { - int fetchSize = remaining > 100 ? 100 : remaining; - IReadOnlyList fetch = await this.ApiClient.GetScheduledGuildEventUsersAsync(guildId, eventId, true, fetchSize, !isAfter ? last ?? before : null, isAfter ? last ?? after : null); - - lastCount = fetch.Count; - remaining -= lastCount; - - if (!isAfter) - { - users.AddRange(fetch); - last = fetch.LastOrDefault()?.Id; - } - else - { - users.InsertRange(0, fetch); - last = fetch.FirstOrDefault()?.Id; - } - } - while (remaining > 0 && lastCount > 0); - - return users.AsReadOnly(); - } - - #endregion - - #region Guild - - /// - /// Searches the given guild for members who's display name start with the specified name. - /// - /// The ID of the guild to search. - /// The name to search for. - /// The maximum amount of members to return. Max 1000. Defaults to 1. - /// The members found, if any. - public async Task> SearchMembersAsync(ulong guildId, string name, int? limit = 1) - => await this.ApiClient.SearchMembersAsync(guildId, name, limit); - - /// - /// Creates a new guild - /// - /// New guild's name - /// New guild's region ID - /// New guild's icon (base64) - /// New guild's verification level - /// New guild's default message notification level - /// New guild's system channel flags - /// - public async Task CreateGuildAsync(string name, string regionId, string iconb64, DiscordVerificationLevel? verificationLevel, DiscordDefaultMessageNotifications? defaultMessageNotifications, DiscordSystemChannelFlags? systemChannelFlags) - => await this.ApiClient.CreateGuildAsync(name, regionId, 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. - public async Task CreateGuildFromTemplateAsync(string code, string name, string icon) - => await this.ApiClient.CreateGuildFromTemplateAsync(code, name, icon); - - /// - /// Deletes a guild - /// - /// Guild ID - /// - public async Task DeleteGuildAsync(ulong id) - => await this.ApiClient.DeleteGuildAsync(id); - - /// - /// Modifies a guild - /// - /// Guild ID - /// New guild Name - /// New guild voice region - /// New guild verification level - /// New guild default message notification level - /// New guild MFA level - /// New guild explicit content filter level - /// New guild AFK channel ID - /// New guild AFK timeout in seconds - /// New guild icon (base64) - /// New guild owner ID - /// New guild splash (base64) - /// New guild system channel ID - /// New guild banner - /// New guild description - /// New guild Discovery splash - /// List of new guild features - /// New preferred locale - /// New updates channel ID - /// New rules channel ID - /// New system channel flags - /// Modify reason - /// - public async Task ModifyGuildAsync(ulong guildId, Optional name, - Optional region, Optional verificationLevel, - Optional defaultMessageNotifications, Optional mfaLevel, - Optional explicitContentFilter, Optional afkChannelId, - Optional afkTimeout, Optional iconb64, Optional ownerId, Optional splashb64, - Optional systemChannelId, Optional banner, Optional description, - Optional discorverySplash, Optional> features, Optional preferredLocale, - Optional publicUpdatesChannelId, Optional rulesChannelId, Optional systemChannelFlags, - string reason) - => await this.ApiClient.ModifyGuildAsync(guildId, name, region, verificationLevel, defaultMessageNotifications, mfaLevel, explicitContentFilter, afkChannelId, afkTimeout, iconb64, - ownerId, splashb64, systemChannelId, banner, description, discorverySplash, features, preferredLocale, publicUpdatesChannelId, rulesChannelId, systemChannelFlags, reason); - - /// - /// Modifies a guild - /// - /// Guild ID - /// Guild modifications - /// - public async Task ModifyGuildAsync(ulong guildId, Action action) - { - GuildEditModel mdl = new(); - action(mdl); - - if (mdl.AfkChannel.HasValue) - { - if (mdl.AfkChannel.Value.Type != DiscordChannelType.Voice) - { - throw new ArgumentException("AFK channel needs to be a voice channel!"); - } - } - - Optional iconb64 = Optional.FromNoValue(); - if (mdl.Icon.HasValue && mdl.Icon.Value is not null) - { - using InlineMediaTool imgtool = new(mdl.Icon.Value); - iconb64 = imgtool.GetBase64(); - } - else if (mdl.Icon.HasValue) - { - iconb64 = null; - } - - Optional splashb64 = Optional.FromNoValue(); - if (mdl.Splash.HasValue && mdl.Splash.Value is not null) - { - using InlineMediaTool imgtool = new(mdl.Splash.Value); - splashb64 = imgtool.GetBase64(); - } - else if (mdl.Splash.HasValue) - { - splashb64 = null; - } - - Optional bannerb64 = Optional.FromNoValue(); - - if (mdl.Banner.HasValue && mdl.Banner.Value is not null) - { - using InlineMediaTool imgtool = new(mdl.Banner.Value); - bannerb64 = imgtool.GetBase64(); - } - else if (mdl.Banner.HasValue) - { - bannerb64 = null; - } - - return await this.ApiClient.ModifyGuildAsync(guildId, mdl.Name, mdl.Region.IfPresent(x => x.Id), mdl.VerificationLevel, mdl.DefaultMessageNotifications, - mdl.MfaLevel, mdl.ExplicitContentFilter, mdl.AfkChannel.IfPresent(x => x?.Id), mdl.AfkTimeout, iconb64, mdl.Owner.IfPresent(x => x.Id), - splashb64, mdl.SystemChannel.IfPresent(x => x?.Id), bannerb64, mdl.Description, mdl.DiscoverySplash, mdl.Features, mdl.PreferredLocale, - mdl.PublicUpdatesChannel.IfPresent(e => e?.Id), mdl.RulesChannel.IfPresent(e => e?.Id), mdl.SystemChannelFlags, mdl.AuditLogReason); - } - - /// - /// Gets guild bans. - /// - /// The ID of the guild to get the bans from. - /// The number of users to return (up to maximum 1000, default 1000). - /// Consider only users before the given user ID. - /// Consider only users after the given user ID. - /// A collection of the guild's bans. - public async Task> GetGuildBansAsync(ulong guildId, int? limit = null, ulong? before = null, ulong? after = null) - => await this.ApiClient.GetGuildBansAsync(guildId, limit, before, after); - - /// - /// Gets the ban of the specified user. Requires Ban Members permission. - /// - /// The ID of the guild to get the ban from. - /// The ID of the user to get the ban for. - /// A guild ban object. - public async Task GetGuildBanAsync(ulong guildId, ulong userId) - => await this.ApiClient.GetGuildBanAsync(guildId, userId); - - /// - /// Creates guild ban - /// - /// Guild ID - /// User ID - /// Days to delete messages - /// Reason why this member was banned - /// - public async Task CreateGuildBanAsync(ulong guildId, ulong userId, int deleteMessageDays, string reason) - => await this.ApiClient.CreateGuildBanAsync(guildId, userId, deleteMessageDays, reason); - - /// - /// Creates multiple guild bans - /// - /// Guild ID - /// Collection of user ids to ban - /// Timespan in seconds to delete messages from the banned users - /// Auditlog reason - /// - public async Task CreateGuildBansAsync(ulong guildId, IEnumerable userIds, int deleteMessageSeconds, string reason) - => await this.ApiClient.CreateGuildBulkBanAsync(guildId, userIds, deleteMessageSeconds, reason); - - /// - /// Removes a guild ban - /// - /// Guild ID - /// User to unban - /// Reason why this member was unbanned - /// - public async Task RemoveGuildBanAsync(ulong guildId, ulong userId, string reason) - => await this.ApiClient.RemoveGuildBanAsync(guildId, userId, reason); - - /// - /// Leaves a guild - /// - /// Guild ID - /// - public async Task LeaveGuildAsync(ulong guildId) - => await this.ApiClient.LeaveGuildAsync(guildId); - - /// - /// Adds a member to a guild - /// - /// Guild ID - /// User ID - /// Access token - /// User nickname - /// Ids of roles to add to the new member. - /// Whether this user should be muted on join - /// Whether this user should be deafened on join - /// Only returns the member if they were not already in the guild - public async Task AddGuildMemberAsync(ulong guildId, ulong userId, string accessToken, string nick, IEnumerable roleIds, bool muted, bool deafened) - => await this.ApiClient.AddGuildMemberAsync(guildId, userId, accessToken, muted, deafened, nick, roleIds); - - /// - /// Gets all guild members - /// - /// Guild ID - /// Member download limit - /// Gets members after this ID - /// - public async Task> ListGuildMembersAsync(ulong guildId, int? limit, ulong? after) - { - List recmbr = []; - - int recd = limit ?? 1000; - int lim = limit ?? 1000; - ulong? last = after; - while (recd == lim) - { - IReadOnlyList tms = await this.ApiClient.ListGuildMembersAsync(guildId, lim, last == 0 ? null : last); - recd = tms.Count; - - foreach (TransportMember xtm in tms) - { - last = xtm.User.Id; - - if (this.UserCache.ContainsKey(xtm.User.Id)) - { - continue; - } - - DiscordUser usr = new(xtm.User) - { - Discord = this - }; - - UpdateUserCache(usr); - } - - recmbr.AddRange(tms.Select(xtm => new DiscordMember(xtm) { Discord = this, guild_id = guildId })); - } - - return new ReadOnlyCollection(recmbr); - } - - /// - /// Add role to guild member - /// - /// Guild ID - /// User ID - /// Role ID - /// Reason this role gets added - /// - public async Task AddGuildMemberRoleAsync(ulong guildId, ulong userId, ulong roleId, string reason) - => await this.ApiClient.AddGuildMemberRoleAsync(guildId, userId, roleId, reason); - - /// - /// Remove role from member - /// - /// Guild ID - /// User ID - /// Role ID - /// Reason this role gets removed - /// - public async Task RemoveGuildMemberRoleAsync(ulong guildId, ulong userId, ulong roleId, string reason) - => await this.ApiClient.RemoveGuildMemberRoleAsync(guildId, userId, roleId, reason); - - /// - /// Updates a role's position - /// - /// Guild ID - /// Role ID - /// Reason this position was modified - /// - public async Task UpdateRolePositionAsync(ulong guildId, ulong roleId, string reason = null) - { - List rgrrps = - [ - new() - { - RoleId = roleId - } - ]; - await this.ApiClient.ModifyGuildRolePositionsAsync(guildId, rgrrps, reason); - } - - /// - /// Updates a channel's position - /// - /// Guild ID - /// Channel ID - /// Channel position - /// Reason this position was modified - /// Whether to sync channel permissions with the parent, if moving to a new category. - /// The new parent id if the channel is to be moved to a new category. - /// - public async Task UpdateChannelPositionAsync(ulong guildId, ulong channelId, int position, string reason, bool? lockPermissions = null, ulong? parentId = null) - { - List rgcrps = - [ - new() - { - ChannelId = channelId, - Position = position, - LockPermissions = lockPermissions, - ParentId = parentId - } - ]; - await this.ApiClient.ModifyGuildChannelPositionAsync(guildId, rgcrps, reason); - } - - /// - /// Gets a guild's widget - /// - /// Guild ID - /// - public async Task GetGuildWidgetAsync(ulong guildId) - => await this.ApiClient.GetGuildWidgetAsync(guildId); - - /// - /// Gets a guild's widget settings - /// - /// Guild ID - /// - public async Task GetGuildWidgetSettingsAsync(ulong guildId) - => await this.ApiClient.GetGuildWidgetSettingsAsync(guildId); - - /// - /// Modifies a guild's widget settings - /// - /// Guild ID - /// If the widget is enabled or not - /// Widget channel ID - /// Reason the widget settings were modified - /// - public async Task ModifyGuildWidgetSettingsAsync(ulong guildId, bool? enabled = null, ulong? channelId = null, string reason = null) - => await this.ApiClient.ModifyGuildWidgetSettingsAsync(guildId, enabled, channelId, reason); - - /// - /// Gets a guild's membership screening form. - /// - /// Guild ID - /// The guild's membership screening form. - public async Task GetGuildMembershipScreeningFormAsync(ulong guildId) - => await this.ApiClient.GetGuildMembershipScreeningFormAsync(guildId); - - /// - /// Modifies a guild's membership screening form. - /// - /// Guild ID - /// Action to perform - /// The modified screening form. - public async Task ModifyGuildMembershipScreeningFormAsync(ulong guildId, Action action) - { - MembershipScreeningEditModel mdl = new(); - action(mdl); - return await this.ApiClient.ModifyGuildMembershipScreeningFormAsync(guildId, mdl.Enabled, mdl.Fields, mdl.Description); - } - - /// - /// Gets a guild's vanity url - /// - /// The ID of the guild. - /// The guild's vanity url. - public async Task GetGuildVanityUrlAsync(ulong guildId) - => await this.ApiClient.GetGuildVanityUrlAsync(guildId); - - /// - /// Updates the current user's suppress state in a stage channel. - /// - /// The ID of the guild. - /// The ID of the channel. - /// Toggles the suppress state. - /// Sets the time the user requested to speak. - public async Task UpdateCurrentUserVoiceStateAsync(ulong guildId, ulong channelId, bool? suppress, DateTimeOffset? requestToSpeakTimestamp = null) - => await this.ApiClient.UpdateCurrentUserVoiceStateAsync(guildId, channelId, suppress, requestToSpeakTimestamp); - - /// - /// Updates a member's suppress state in a stage channel. - /// - /// The ID of the guild. - /// The ID of the member. - /// The ID of the stage channel. - /// Toggles the member's suppress state. - /// - public async Task UpdateUserVoiceStateAsync(ulong guildId, ulong userId, ulong channelId, bool? suppress) - => await this.ApiClient.UpdateUserVoiceStateAsync(guildId, userId, channelId, suppress); - #endregion - - #region Channel - /// - /// Creates a guild channel - /// - /// Channel ID - /// Channel name - /// Channel type - /// Channel parent ID - /// Channel topic - /// Voice channel bitrate - /// Voice channel user limit - /// Channel overwrites - /// Whether this channel should be marked as NSFW - /// Slow mode timeout for users. - /// Voice channel video quality mode. - /// Sorting position of the channel. - /// Reason this channel was created - /// Default duration for newly created forum posts in the channel. - /// Default emoji used for reacting to forum posts. - /// Tags available for use by forum posts in the channel. - /// Default sorting order for forum posts in the channel. - /// - public async Task CreateGuildChannelAsync - ( - ulong id, - string name, - DiscordChannelType type, - ulong? parent, - Optional topic, - int? bitrate, - int? userLimit, - IEnumerable overwrites, - bool? nsfw, - Optional perUserRateLimit, - DiscordVideoQualityMode? qualityMode, - int? position, - string reason, - DiscordAutoArchiveDuration? defaultAutoArchiveDuration = null, - DefaultReaction? defaultReactionEmoji = null, - IEnumerable availableTags = null, - DiscordDefaultSortOrder? defaultSortOrder = null - ) => type is not (DiscordChannelType.Text or DiscordChannelType.Voice or DiscordChannelType.Category or DiscordChannelType.News or DiscordChannelType.Stage or DiscordChannelType.GuildForum) - ? throw new ArgumentException("Channel type must be text, voice, stage, category, or a forum.", nameof(type)) - : await this.ApiClient.CreateGuildChannelAsync - ( - id, - name, - type, - parent, - topic, - bitrate, - userLimit, - overwrites, - nsfw, - perUserRateLimit, - qualityMode, - position, - reason, - defaultAutoArchiveDuration, - defaultReactionEmoji, - availableTags, - defaultSortOrder - ); - - /// - /// Modifies a channel - /// - /// Channel ID - /// New channel name - /// New channel position - /// New channel topic - /// Whether this channel should be marked as NSFW - /// New channel parent - /// New voice channel bitrate - /// New voice channel user limit - /// Slow mode timeout for users. - /// New region override. - /// New video quality mode. - /// New channel type. - /// New channel permission overwrites. - /// Reason why this channel was modified - /// Channel flags. - /// Default duration for newly created forum posts in the channel. - /// Default emoji used for reacting to forum posts. - /// Tags available for use by forum posts in the channel. - /// Default per-user ratelimit for forum posts in the channel. - /// Default sorting order for forum posts in the channel. - /// Default layout for forum posts in the channel. - /// - public async Task ModifyChannelAsync - ( - ulong id, - string name, - int? position, - Optional topic, - bool? nsfw, - Optional parent, - int? bitrate, - int? userLimit, - Optional perUserRateLimit, - Optional rtcRegion, - DiscordVideoQualityMode? qualityMode, - Optional type, - IEnumerable permissionOverwrites, - string reason, - Optional flags, - IEnumerable? availableTags, - Optional defaultAutoArchiveDuration, - Optional defaultReactionEmoji, - Optional defaultPerUserRatelimit, - Optional defaultSortOrder, - Optional defaultForumLayout - ) - => await this.ApiClient.ModifyChannelAsync - ( - id, - name, - position, - topic, - nsfw, - parent, - bitrate, - userLimit, - perUserRateLimit, - rtcRegion.IfPresent(e => e?.Id), - qualityMode, - type, - permissionOverwrites, - flags, - availableTags, - defaultAutoArchiveDuration, - defaultReactionEmoji, - defaultPerUserRatelimit, - defaultSortOrder, - defaultForumLayout, - reason - ); - - /// - /// Modifies a channel - /// - /// Channel ID - /// Channel modifications - /// - public async Task ModifyChannelAsync(ulong channelId, Action action) - { - ChannelEditModel mdl = new(); - action(mdl); - - await this.ApiClient.ModifyChannelAsync - ( - channelId, mdl.Name, - mdl.Position, - mdl.Topic, - mdl.Nsfw, - mdl.Parent.HasValue ? mdl.Parent.Value?.Id : default(Optional), - mdl.Bitrate, - mdl.Userlimit, - mdl.PerUserRateLimit, - mdl.RtcRegion.IfPresent(r => r?.Id), - mdl.QualityMode, - mdl.Type, - mdl.PermissionOverwrites, - mdl.Flags, - mdl.AvailableTags, - mdl.DefaultAutoArchiveDuration, - mdl.DefaultReaction, - mdl.DefaultThreadRateLimit, - mdl.DefaultSortOrder, - mdl.DefaultForumLayout, - mdl.AuditLogReason - ); - } - - /// - /// Gets a channel object - /// - /// Channel ID - /// - public async Task GetChannelAsync(ulong id) - => await this.ApiClient.GetChannelAsync(id); - - /// - /// Deletes a channel - /// - /// Channel ID - /// Reason why this channel was deleted - /// - public async Task DeleteChannelAsync(ulong id, string reason) - => await this.ApiClient.DeleteChannelAsync(id, reason); - - /// - /// Gets message in a channel - /// - /// Channel ID - /// Message ID - /// - public async Task GetMessageAsync(ulong channelId, ulong messageId) - => await this.ApiClient.GetMessageAsync(channelId, messageId); - - /// - /// Sends a message - /// - /// Channel ID - /// Message (text) content - /// - public async Task CreateMessageAsync(ulong channelId, string content) - => await this.ApiClient.CreateMessageAsync(channelId, content, null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false, suppressNotifications: false); - - /// - /// Sends a message - /// - /// Channel ID - /// Embed to attach - /// - public async Task CreateMessageAsync(ulong channelId, DiscordEmbed embed) - => await this.ApiClient.CreateMessageAsync(channelId, null, embed is not null ? new[] { embed } : null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false, suppressNotifications: false); - - /// - /// Sends a message - /// - /// Channel ID - /// Message (text) content - /// Embed to attach - /// - public async Task CreateMessageAsync(ulong channelId, string content, DiscordEmbed embed) - => await this.ApiClient.CreateMessageAsync(channelId, content, embed is not null ? new[] { embed } : null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false, suppressNotifications: false); - - /// - /// Sends a message - /// - /// Channel ID - /// The Discord Message builder. - /// - public async Task CreateMessageAsync(ulong channelId, DiscordMessageBuilder builder) - => await this.ApiClient.CreateMessageAsync(channelId, builder); - - /// - /// Sends a message - /// - /// Channel ID - /// The Discord Message builder. - /// - public async Task CreateMessageAsync(ulong channelId, Action action) - { - DiscordMessageBuilder builder = new(); - action(builder); - return await this.ApiClient.CreateMessageAsync(channelId, builder); - } - - /// - /// Gets channels from a guild - /// - /// Guild ID - /// - public async Task> GetGuildChannelsAsync(ulong guildId) - => await this.ApiClient.GetGuildChannelsAsync(guildId); - - /// - /// Gets messages from a channel - /// - /// Channel ID - /// Limit of messages to get - /// Gets messages before this ID - /// Gets messages after this ID - /// Gets messages around this ID - /// - public async Task> GetChannelMessagesAsync(ulong channelId, int limit, ulong? before, ulong? after, ulong? around) - => await this.ApiClient.GetChannelMessagesAsync(channelId, limit, before, after, around); - - /// - /// Gets a message from a channel - /// - /// Channel ID - /// Message ID - /// - public async Task GetChannelMessageAsync(ulong channelId, ulong messageId) - => await this.ApiClient.GetChannelMessageAsync(channelId, messageId); - - /// - /// Edits a message - /// - /// Channel ID - /// Message ID - /// New message content - /// - public async Task EditMessageAsync(ulong channelId, ulong messageId, Optional content) - => await this.ApiClient.EditMessageAsync(channelId, messageId, content, default, default, default, Array.Empty()); - - /// - /// Edits a message - /// - /// Channel ID - /// Message ID - /// New message embed - /// - public async Task EditMessageAsync(ulong channelId, ulong messageId, Optional embed) - => await this.ApiClient.EditMessageAsync(channelId, messageId, default, embed.HasValue ? [embed.Value] : Array.Empty(), default, default, Array.Empty()); - - /// - /// Edits a message - /// - /// Channel ID - /// Message ID - /// The builder of the message to edit. - /// Whether to suppress embeds on the message. - /// Attached files to keep. - /// - public async Task EditMessageAsync(ulong channelId, ulong messageId, DiscordMessageBuilder builder, bool suppressEmbeds = false, IEnumerable attachments = default) - { - builder.Validate(); - - return await this.ApiClient.EditMessageAsync(channelId, messageId, builder.Content, new Optional>(builder.Embeds), builder.mentions, builder.Components, builder.Files, suppressEmbeds ? DiscordMessageFlags.SuppressedEmbeds : null, attachments); - } - - /// - /// Modifies the visibility of embeds in a message. - /// - /// Channel ID - /// Message ID - /// Whether to hide all embeds. - public async Task ModifyEmbedSuppressionAsync(ulong channelId, ulong messageId, bool hideEmbeds) - => await this.ApiClient.EditMessageAsync(channelId, messageId, default, default, default, default, Array.Empty(), hideEmbeds ? DiscordMessageFlags.SuppressedEmbeds : null); - - /// - /// Deletes a message - /// - /// Channel ID - /// Message ID - /// Why this message was deleted - /// - public async Task DeleteMessageAsync(ulong channelId, ulong messageId, string reason) - => await this.ApiClient.DeleteMessageAsync(channelId, messageId, reason); - - /// - /// Deletes multiple messages - /// - /// Channel ID - /// Message IDs - /// Reason these messages were deleted - /// - public async Task DeleteMessagesAsync(ulong channelId, IEnumerable messageIds, string reason) - => await this.ApiClient.DeleteMessagesAsync(channelId, messageIds, reason); - - /// - /// Gets a channel's invites - /// - /// Channel ID - /// - public async Task> GetChannelInvitesAsync(ulong channelId) - => await this.ApiClient.GetChannelInvitesAsync(channelId); - - /// - /// Creates a channel invite - /// - /// Channel ID - /// For how long the invite should exist - /// How often the invite may be used - /// Whether this invite should be temporary - /// Whether this invite should be unique (false might return an existing invite) - /// Why you made an invite - /// The target type of the invite, for stream and embedded application invites. - /// The ID of the target user. - /// The ID of the target application. - /// - public async Task CreateChannelInviteAsync(ulong channelId, int maxAge, int maxUses, bool temporary, bool unique, string reason, DiscordInviteTargetType? targetType = null, ulong? targetUserId = null, ulong? targetApplicationId = null) - => await this.ApiClient.CreateChannelInviteAsync(channelId, maxAge, maxUses, temporary, unique, reason, targetType, targetUserId, targetApplicationId); - - /// - /// Deletes channel overwrite - /// - /// Channel ID - /// Overwrite ID - /// Reason it was deleted - /// - public async Task DeleteChannelPermissionAsync(ulong channelId, ulong overwriteId, string reason) - => await this.ApiClient.DeleteChannelPermissionAsync(channelId, overwriteId, reason); - - /// - /// Edits channel overwrite - /// - /// Channel ID - /// Overwrite ID - /// Permissions to allow - /// Permissions to deny - /// Overwrite type - /// Reason this overwrite was created - /// - public async Task EditChannelPermissionsAsync(ulong channelId, ulong overwriteId, DiscordPermissions allow, DiscordPermissions deny, string type, string reason) - => await this.ApiClient.EditChannelPermissionsAsync(channelId, overwriteId, allow, deny, type, reason); - - /// - /// Send a typing indicator to a channel - /// - /// Channel ID - /// - public async Task TriggerTypingAsync(ulong channelId) - => await this.ApiClient.TriggerTypingAsync(channelId); - - /// - /// Gets pinned messages - /// - /// Channel ID - /// - public async Task> GetPinnedMessagesAsync(ulong channelId) - => await this.ApiClient.GetPinnedMessagesAsync(channelId); - - /// - /// Unpins a message - /// - /// Channel ID - /// Message ID - /// - public async Task UnpinMessageAsync(ulong channelId, ulong messageId) - => await this.ApiClient.UnpinMessageAsync(channelId, messageId); - - /// - /// Joins a group DM - /// - /// Channel ID - /// DM nickname - /// - public async Task JoinGroupDmAsync(ulong channelId, string nickname) - => await this.ApiClient.AddGroupDmRecipientAsync(channelId, this.CurrentUser.Id, this.Token, nickname); - - /// - /// Adds a member to a group DM - /// - /// Channel ID - /// User ID - /// User's access token - /// Nickname for user - /// - public async Task GroupDmAddRecipientAsync(ulong channelId, ulong userId, string accessToken, string nickname) - => await this.ApiClient.AddGroupDmRecipientAsync(channelId, userId, accessToken, nickname); - - /// - /// Leaves a group DM - /// - /// Channel ID - /// - public async Task LeaveGroupDmAsync(ulong channelId) - => await this.ApiClient.RemoveGroupDmRecipientAsync(channelId, this.CurrentUser.Id); - - /// - /// Removes a member from a group DM - /// - /// Channel ID - /// User ID - /// - public async Task GroupDmRemoveRecipientAsync(ulong channelId, ulong userId) - => await this.ApiClient.RemoveGroupDmRecipientAsync(channelId, userId); - - /// - /// Creates a group DM - /// - /// Access tokens - /// Nicknames per user - /// - public async Task CreateGroupDmAsync(IEnumerable accessTokens, IDictionary nicks) - => await this.ApiClient.CreateGroupDmAsync(accessTokens, nicks); - - /// - /// Creates a group DM with current user - /// - /// Access tokens - /// Nicknames - /// - public async Task CreateGroupDmWithCurrentUserAsync(IEnumerable accessTokens, IDictionary nicks) - { - List a = accessTokens.ToList(); - a.Add(this.Token); - return await this.ApiClient.CreateGroupDmAsync(a, nicks); - } - - /// - /// Creates a DM - /// - /// Recipient user ID - /// - public async Task CreateDmAsync(ulong recipientId) - => await this.ApiClient.CreateDmAsync(recipientId); - - /// - /// Follows a news channel - /// - /// ID of the channel to follow - /// ID of the channel to crosspost messages to - /// Thrown when the current user doesn't have on the target channel - public async Task FollowChannelAsync(ulong channelId, ulong webhookChannelId) - => await this.ApiClient.FollowChannelAsync(channelId, webhookChannelId); - - /// - /// Publishes a message in a news channel to following channels - /// - /// ID of the news channel the message to crosspost belongs to - /// ID of the message to crosspost - /// - /// Thrown when the current user doesn't have and/or - /// - public async Task CrosspostMessageAsync(ulong channelId, ulong messageId) - => await this.ApiClient.CrosspostMessageAsync(channelId, messageId); - - /// - /// Creates a stage instance in a stage channel. - /// - /// The ID of the stage channel to create it in. - /// The topic of the stage instance. - /// The privacy level of the stage instance. - /// The reason the stage instance was created. - /// The created stage instance. - public async Task CreateStageInstanceAsync(ulong channelId, string topic, DiscordStagePrivacyLevel? privacyLevel = null, string reason = null) - => await this.ApiClient.CreateStageInstanceAsync(channelId, topic, privacyLevel, reason); - - /// - /// Gets a stage instance in a stage channel. - /// - /// The ID of the channel. - /// The stage instance in the channel. - public async Task GetStageInstanceAsync(ulong channelId) - => await this.ApiClient.GetStageInstanceAsync(channelId); - - /// - /// Modifies a stage instance in a stage channel. - /// - /// The ID of the channel to modify the stage instance of. - /// Action to perform. - /// The modified stage instance. - public async Task ModifyStageInstanceAsync(ulong channelId, Action action) - { - StageInstanceEditModel mdl = new(); - action(mdl); - return await this.ApiClient.ModifyStageInstanceAsync(channelId, mdl.Topic, mdl.PrivacyLevel, mdl.AuditLogReason); - } - - /// - /// Deletes a stage instance in a stage channel. - /// - /// The ID of the channel to delete the stage instance of. - /// The reason the stage instance was deleted. - public async Task DeleteStageInstanceAsync(ulong channelId, string reason = null) - => await this.ApiClient.DeleteStageInstanceAsync(channelId, reason); - - /// - /// Pins a message. - /// - /// The ID of the channel the message is in. - /// The ID of the message. - public async Task PinMessageAsync(ulong channelId, ulong messageId) - => await this.ApiClient.PinMessageAsync(channelId, messageId); - - #endregion - - #region Member - /// - /// Gets current user object - /// - /// - public async Task GetCurrentUserAsync() - => await this.ApiClient.GetCurrentUserAsync(); - - /// - /// Gets user object - /// - /// User ID - /// - public async Task GetUserAsync(ulong user) - => await this.ApiClient.GetUserAsync(user); - - /// - /// Gets guild member - /// - /// Guild ID - /// Member ID - /// - public async Task GetGuildMemberAsync(ulong guildId, ulong memberId) - => await this.ApiClient.GetGuildMemberAsync(guildId, memberId); - - /// - /// Removes guild member - /// - /// Guild ID - /// User ID - /// Why this user was removed - /// - public async Task RemoveGuildMemberAsync(ulong guildId, ulong userId, string reason) - => await this.ApiClient.RemoveGuildMemberAsync(guildId, userId, reason); - - /// - /// Modifies current user - /// - /// New username - /// New avatar (base64) - /// New banner (base64) - /// - public async Task ModifyCurrentUserAsync(string username, string base64Avatar, string base64Banner) - => new DiscordUser(await this.ApiClient.ModifyCurrentUserAsync(username, base64Avatar, base64Banner)) { Discord = this }; - - /// - /// Modifies current user - /// - /// username - /// avatar - /// New banner - /// - public async Task ModifyCurrentUserAsync(string username = null, Stream? avatar = null, Stream? banner = null) - { - string avatarBase64 = null; - if (avatar is not null) - { - using InlineMediaTool imgtool = new(avatar); - avatarBase64 = imgtool.GetBase64(); - } - - string bannerBase64 = null; - if (banner is not null) - { - using InlineMediaTool imgtool = new(banner); - bannerBase64 = imgtool.GetBase64(); - } - - return new DiscordUser(await this.ApiClient.ModifyCurrentUserAsync(username, avatarBase64, bannerBase64)) { Discord = this }; - } - - /// - /// Gets current user's guilds - /// - /// Limit of guilds to get - /// Gets guild before ID - /// Gets guilds after ID - /// - public async Task> GetCurrentUserGuildsAsync(int limit = 100, ulong? before = null, ulong? after = null) - => await this.ApiClient.GetCurrentUserGuildsAsync(limit, before, after); - - /// - /// Gets the guild member for the current user in the specified guild. Only works with bearer tokens with the guilds.members.read scope. - /// - /// Guild ID - /// - public async Task GetCurrentUserGuildMemberAsync(ulong guildId) - => await this.ApiClient.GetCurrentUserGuildMemberAsync(guildId); - - /// - /// Modifies guild member. - /// - /// Guild ID - /// User ID - /// New nickname - /// New roles - /// Whether this user should be muted - /// Whether this user should be deafened - /// Voice channel to move this user to - /// How long this member should be timed out for. Requires MODERATE_MEMBERS permission. - /// Flags for this guild member. - /// Reason this user was modified - /// - public async Task ModifyGuildMemberAsync(ulong guildId, ulong userId, Optional nick, - Optional> roleIds, Optional mute, Optional deaf, - Optional voiceChannelId, Optional communicationDisabledUntil, Optional memberFlags, string reason) - => await this.ApiClient.ModifyGuildMemberAsync(guildId, userId, nick, roleIds, mute, deaf, voiceChannelId, communicationDisabledUntil, memberFlags, reason); - - /// - /// Modifies a member - /// - /// Member ID - /// Guild ID - /// Modifications - /// - public async Task ModifyAsync(ulong memberId, ulong guildId, Action action) - { - MemberEditModel mdl = new(); - action(mdl); - - if (mdl.VoiceChannel.HasValue && mdl.VoiceChannel.Value is not null && mdl.VoiceChannel.Value.Type != DiscordChannelType.Voice && mdl.VoiceChannel.Value.Type != DiscordChannelType.Stage) - { - throw new ArgumentException($"{nameof(MemberEditModel)}.{mdl.VoiceChannel} must be a voice or stage channel.", nameof(action)); - } - - if (mdl.Nickname.HasValue && this.CurrentUser.Id == memberId) - { - await this.ApiClient.ModifyCurrentMemberAsync(guildId, mdl.Nickname.Value, - mdl.AuditLogReason); - await this.ApiClient.ModifyGuildMemberAsync(guildId, memberId, Optional.FromNoValue(), - mdl.Roles.IfPresent(e => e.Select(xr => xr.Id)), mdl.Muted, mdl.Deafened, - mdl.VoiceChannel.IfPresent(e => e?.Id), mdl.CommunicationDisabledUntil, mdl.MemberFlags, mdl.AuditLogReason); - } - else - { - await this.ApiClient.ModifyGuildMemberAsync(guildId, memberId, mdl.Nickname, - mdl.Roles.IfPresent(e => e.Select(xr => xr.Id)), mdl.Muted, mdl.Deafened, - mdl.VoiceChannel.IfPresent(e => e?.Id), mdl.CommunicationDisabledUntil, mdl.MemberFlags, mdl.AuditLogReason); - } - } - - /// - /// Changes the current user in a guild. - /// - /// Guild ID - /// Nickname to set - /// Audit log reason - /// - public async Task ModifyCurrentMemberAsync(ulong guildId, string nickname, string reason) - => await this.ApiClient.ModifyCurrentMemberAsync(guildId, nickname, reason); - - #endregion - - #region Roles - /// - /// Gets roles - /// - /// Guild ID - /// - public async Task> GetGuildRolesAsync(ulong guildId) - => await this.ApiClient.GetGuildRolesAsync(guildId); - - /// - /// Gets a guild. - /// - /// The guild ID to search for. - /// Whether to include approximate presence and member counts in the returned guild. - /// - public async Task GetGuildAsync(ulong guildId, bool? withCounts = null) - => await this.ApiClient.GetGuildAsync(guildId, withCounts); - - /// - /// Modifies a role - /// - /// Guild ID - /// Role ID - /// New role name - /// New role permissions - /// New role color - /// Whether this role should be hoisted - /// Whether this role should be mentionable - /// Why this role was modified - /// The icon to add to this role - /// The emoji to add to this role. Must be unicode. - /// - public async Task ModifyGuildRoleAsync(ulong guildId, ulong roleId, string name, DiscordPermissions? permissions, DiscordColor? color, bool? hoist, bool? mentionable, string reason, Stream icon, DiscordEmoji emoji) - => await this.ApiClient.ModifyGuildRoleAsync(guildId, roleId, name, permissions, color.HasValue ? color.Value.Value : null, hoist, mentionable, icon, emoji?.ToString(), reason); - - /// - /// Modifies a role - /// - /// Role ID - /// Guild ID - /// Modifications - /// - public async Task ModifyGuildRoleAsync(ulong roleId, ulong guildId, Action action) - { - RoleEditModel mdl = new(); - action(mdl); - - await ModifyGuildRoleAsync(guildId, roleId, mdl.Name, mdl.Permissions, mdl.Color, mdl.Hoist, mdl.Mentionable, mdl.AuditLogReason, mdl.Icon, mdl.Emoji); - } - - /// - /// Deletes a role - /// - /// Guild ID - /// Role ID - /// Reason why this role was deleted - /// - public async Task DeleteGuildRoleAsync(ulong guildId, ulong roleId, string reason) - => await this.ApiClient.DeleteRoleAsync(guildId, roleId, reason); - - /// - /// Creates a new role - /// - /// Guild ID - /// Role name - /// Role permissions - /// Role color - /// Whether this role should be hoisted - /// Whether this role should be mentionable - /// Reason why this role was created - /// The icon to add to this role - /// The emoji to add to this role. Must be unicode. - /// - public async Task CreateGuildRoleAsync(ulong guildId, string name, DiscordPermissions? permissions, int? color, bool? hoist, bool? mentionable, string reason, Stream icon = null, DiscordEmoji emoji = null) - => await this.ApiClient.CreateGuildRoleAsync(guildId, name, permissions, color, hoist, mentionable, icon, emoji?.ToString(), reason); - #endregion - - #region Prune - /// - /// Get a guild's prune count. - /// - /// Guild ID - /// Days to check for - /// The roles to be included in the prune. - /// - public async Task GetGuildPruneCountAsync(ulong guildId, int days, IEnumerable includeRoles) - => await this.ApiClient.GetGuildPruneCountAsync(guildId, days, includeRoles); - - /// - /// Begins a guild prune. - /// - /// Guild ID - /// Days to prune for - /// Whether to return the prune count after this method completes. This is discouraged for larger guilds. - /// The roles to be included in the prune. - /// Reason why this guild was pruned - /// - public async Task BeginGuildPruneAsync(ulong guildId, int days, bool computePruneCount, IEnumerable includeRoles, string reason) - => await this.ApiClient.BeginGuildPruneAsync(guildId, days, computePruneCount, includeRoles, reason); - #endregion - - #region GuildVarious - /// - /// Gets guild integrations - /// - /// Guild ID - /// - public async Task> GetGuildIntegrationsAsync(ulong guildId) - => await this.ApiClient.GetGuildIntegrationsAsync(guildId); - - /// - /// Creates guild integration - /// - /// Guild ID - /// Integration type - /// Integration id - /// - public async Task CreateGuildIntegrationAsync(ulong guildId, string type, ulong id) - => await this.ApiClient.CreateGuildIntegrationAsync(guildId, type, id); - - /// - /// Modifies a guild integration - /// - /// Guild ID - /// Integration ID - /// Expiration behaviour - /// Expiration grace period - /// Whether to enable emojis for this integration - /// - public async Task ModifyGuildIntegrationAsync(ulong guildId, ulong integrationId, int expireBehaviour, int expireGracePeriod, bool enableEmoticons) - => await this.ApiClient.ModifyGuildIntegrationAsync(guildId, integrationId, expireBehaviour, expireGracePeriod, enableEmoticons); - - /// - /// Removes a guild integration - /// - /// Guild ID - /// Integration to remove - /// Reason why this integration was removed - /// - public async Task DeleteGuildIntegrationAsync(ulong guildId, DiscordIntegration integration, string reason = null) - => await this.ApiClient.DeleteGuildIntegrationAsync(guildId, integration.Id, reason); - - /// - /// Syncs guild integration - /// - /// Guild ID - /// Integration ID - /// - public async Task SyncGuildIntegrationAsync(ulong guildId, ulong integrationId) - => await this.ApiClient.SyncGuildIntegrationAsync(guildId, integrationId); - - /// - /// Get a guild's voice region - /// - /// Guild ID - /// - public async Task> GetGuildVoiceRegionsAsync(ulong guildId) - => await this.ApiClient.GetGuildVoiceRegionsAsync(guildId); - - /// - /// Get a guild's invites - /// - /// Guild ID - /// - public async Task> GetGuildInvitesAsync(ulong guildId) - => await this.ApiClient.GetGuildInvitesAsync(guildId); - - /// - /// Gets a guild's templates. - /// - /// Guild ID - /// All of the guild's templates. - public async Task> GetGuildTemplatesAsync(ulong guildId) - => await this.ApiClient.GetGuildTemplatesAsync(guildId); - - /// - /// Creates a guild template. - /// - /// Guild ID - /// Name of the template. - /// Description of the template. - /// The template created. - public async Task CreateGuildTemplateAsync(ulong guildId, string name, string description = null) - => await this.ApiClient.CreateGuildTemplateAsync(guildId, name, description); - - /// - /// Syncs the template to the current guild's state. - /// - /// Guild ID - /// The code of the template to sync. - /// The template synced. - public async Task SyncGuildTemplateAsync(ulong guildId, string code) - => await this.ApiClient.SyncGuildTemplateAsync(guildId, code); - - /// - /// Modifies the template's metadata. - /// - /// Guild ID - /// The template's code. - /// Name of the template. - /// Description of the template. - /// The template modified. - public async Task ModifyGuildTemplateAsync(ulong guildId, string code, string name = null, string description = null) - => await this.ApiClient.ModifyGuildTemplateAsync(guildId, code, name, description); - - /// - /// Deletes the template. - /// - /// Guild ID - /// The code of the template to delete. - /// The deleted template. - public async Task DeleteGuildTemplateAsync(ulong guildId, string code) - => await this.ApiClient.DeleteGuildTemplateAsync(guildId, code); - - /// - /// Gets a guild's welcome screen. - /// - /// The guild's welcome screen object. - public async Task GetGuildWelcomeScreenAsync(ulong guildId) => - await this.ApiClient.GetGuildWelcomeScreenAsync(guildId); - - /// - /// Modifies a guild's welcome screen. - /// - /// The guild ID to modify. - /// Action to perform. - /// The audit log reason for this action. - /// The modified welcome screen. - public async Task ModifyGuildWelcomeScreenAsync(ulong guildId, Action action, string reason = null) - { - WelcomeScreenEditModel mdl = new(); - action(mdl); - return await this.ApiClient.ModifyGuildWelcomeScreenAsync(guildId, mdl.Enabled, mdl.WelcomeChannels, mdl.Description, reason); - } - - /// - /// Gets a guild preview. - /// - /// The ID of the guild. - public async Task GetGuildPreviewAsync(ulong guildId) - => await this.ApiClient.GetGuildPreviewAsync(guildId); - - #endregion - - #region Invites - /// - /// 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. - /// - public async Task GetInviteAsync(string inviteCode, bool? withCounts = null, bool? withExpiration = null) - => await this.ApiClient.GetInviteAsync(inviteCode, withCounts, withExpiration); - - /// - /// Removes an invite - /// - /// Invite code - /// Reason why this invite was removed - /// - public async Task DeleteInviteAsync(string inviteCode, string reason) - => await this.ApiClient.DeleteInviteAsync(inviteCode, reason); - #endregion - - #region Connections - /// - /// Gets current user's connections - /// - /// - public async Task> GetUsersConnectionsAsync() - => await this.ApiClient.GetUsersConnectionsAsync(); - #endregion - - #region Webhooks - /// - /// Creates a new webhook - /// - /// Channel ID - /// Webhook name - /// Webhook avatar (base64) - /// Reason why this webhook was created - /// - public async Task CreateWebhookAsync(ulong channelId, string name, string base64Avatar, string reason) - => await this.ApiClient.CreateWebhookAsync(channelId, name, base64Avatar, reason); - - /// - /// Creates a new webhook - /// - /// Channel ID - /// Webhook name - /// Webhook avatar - /// Reason why this webhook was created - /// - public async Task CreateWebhookAsync(ulong channelId, string name, Stream avatar = null, string reason = null) - { - string av64 = null; - if (avatar is not null) - { - using InlineMediaTool imgtool = new(avatar); - av64 = imgtool.GetBase64(); - } - - return await this.ApiClient.CreateWebhookAsync(channelId, name, av64, reason); - } - - /// - /// Gets all webhooks from a channel - /// - /// Channel ID - /// - public async Task> GetChannelWebhooksAsync(ulong channelId) - => await this.ApiClient.GetChannelWebhooksAsync(channelId); - - /// - /// Gets all webhooks from a guild - /// - /// Guild ID - /// - public async Task> GetGuildWebhooksAsync(ulong guildId) - => await this.ApiClient.GetGuildWebhooksAsync(guildId); - - /// - /// Gets a webhook - /// - /// Webhook ID - /// - public async Task GetWebhookAsync(ulong webhookId) - => await this.ApiClient.GetWebhookAsync(webhookId); - - /// - /// Gets a webhook with its token (when user is not in said guild) - /// - /// Webhook ID - /// Webhook token - /// - public async Task GetWebhookWithTokenAsync(ulong webhookId, string webhookToken) - => await this.ApiClient.GetWebhookWithTokenAsync(webhookId, webhookToken); - - /// - /// Modifies a webhook - /// - /// Webhook ID - /// The new channel ID the webhook should be moved to. - /// New webhook name - /// New webhook avatar (base64) - /// Reason why this webhook was modified - /// - public async Task ModifyWebhookAsync(ulong webhookId, ulong channelId, string name, string base64Avatar, string reason) - => await this.ApiClient.ModifyWebhookAsync(webhookId, channelId, name, base64Avatar, reason); - - /// - /// Modifies a webhook - /// - /// Webhook ID - /// The new channel ID the webhook should be moved to. - /// New webhook name - /// New webhook avatar - /// Reason why this webhook was modified - /// - public async Task ModifyWebhookAsync(ulong webhookId, ulong channelId, string name, Stream avatar, string reason) - { - string av64 = null; - if (avatar is not null) - { - using InlineMediaTool imgtool = new(avatar); - av64 = imgtool.GetBase64(); - } - - return await this.ApiClient.ModifyWebhookAsync(webhookId, channelId, name, av64, reason); - } - - /// - /// Modifies a webhook (when user is not in said guild) - /// - /// Webhook ID - /// New webhook name - /// New webhook avatar (base64) - /// Webhook token - /// Reason why this webhook was modified - /// - public async Task ModifyWebhookAsync(ulong webhookId, string name, string base64Avatar, string webhookToken, string reason) - => await this.ApiClient.ModifyWebhookAsync(webhookId, name, base64Avatar, webhookToken, reason); - - /// - /// Modifies a webhook (when user is not in said guild) - /// - /// Webhook ID - /// New webhook name - /// New webhook avatar - /// Webhook token - /// Reason why this webhook was modified - /// - public async Task ModifyWebhookAsync(ulong webhookId, string name, Stream avatar, string webhookToken, string reason) - { - string av64 = null; - if (avatar is not null) - { - using InlineMediaTool imgtool = new(avatar); - av64 = imgtool.GetBase64(); - } - - return await this.ApiClient.ModifyWebhookAsync(webhookId, name, av64, webhookToken, reason); - } - - /// - /// Deletes a webhook - /// - /// Webhook ID - /// Reason this webhook was deleted - /// - public async Task DeleteWebhookAsync(ulong webhookId, string reason) - => await this.ApiClient.DeleteWebhookAsync(webhookId, reason); - - /// - /// Deletes a webhook (when user is not in said guild) - /// - /// Webhook ID - /// Reason this webhook was removed - /// Webhook token - /// - public async Task DeleteWebhookAsync(ulong webhookId, string reason, string webhookToken) - => await this.ApiClient.DeleteWebhookAsync(webhookId, webhookToken, reason); - - /// - /// Sends a message to a webhook - /// - /// Webhook ID - /// Webhook token - /// Webhook builder filled with data to send. - /// - public async Task ExecuteWebhookAsync(ulong webhookId, string webhookToken, DiscordWebhookBuilder builder) - => await this.ApiClient.ExecuteWebhookAsync(webhookId, webhookToken, builder); - - /// - /// Edits a previously-sent webhook message. - /// - /// Webhook ID - /// Webhook token - /// The ID of the message to edit. - /// The builder of the message to edit. - /// Attached files to keep. - /// The modified - public async Task EditWebhookMessageAsync(ulong webhookId, string webhookToken, ulong messageId, DiscordWebhookBuilder builder, IEnumerable attachments = default) - { - builder.Validate(true); - - return await this.ApiClient.EditWebhookMessageAsync(webhookId, webhookToken, messageId, builder, attachments); - } - - /// - /// Deletes a message that was created by a webhook. - /// - /// Webhook ID - /// Webhook token - /// The ID of the message to delete - /// - public async Task DeleteWebhookMessageAsync(ulong webhookId, string webhookToken, ulong messageId) - => await this.ApiClient.DeleteWebhookMessageAsync(webhookId, webhookToken, messageId); - #endregion - - #region Reactions - /// - /// Creates a new reaction - /// - /// Channel ID - /// Message ID - /// Emoji to react - /// - public async Task CreateReactionAsync(ulong channelId, ulong messageId, string emoji) - => await this.ApiClient.CreateReactionAsync(channelId, messageId, emoji); - - /// - /// Deletes own reaction - /// - /// Channel ID - /// Message ID - /// Emoji to remove from reaction - /// - public async Task DeleteOwnReactionAsync(ulong channelId, ulong messageId, string emoji) - => await this.ApiClient.DeleteOwnReactionAsync(channelId, messageId, emoji); - - /// - /// Deletes someone elses reaction - /// - /// Channel ID - /// Message ID - /// User ID - /// Emoji to remove - /// Reason why this reaction was removed - /// - public async Task DeleteUserReactionAsync(ulong channelId, ulong messageId, ulong userId, string emoji, string reason) - => await this.ApiClient.DeleteUserReactionAsync(channelId, messageId, userId, emoji, reason); - - /// - /// Gets all users that reacted with a specific emoji to a message - /// - /// Channel ID - /// Message ID - /// Emoji to check for - /// Whether to search for reactions after this message id. - /// The maximum amount of reactions to fetch. - /// - public async Task> GetReactionsAsync(ulong channelId, ulong messageId, string emoji, ulong? afterId = null, int limit = 25) - => await this.ApiClient.GetReactionsAsync(channelId, messageId, emoji, afterId, limit); - - /// - /// Gets all users that reacted with a specific emoji to a message - /// - /// Channel ID - /// Message ID - /// Emoji to check for - /// Whether to search for reactions after this message id. - /// The maximum amount of reactions to fetch. - /// - public async Task> GetReactionsAsync(ulong channelId, ulong messageId, DiscordEmoji emoji, ulong? afterId = null, int limit = 25) - => await this.ApiClient.GetReactionsAsync(channelId, messageId, emoji.ToReactionString(), afterId, limit); - - /// - /// Deletes all reactions from a message - /// - /// Channel ID - /// Message ID - /// Reason why all reactions were removed - /// - public async Task DeleteAllReactionsAsync(ulong channelId, ulong messageId, string reason) - => await this.ApiClient.DeleteAllReactionsAsync(channelId, messageId, reason); - - /// - /// Deletes all reactions of a specific reaction for a message. - /// - /// The ID of the channel. - /// The ID of the message. - /// The emoji to clear. - /// - public async Task DeleteReactionsEmojiAsync(ulong channelid, ulong messageId, string emoji) - => await this.ApiClient.DeleteReactionsEmojiAsync(channelid, messageId, emoji); - - #endregion - - #region Application Commands - /// - /// Gets all the global application commands for this application. - /// - /// A list of global application commands. - public async Task> GetGlobalApplicationCommandsAsync() => - await 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 async Task> BulkOverwriteGlobalApplicationCommandsAsync(IEnumerable commands) => - await this.ApiClient.BulkOverwriteGlobalApplicationCommandsAsync(this.CurrentApplication.Id, commands); - - /// - /// Creates or overwrites a global application command. - /// - /// The command to create. - /// The created command. - public async Task CreateGlobalApplicationCommandAsync(DiscordApplicationCommand command) => - await 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 async Task GetGlobalApplicationCommandAsync(ulong commandId) => - await 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) - { - ApplicationCommandEditModel mdl = new(); - action(mdl); - ulong applicationId = this.CurrentApplication?.Id ?? (await GetCurrentApplicationAsync()).Id; - return await this.ApiClient.EditGlobalApplicationCommandAsync(applicationId, commandId, mdl.Name, mdl.Description, mdl.Options, mdl.DefaultPermission, mdl.NSFW, default, default, mdl.AllowDMUsage, mdl.DefaultMemberPermissions); - } - - /// - /// Deletes a global application command. - /// - /// The ID of the command to delete. - public async Task DeleteGlobalApplicationCommandAsync(ulong commandId) => - await 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 async Task> GetGuildApplicationCommandsAsync(ulong guildId) => - await 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 async Task> BulkOverwriteGuildApplicationCommandsAsync(ulong guildId, IEnumerable commands) => - await 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 async Task CreateGuildApplicationCommandAsync(ulong guildId, DiscordApplicationCommand command) => - await 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 async Task GetGuildApplicationCommandAsync(ulong guildId, ulong commandId) => - await 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) - { - ApplicationCommandEditModel mdl = new(); - action(mdl); - ulong applicationId = this.CurrentApplication?.Id ?? (await GetCurrentApplicationAsync()).Id; - return await this.ApiClient.EditGuildApplicationCommandAsync(applicationId, guildId, commandId, mdl.Name, mdl.Description, mdl.Options, mdl.DefaultPermission, mdl.NSFW, default, default, mdl.AllowDMUsage, mdl.DefaultMemberPermissions); - } - - /// - /// Deletes a application command in a guild. - /// - /// The ID of the guild to delete the application command in. - /// The ID of the command. - public async Task DeleteGuildApplicationCommandAsync(ulong guildId, ulong commandId) => - await this.ApiClient.DeleteGuildApplicationCommandAsync(this.CurrentApplication.Id, guildId, commandId); - - /// - /// Creates a response to an interaction. - /// - /// The ID of the interaction. - /// The token of the interaction - /// The type of the response. - /// The data, if any, to send. - public async Task CreateInteractionResponseAsync(ulong interactionId, string interactionToken, DiscordInteractionResponseType type, DiscordInteractionResponseBuilder builder = null) => - await this.ApiClient.CreateInteractionResponseAsync(interactionId, interactionToken, type, builder); - - /// - /// Gets the original interaction response. - /// - /// The original message that was sent. This does not work on ephemeral messages. - public async Task GetOriginalInteractionResponseAsync(string interactionToken) => - await this.ApiClient.GetOriginalInteractionResponseAsync(this.CurrentApplication.Id, interactionToken); - - /// - /// Edits the original interaction response. - /// - /// The token of the interaction. - /// The webhook builder. - /// Attached files to keep. - /// The edited. - public async Task EditOriginalInteractionResponseAsync(string interactionToken, DiscordWebhookBuilder builder, IEnumerable attachments = default) - { - builder.Validate(isInteractionResponse: true); - - return await this.ApiClient.EditOriginalInteractionResponseAsync(this.CurrentApplication.Id, interactionToken, builder, attachments); - } - - /// - /// Deletes the original interaction response. - /// The token of the interaction. - /// > - public async Task DeleteOriginalInteractionResponseAsync(string interactionToken) => - await this.ApiClient.DeleteOriginalInteractionResponseAsync(this.CurrentApplication.Id, interactionToken); - - /// - /// Creates a follow up message to an interaction. - /// - /// The token of the interaction. - /// The webhook builder. - /// The created. - public async Task CreateFollowupMessageAsync(string interactionToken, DiscordFollowupMessageBuilder builder) - { - builder.Validate(); - - return await this.ApiClient.CreateFollowupMessageAsync(this.CurrentApplication.Id, interactionToken, builder); - } - - /// - /// Edits a follow up message. - /// - /// The token of the interaction. - /// The ID of the follow up message. - /// The webhook builder. - /// Attached files to keep. - /// The edited. - public async Task EditFollowupMessageAsync(string interactionToken, ulong messageId, DiscordWebhookBuilder builder, IEnumerable attachments = default) - { - builder.Validate(isFollowup: true); - - return await this.ApiClient.EditFollowupMessageAsync(this.CurrentApplication.Id, interactionToken, messageId, builder, attachments); - } - - /// - /// Deletes a follow up message. - /// - /// The token of the interaction. - /// The ID of the follow up message. - public async Task DeleteFollowupMessageAsync(string interactionToken, ulong messageId) => - await this.ApiClient.DeleteFollowupMessageAsync(this.CurrentApplication.Id, interactionToken, messageId); - - /// - /// Gets all application command permissions in a guild. - /// - /// The guild ID. - /// A list of permissions. - public async Task> GetGuildApplicationCommandsPermissionsAsync(ulong guildId) - => await this.ApiClient.GetGuildApplicationCommandPermissionsAsync(this.CurrentApplication.Id, guildId); - - /// - /// Gets permissions for a application command in a guild. - /// - /// The guild ID. - /// The ID of the command to get them for. - /// The permissions. - public async Task GetGuildApplicationCommandPermissionsAsync(ulong guildId, ulong commandId) - => await this.ApiClient.GetApplicationCommandPermissionsAsync(this.CurrentApplication.Id, guildId, commandId); - - /// - /// Edits permissions for a application command in a guild. - /// - /// The guild ID. - /// The ID of the command to edit permissions for. - /// The list of permissions to use. - /// The edited permissions. - public async Task EditApplicationCommandPermissionsAsync(ulong guildId, ulong commandId, IEnumerable permissions) - => await this.ApiClient.EditApplicationCommandPermissionsAsync(this.CurrentApplication.Id, guildId, commandId, permissions); - - /// - /// Batch edits permissions for a application command in a guild. - /// - /// The guild ID. - /// The list of permissions to use. - /// A list of edited permissions. - public async Task> BatchEditApplicationCommandPermissionsAsync(ulong guildId, IEnumerable permissions) - => await this.ApiClient.BatchEditApplicationCommandPermissionsAsync(this.CurrentApplication.Id, guildId, permissions); - - public async Task GetFollowupMessageAsync(string interactionToken, ulong messageId) - => await this.ApiClient.GetFollowupMessageAsync(this.CurrentApplication.Id, interactionToken, messageId); - - #endregion - - #region Stickers - - /// - /// Gets a sticker from a guild. - /// - /// The ID of the guild. - /// The ID of the sticker. - public async Task GetGuildStickerAsync(ulong guildId, ulong stickerId) - => await this.ApiClient.GetGuildStickerAsync(guildId, stickerId); - - /// - /// Gets a sticker by its ID. - /// - /// The ID of the sticker. - public async Task GetStickerAsync(ulong stickerId) - => await this.ApiClient.GetStickerAsync(stickerId); - - /// - /// Gets a collection of sticker packs that may be used by nitro users. - /// - public async Task> GetStickerPacksAsync() - => await this.ApiClient.GetStickerPacksAsync(); - - /// - /// Gets a list of stickers from a guild. - /// - /// The ID of the guild. - public async Task> GetGuildStickersAsync(ulong guildId) - => await this.ApiClient.GetGuildStickersAsync(guildId); - - /// - /// Creates a sticker in a guild. - /// - /// The ID of the guild. - /// The name of the sticker. - /// The description of the sticker. - /// The tags of the sticker. - /// The image content of the sticker. - /// The image format of the sticker. - /// The reason this sticker is being created. - - public async Task CreateGuildStickerAsync(ulong guildId, string name, string description, string tags, Stream imageContents, DiscordStickerFormat format, string reason = null) - { - string contentType, extension; - - if (format is DiscordStickerFormat.PNG or DiscordStickerFormat.APNG) - { - contentType = "image/png"; - extension = "png"; - } - else - { - contentType = "application/json"; - extension = "json"; - } - - return await this.ApiClient.CreateGuildStickerAsync(guildId, name, description ?? string.Empty, tags, new DiscordMessageFile(null, imageContents, null, extension, contentType), reason); - } - - /// - /// Modifies a sticker in a guild. - /// - /// The ID of the guild. - /// The ID of the sticker. - /// Action to perform. - /// Reason for audit log. - public async Task ModifyGuildStickerAsync(ulong guildId, ulong stickerId, Action action, string reason = null) - { - StickerEditModel mdl = new(); - action(mdl); - return await this.ApiClient.ModifyStickerAsync(guildId, stickerId, mdl.Name, mdl.Description, mdl.Tags, reason); - } - - /// - /// Deletes a sticker in a guild. - /// - /// The ID of the guild. - /// The ID of the sticker. - /// Reason for audit log. - /// - public async Task DeleteGuildStickerAsync(ulong guildId, ulong stickerId, string reason = null) - => await this.ApiClient.DeleteStickerAsync(guildId, stickerId, reason); - - #endregion - - #region Threads - - /// - /// Creates a thread from a message. - /// - /// The ID of the channel. - /// The ID of the message - /// The name of the thread. - /// The auto archive duration. - /// Reason for audit logs. - public async Task CreateThreadFromMessageAsync(ulong channelId, ulong messageId, string name, DiscordAutoArchiveDuration archiveAfter, string reason = null) - => await this.ApiClient.CreateThreadFromMessageAsync(channelId, messageId, name, archiveAfter, reason); - - /// - /// Creates a thread. - /// - /// The ID of the channel. - /// The name of the thread. - /// The auto archive duration. - /// The type of the thread. - /// Reason for audit logs. - /// - public async Task CreateThreadAsync(ulong channelId, string name, DiscordAutoArchiveDuration archiveAfter, DiscordChannelType threadType, string reason = null) - => await this.ApiClient.CreateThreadAsync(channelId, name, archiveAfter, threadType, reason); - - /// - /// Joins a thread. - /// - /// The ID of the thread. - public async Task JoinThreadAsync(ulong threadId) - => await this.ApiClient.JoinThreadAsync(threadId); - - /// - /// Leaves a thread. - /// - /// The ID of the thread. - public async Task LeaveThreadAsync(ulong threadId) - => await this.ApiClient.LeaveThreadAsync(threadId); - - /// - /// Adds a member to a thread. - /// - /// The ID of the thread. - /// The ID of the member. - public async Task AddThreadMemberAsync(ulong threadId, ulong userId) - => await this.ApiClient.AddThreadMemberAsync(threadId, userId); - - /// - /// Removes a member from a thread. - /// - /// The ID of the thread. - /// The ID of the member. - public async Task RemoveThreadMemberAsync(ulong threadId, ulong userId) - => await this.ApiClient.RemoveThreadMemberAsync(threadId, userId); - - /// - /// Lists the members of a thread. - /// - /// The ID of the thread. - public async Task> ListThreadMembersAsync(ulong threadId) - => await this.ApiClient.ListThreadMembersAsync(threadId); - - /// - /// Lists the active threads of a guild. - /// - /// The ID of the guild. - public async Task ListActiveThreadAsync(ulong guildId) - => await this.ApiClient.ListActiveThreadsAsync(guildId); - - /// - /// Gets the threads that are public and archived for a channel. - /// - /// The ID of the guild. - /// The ID of the channel. - /// Date to filter by. - /// Limit. - public async Task ListPublicArchivedThreadsAsync(ulong guildId, ulong channelId, DateTimeOffset? before = null, int limit = 0) - => await this.ApiClient.ListPublicArchivedThreadsAsync(guildId, channelId, before?.ToString("o"), limit); - - /// - /// Gets the threads that are public and archived for a channel. - /// - /// The ID of the guild. - /// The ID of the channel. - /// Date to filter by. - /// Limit. - public async Task ListPrivateArchivedThreadAsync(ulong guildId, ulong channelId, DateTimeOffset? before = null, int limit = 0) - => await this.ApiClient.ListPrivateArchivedThreadsAsync(guildId, channelId, limit, before?.ToString("o")); - - /// - /// Gets the private archived threads the user has joined for a channel. - /// - /// The ID of the guild. - /// The ID of the channel. - /// Date to filter by. - /// Limit. - public async Task ListJoinedPrivateArchivedThreadsAsync(ulong guildId, ulong channelId, DateTimeOffset? before = null, int limit = 0) - => await this.ApiClient.ListJoinedPrivateArchivedThreadsAsync(guildId, channelId, limit, (ulong?)before?.ToUnixTimeSeconds()); - - #endregion - - #region Emoji - - /// - /// Gets a guild's emojis. - /// - /// The ID of the guild. - public async Task> GetGuildEmojisAsync(ulong guildId) - => await this.ApiClient.GetGuildEmojisAsync(guildId); - - /// - /// Gets a guild emoji. - /// - /// The ID of the guild. - /// The ID of the emoji. - public async Task GetGuildEmojiAsync(ulong guildId, ulong emojiId) - => await this.ApiClient.GetGuildEmojiAsync(guildId, emojiId); - - /// - /// Creates an emoji in a guild. - /// - /// Name of the emoji. - /// The ID of the guild. - /// Image to use as the emoji. - /// Roles for which the emoji will be available. - /// Reason for audit logs. - public async Task CreateEmojiAsync(ulong guildId, string name, Stream image, IEnumerable roles = null, string reason = null) - { - if (string.IsNullOrWhiteSpace(name)) - { - throw new ArgumentNullException(nameof(name)); - } - - name = name.Trim(); - if (name.Length is < 2 or > 50) - { - throw new ArgumentException("Emoji name needs to be between 2 and 50 characters long."); - } - - ArgumentNullException.ThrowIfNull(image); - string image64; - using InlineMediaTool imgtool = new(image); - image64 = imgtool.GetBase64(); - - return await this.ApiClient.CreateGuildEmojiAsync(guildId, name, image64, roles, reason); - } - - /// - /// Modifies a guild's emoji. - /// - /// The ID of the guild. - /// The ID of the emoji. - /// New name of the emoji. - /// Roles for which the emoji will be available. - /// Reason for audit logs. - public async Task ModifyGuildEmojiAsync(ulong guildId, ulong emojiId, string name, IEnumerable roles = null, string reason = null) - => await this.ApiClient.ModifyGuildEmojiAsync(guildId, emojiId, name, roles, reason); - - /// - /// Deletes a guild's emoji. - /// - /// The ID of the guild. - /// The ID of the emoji. - /// Reason for audit logs. - public async Task DeleteGuildEmojiAsync(ulong guildId, ulong emojiId, string reason = null) - => await this.ApiClient.DeleteGuildEmojiAsync(guildId, emojiId, reason); - - #endregion - - #region Misc - /// - /// Gets assets from an application - /// - /// Application to get assets from - /// - public async Task> GetApplicationAssetsAsync(DiscordApplication application) - => await this.ApiClient.GetApplicationAssetsAsync(application); - - /// - /// Gets a guild template by the code. - /// - /// The code of the template. - /// The guild template for the code.\ - public async Task GetTemplateAsync(string code) - => await this.ApiClient.GetTemplateAsync(code); - - protected virtual void Dispose(bool disposing) - { - if (!this.disposedValue) - { - if (disposing) - { - this.guilds = null; - this.ApiClient?.rest?.Dispose(); - } - - this.disposedValue = true; - } - } - - public override void Dispose() - { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: true); - GC.SuppressFinalize(this); - } - #endregion -} +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; + +using DSharpPlus.Entities; +using DSharpPlus.Exceptions; +using DSharpPlus.Net; +using DSharpPlus.Net.Abstractions; +using DSharpPlus.Net.Models; + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace DSharpPlus; + +public class DiscordRestClient : BaseDiscordClient +{ + /// + /// Gets the dictionary of guilds cached by this client. + /// + public override IReadOnlyDictionary Guilds + => this.guilds; + + internal ConcurrentDictionary guilds = []; + private bool disposedValue; + + public string Token { get; } + + public TokenType TokenType { get; } + + public DiscordRestClient(RestClientOptions options, string token, TokenType tokenType, ILogger? logger = null) : base() + { + string headerTokenType = tokenType == TokenType.Bot ? "Bot" : "Bearer"; + + HttpClient httpClient = new(); + httpClient.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", $"{headerTokenType} {token}"); + + this.ApiClient = new(new + ( + httpClient, + options.Timeout, + logger ?? NullLogger.Instance, + options.MaximumRatelimitRetries, + (int)options.RatelimitRetryDelayFallback.TotalMilliseconds, + (int)options.InitialRequestTimeout.TotalMilliseconds, + options.MaximumConcurrentRestRequests + )); + + this.ApiClient.SetClient(this); + this.Token = token; + this.TokenType = tokenType; + } + + /// + /// Initializes cache + /// + /// + public async Task InitializeCacheAsync() + { + await base.InitializeAsync(); + IReadOnlyList currentUserGuilds = await this.ApiClient.GetCurrentUserGuildsAsync(); + foreach (DiscordGuild guild in currentUserGuilds) + { + this.guilds[guild.Id] = guild; + } + } + + #region Scheduled Guild Events + + /// + /// Creates a new scheduled guild event. + /// + /// The guild to create an event on. + /// The name of the event, up to 100 characters. + /// The description of the event, up to 1000 characters. + /// The channel the event will take place in, if applicable. + /// The type of event. If , a end time must be specified. + /// The image of event. + /// The privacy level of the event. + /// When the event starts. Must be in the future and before the end date, if specified. + /// When the event ends. Required for + /// Where this location takes place. + /// The created event. + public async Task CreateScheduledGuildEventAsync(ulong guildId, string name, string description, ulong? channelId, DiscordScheduledGuildEventType type, DiscordScheduledGuildEventPrivacyLevel privacyLevel, DateTimeOffset start, DateTimeOffset? end, Stream? image = null, string location = null) + => await this.ApiClient.CreateScheduledGuildEventAsync(guildId, name, description, start, type, privacyLevel, new DiscordScheduledGuildEventMetadata(location), end, channelId, image); + + /// + /// Delete a scheduled guild event. + /// + /// The ID the guild the event resides on. + /// The ID of the event to delete. + public async Task DeleteScheduledGuildEventAsync(ulong guildId, ulong eventId) + => await this.ApiClient.DeleteScheduledGuildEventAsync(guildId, eventId); + + /// + /// Gets a specific scheduled guild event. + /// + /// The ID of the guild the event resides on. + /// The ID of the event to get + /// The requested event. + public async Task GetScheduledGuildEventAsync(ulong guildId, ulong eventId) + => await this.ApiClient.GetScheduledGuildEventAsync(guildId, eventId); + + /// + /// Gets all available scheduled guild events. + /// + /// The ID of the guild to query. + /// All active and scheduled events. + public async Task> GetScheduledGuildEventsAsync(ulong guildId) + => await this.ApiClient.GetScheduledGuildEventsAsync(guildId); + + /// + /// Modify a scheduled guild event. + /// + /// The ID of the guild the event resides on. + /// The ID of the event to modify. + /// The action to apply to the event. + /// The modified event. + public async Task ModifyScheduledGuildEventAsync(ulong guildId, ulong eventId, Action mdl) + { + ScheduledGuildEventEditModel model = new(); + mdl(model); + + if (model.Type.HasValue && model.Type.Value is DiscordScheduledGuildEventType.StageInstance or DiscordScheduledGuildEventType.VoiceChannel) + { + if (!model.Channel.HasValue) + { + throw new ArgumentException("Channel must be supplied if the event is a stage instance or voice channel event."); + } + } + + if (model.Type.HasValue && model.Type.Value is DiscordScheduledGuildEventType.External) + { + if (!model.EndTime.HasValue) + { + throw new ArgumentException("End must be supplied if the event is an external event."); + } + + if (!model.Metadata.HasValue || string.IsNullOrEmpty(model.Metadata.Value.Location)) + { + throw new ArgumentException("Location must be supplied if the event is an external event."); + } + + if (model.Channel.HasValue && model.Channel.Value is not null) + { + throw new ArgumentException("Channel must not be supplied if the event is an external event."); + } + } + + // We only have an ID to work off of, so we have no validation as to the current state of the event. + return model.Status.HasValue && model.Status.Value is DiscordScheduledGuildEventStatus.Scheduled + ? throw new ArgumentException("Status cannot be set to scheduled.") + : await this.ApiClient.ModifyScheduledGuildEventAsync( + guildId, eventId, + model.Name, model.Description, + model.Channel.IfPresent(c => c?.Id), + model.StartTime, model.EndTime, + model.Type, model.PrivacyLevel, + model.Metadata, model.Status); + } + + /// + /// Gets the users interested in the guild event. + /// + /// The ID of the guild the event resides on. + /// The ID of the event. + /// How many users to query. + /// Fetch users after this ID. + /// Fetch users before this ID. + /// The users interested in the event. + public async Task> GetScheduledGuildEventUsersAsync(ulong guildId, ulong eventId, int limit = 100, ulong? after = null, ulong? before = null) + { + int remaining = limit; + ulong? last = null; + bool isAfter = after is not null; + + List users = []; + + int lastCount; + do + { + int fetchSize = remaining > 100 ? 100 : remaining; + IReadOnlyList fetch = await this.ApiClient.GetScheduledGuildEventUsersAsync(guildId, eventId, true, fetchSize, !isAfter ? last ?? before : null, isAfter ? last ?? after : null); + + lastCount = fetch.Count; + remaining -= lastCount; + + if (!isAfter) + { + users.AddRange(fetch); + last = fetch.LastOrDefault()?.Id; + } + else + { + users.InsertRange(0, fetch); + last = fetch.FirstOrDefault()?.Id; + } + } + while (remaining > 0 && lastCount > 0); + + return users.AsReadOnly(); + } + + #endregion + + #region Guild + + /// + /// Searches the given guild for members who's display name start with the specified name. + /// + /// The ID of the guild to search. + /// The name to search for. + /// The maximum amount of members to return. Max 1000. Defaults to 1. + /// The members found, if any. + public async Task> SearchMembersAsync(ulong guildId, string name, int? limit = 1) + => await this.ApiClient.SearchMembersAsync(guildId, name, limit); + + /// + /// Creates a new guild + /// + /// New guild's name + /// New guild's region ID + /// New guild's icon (base64) + /// New guild's verification level + /// New guild's default message notification level + /// New guild's system channel flags + /// + public async Task CreateGuildAsync(string name, string regionId, string iconb64, DiscordVerificationLevel? verificationLevel, DiscordDefaultMessageNotifications? defaultMessageNotifications, DiscordSystemChannelFlags? systemChannelFlags) + => await this.ApiClient.CreateGuildAsync(name, regionId, 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. + public async Task CreateGuildFromTemplateAsync(string code, string name, string icon) + => await this.ApiClient.CreateGuildFromTemplateAsync(code, name, icon); + + /// + /// Deletes a guild + /// + /// Guild ID + /// + public async Task DeleteGuildAsync(ulong id) + => await this.ApiClient.DeleteGuildAsync(id); + + /// + /// Modifies a guild + /// + /// Guild ID + /// New guild Name + /// New guild voice region + /// New guild verification level + /// New guild default message notification level + /// New guild MFA level + /// New guild explicit content filter level + /// New guild AFK channel ID + /// New guild AFK timeout in seconds + /// New guild icon (base64) + /// New guild owner ID + /// New guild splash (base64) + /// New guild system channel ID + /// New guild banner + /// New guild description + /// New guild Discovery splash + /// List of new guild features + /// New preferred locale + /// New updates channel ID + /// New rules channel ID + /// New system channel flags + /// Modify reason + /// + public async Task ModifyGuildAsync(ulong guildId, Optional name, + Optional region, Optional verificationLevel, + Optional defaultMessageNotifications, Optional mfaLevel, + Optional explicitContentFilter, Optional afkChannelId, + Optional afkTimeout, Optional iconb64, Optional ownerId, Optional splashb64, + Optional systemChannelId, Optional banner, Optional description, + Optional discorverySplash, Optional> features, Optional preferredLocale, + Optional publicUpdatesChannelId, Optional rulesChannelId, Optional systemChannelFlags, + string reason) + => await this.ApiClient.ModifyGuildAsync(guildId, name, region, verificationLevel, defaultMessageNotifications, mfaLevel, explicitContentFilter, afkChannelId, afkTimeout, iconb64, + ownerId, splashb64, systemChannelId, banner, description, discorverySplash, features, preferredLocale, publicUpdatesChannelId, rulesChannelId, systemChannelFlags, reason); + + /// + /// Modifies a guild + /// + /// Guild ID + /// Guild modifications + /// + public async Task ModifyGuildAsync(ulong guildId, Action action) + { + GuildEditModel mdl = new(); + action(mdl); + + if (mdl.AfkChannel.HasValue) + { + if (mdl.AfkChannel.Value.Type != DiscordChannelType.Voice) + { + throw new ArgumentException("AFK channel needs to be a voice channel!"); + } + } + + Optional iconb64 = Optional.FromNoValue(); + if (mdl.Icon.HasValue && mdl.Icon.Value is not null) + { + using InlineMediaTool imgtool = new(mdl.Icon.Value); + iconb64 = imgtool.GetBase64(); + } + else if (mdl.Icon.HasValue) + { + iconb64 = null; + } + + Optional splashb64 = Optional.FromNoValue(); + if (mdl.Splash.HasValue && mdl.Splash.Value is not null) + { + using InlineMediaTool imgtool = new(mdl.Splash.Value); + splashb64 = imgtool.GetBase64(); + } + else if (mdl.Splash.HasValue) + { + splashb64 = null; + } + + Optional bannerb64 = Optional.FromNoValue(); + + if (mdl.Banner.HasValue && mdl.Banner.Value is not null) + { + using InlineMediaTool imgtool = new(mdl.Banner.Value); + bannerb64 = imgtool.GetBase64(); + } + else if (mdl.Banner.HasValue) + { + bannerb64 = null; + } + + return await this.ApiClient.ModifyGuildAsync(guildId, mdl.Name, mdl.Region.IfPresent(x => x.Id), mdl.VerificationLevel, mdl.DefaultMessageNotifications, + mdl.MfaLevel, mdl.ExplicitContentFilter, mdl.AfkChannel.IfPresent(x => x?.Id), mdl.AfkTimeout, iconb64, mdl.Owner.IfPresent(x => x.Id), + splashb64, mdl.SystemChannel.IfPresent(x => x?.Id), bannerb64, mdl.Description, mdl.DiscoverySplash, mdl.Features, mdl.PreferredLocale, + mdl.PublicUpdatesChannel.IfPresent(e => e?.Id), mdl.RulesChannel.IfPresent(e => e?.Id), mdl.SystemChannelFlags, mdl.AuditLogReason); + } + + /// + /// Gets guild bans. + /// + /// The ID of the guild to get the bans from. + /// The number of users to return (up to maximum 1000, default 1000). + /// Consider only users before the given user ID. + /// Consider only users after the given user ID. + /// A collection of the guild's bans. + public async Task> GetGuildBansAsync(ulong guildId, int? limit = null, ulong? before = null, ulong? after = null) + => await this.ApiClient.GetGuildBansAsync(guildId, limit, before, after); + + /// + /// Gets the ban of the specified user. Requires Ban Members permission. + /// + /// The ID of the guild to get the ban from. + /// The ID of the user to get the ban for. + /// A guild ban object. + public async Task GetGuildBanAsync(ulong guildId, ulong userId) + => await this.ApiClient.GetGuildBanAsync(guildId, userId); + + /// + /// Creates guild ban + /// + /// Guild ID + /// User ID + /// Days to delete messages + /// Reason why this member was banned + /// + public async Task CreateGuildBanAsync(ulong guildId, ulong userId, int deleteMessageDays, string reason) + => await this.ApiClient.CreateGuildBanAsync(guildId, userId, deleteMessageDays, reason); + + /// + /// Creates multiple guild bans + /// + /// Guild ID + /// Collection of user ids to ban + /// Timespan in seconds to delete messages from the banned users + /// Auditlog reason + /// + public async Task CreateGuildBansAsync(ulong guildId, IEnumerable userIds, int deleteMessageSeconds, string reason) + => await this.ApiClient.CreateGuildBulkBanAsync(guildId, userIds, deleteMessageSeconds, reason); + + /// + /// Removes a guild ban + /// + /// Guild ID + /// User to unban + /// Reason why this member was unbanned + /// + public async Task RemoveGuildBanAsync(ulong guildId, ulong userId, string reason) + => await this.ApiClient.RemoveGuildBanAsync(guildId, userId, reason); + + /// + /// Leaves a guild + /// + /// Guild ID + /// + public async Task LeaveGuildAsync(ulong guildId) + => await this.ApiClient.LeaveGuildAsync(guildId); + + /// + /// Adds a member to a guild + /// + /// Guild ID + /// User ID + /// Access token + /// User nickname + /// Ids of roles to add to the new member. + /// Whether this user should be muted on join + /// Whether this user should be deafened on join + /// Only returns the member if they were not already in the guild + public async Task AddGuildMemberAsync(ulong guildId, ulong userId, string accessToken, string nick, IEnumerable roleIds, bool muted, bool deafened) + => await this.ApiClient.AddGuildMemberAsync(guildId, userId, accessToken, muted, deafened, nick, roleIds); + + /// + /// Gets all guild members + /// + /// Guild ID + /// Member download limit + /// Gets members after this ID + /// + public async Task> ListGuildMembersAsync(ulong guildId, int? limit, ulong? after) + { + List recmbr = []; + + int recd = limit ?? 1000; + int lim = limit ?? 1000; + ulong? last = after; + while (recd == lim) + { + IReadOnlyList tms = await this.ApiClient.ListGuildMembersAsync(guildId, lim, last == 0 ? null : last); + recd = tms.Count; + + foreach (TransportMember xtm in tms) + { + last = xtm.User.Id; + + if (this.UserCache.ContainsKey(xtm.User.Id)) + { + continue; + } + + DiscordUser usr = new(xtm.User) + { + Discord = this + }; + + UpdateUserCache(usr); + } + + recmbr.AddRange(tms.Select(xtm => new DiscordMember(xtm) { Discord = this, guild_id = guildId })); + } + + return new ReadOnlyCollection(recmbr); + } + + /// + /// Add role to guild member + /// + /// Guild ID + /// User ID + /// Role ID + /// Reason this role gets added + /// + public async Task AddGuildMemberRoleAsync(ulong guildId, ulong userId, ulong roleId, string reason) + => await this.ApiClient.AddGuildMemberRoleAsync(guildId, userId, roleId, reason); + + /// + /// Remove role from member + /// + /// Guild ID + /// User ID + /// Role ID + /// Reason this role gets removed + /// + public async Task RemoveGuildMemberRoleAsync(ulong guildId, ulong userId, ulong roleId, string reason) + => await this.ApiClient.RemoveGuildMemberRoleAsync(guildId, userId, roleId, reason); + + /// + /// Updates a role's position + /// + /// Guild ID + /// Role ID + /// Reason this position was modified + /// + public async Task UpdateRolePositionAsync(ulong guildId, ulong roleId, string reason = null) + { + List rgrrps = + [ + new() + { + RoleId = roleId + } + ]; + await this.ApiClient.ModifyGuildRolePositionsAsync(guildId, rgrrps, reason); + } + + /// + /// Updates a channel's position + /// + /// Guild ID + /// Channel ID + /// Channel position + /// Reason this position was modified + /// Whether to sync channel permissions with the parent, if moving to a new category. + /// The new parent id if the channel is to be moved to a new category. + /// + public async Task UpdateChannelPositionAsync(ulong guildId, ulong channelId, int position, string reason, bool? lockPermissions = null, ulong? parentId = null) + { + List rgcrps = + [ + new() + { + ChannelId = channelId, + Position = position, + LockPermissions = lockPermissions, + ParentId = parentId + } + ]; + await this.ApiClient.ModifyGuildChannelPositionAsync(guildId, rgcrps, reason); + } + + /// + /// Gets a guild's widget + /// + /// Guild ID + /// + public async Task GetGuildWidgetAsync(ulong guildId) + => await this.ApiClient.GetGuildWidgetAsync(guildId); + + /// + /// Gets a guild's widget settings + /// + /// Guild ID + /// + public async Task GetGuildWidgetSettingsAsync(ulong guildId) + => await this.ApiClient.GetGuildWidgetSettingsAsync(guildId); + + /// + /// Modifies a guild's widget settings + /// + /// Guild ID + /// If the widget is enabled or not + /// Widget channel ID + /// Reason the widget settings were modified + /// + public async Task ModifyGuildWidgetSettingsAsync(ulong guildId, bool? enabled = null, ulong? channelId = null, string reason = null) + => await this.ApiClient.ModifyGuildWidgetSettingsAsync(guildId, enabled, channelId, reason); + + /// + /// Gets a guild's membership screening form. + /// + /// Guild ID + /// The guild's membership screening form. + public async Task GetGuildMembershipScreeningFormAsync(ulong guildId) + => await this.ApiClient.GetGuildMembershipScreeningFormAsync(guildId); + + /// + /// Modifies a guild's membership screening form. + /// + /// Guild ID + /// Action to perform + /// The modified screening form. + public async Task ModifyGuildMembershipScreeningFormAsync(ulong guildId, Action action) + { + MembershipScreeningEditModel mdl = new(); + action(mdl); + return await this.ApiClient.ModifyGuildMembershipScreeningFormAsync(guildId, mdl.Enabled, mdl.Fields, mdl.Description); + } + + /// + /// Gets a guild's vanity url + /// + /// The ID of the guild. + /// The guild's vanity url. + public async Task GetGuildVanityUrlAsync(ulong guildId) + => await this.ApiClient.GetGuildVanityUrlAsync(guildId); + + /// + /// Updates the current user's suppress state in a stage channel. + /// + /// The ID of the guild. + /// The ID of the channel. + /// Toggles the suppress state. + /// Sets the time the user requested to speak. + public async Task UpdateCurrentUserVoiceStateAsync(ulong guildId, ulong channelId, bool? suppress, DateTimeOffset? requestToSpeakTimestamp = null) + => await this.ApiClient.UpdateCurrentUserVoiceStateAsync(guildId, channelId, suppress, requestToSpeakTimestamp); + + /// + /// Updates a member's suppress state in a stage channel. + /// + /// The ID of the guild. + /// The ID of the member. + /// The ID of the stage channel. + /// Toggles the member's suppress state. + /// + public async Task UpdateUserVoiceStateAsync(ulong guildId, ulong userId, ulong channelId, bool? suppress) + => await this.ApiClient.UpdateUserVoiceStateAsync(guildId, userId, channelId, suppress); + #endregion + + #region Channel + /// + /// Creates a guild channel + /// + /// Channel ID + /// Channel name + /// Channel type + /// Channel parent ID + /// Channel topic + /// Voice channel bitrate + /// Voice channel user limit + /// Channel overwrites + /// Whether this channel should be marked as NSFW + /// Slow mode timeout for users. + /// Voice channel video quality mode. + /// Sorting position of the channel. + /// Reason this channel was created + /// Default duration for newly created forum posts in the channel. + /// Default emoji used for reacting to forum posts. + /// Tags available for use by forum posts in the channel. + /// Default sorting order for forum posts in the channel. + /// + public async Task CreateGuildChannelAsync + ( + ulong id, + string name, + DiscordChannelType type, + ulong? parent, + Optional topic, + int? bitrate, + int? userLimit, + IEnumerable overwrites, + bool? nsfw, + Optional perUserRateLimit, + DiscordVideoQualityMode? qualityMode, + int? position, + string reason, + DiscordAutoArchiveDuration? defaultAutoArchiveDuration = null, + DefaultReaction? defaultReactionEmoji = null, + IEnumerable availableTags = null, + DiscordDefaultSortOrder? defaultSortOrder = null + ) => type is not (DiscordChannelType.Text or DiscordChannelType.Voice or DiscordChannelType.Category or DiscordChannelType.News or DiscordChannelType.Stage or DiscordChannelType.GuildForum) + ? throw new ArgumentException("Channel type must be text, voice, stage, category, or a forum.", nameof(type)) + : await this.ApiClient.CreateGuildChannelAsync + ( + id, + name, + type, + parent, + topic, + bitrate, + userLimit, + overwrites, + nsfw, + perUserRateLimit, + qualityMode, + position, + reason, + defaultAutoArchiveDuration, + defaultReactionEmoji, + availableTags, + defaultSortOrder + ); + + /// + /// Modifies a channel + /// + /// Channel ID + /// New channel name + /// New channel position + /// New channel topic + /// Whether this channel should be marked as NSFW + /// New channel parent + /// New voice channel bitrate + /// New voice channel user limit + /// Slow mode timeout for users. + /// New region override. + /// New video quality mode. + /// New channel type. + /// New channel permission overwrites. + /// Reason why this channel was modified + /// Channel flags. + /// Default duration for newly created forum posts in the channel. + /// Default emoji used for reacting to forum posts. + /// Tags available for use by forum posts in the channel. + /// Default per-user ratelimit for forum posts in the channel. + /// Default sorting order for forum posts in the channel. + /// Default layout for forum posts in the channel. + /// + public async Task ModifyChannelAsync + ( + ulong id, + string name, + int? position, + Optional topic, + bool? nsfw, + Optional parent, + int? bitrate, + int? userLimit, + Optional perUserRateLimit, + Optional rtcRegion, + DiscordVideoQualityMode? qualityMode, + Optional type, + IEnumerable permissionOverwrites, + string reason, + Optional flags, + IEnumerable? availableTags, + Optional defaultAutoArchiveDuration, + Optional defaultReactionEmoji, + Optional defaultPerUserRatelimit, + Optional defaultSortOrder, + Optional defaultForumLayout + ) + => await this.ApiClient.ModifyChannelAsync + ( + id, + name, + position, + topic, + nsfw, + parent, + bitrate, + userLimit, + perUserRateLimit, + rtcRegion.IfPresent(e => e?.Id), + qualityMode, + type, + permissionOverwrites, + flags, + availableTags, + defaultAutoArchiveDuration, + defaultReactionEmoji, + defaultPerUserRatelimit, + defaultSortOrder, + defaultForumLayout, + reason + ); + + /// + /// Modifies a channel + /// + /// Channel ID + /// Channel modifications + /// + public async Task ModifyChannelAsync(ulong channelId, Action action) + { + ChannelEditModel mdl = new(); + action(mdl); + + await this.ApiClient.ModifyChannelAsync + ( + channelId, mdl.Name, + mdl.Position, + mdl.Topic, + mdl.Nsfw, + mdl.Parent.HasValue ? mdl.Parent.Value?.Id : default(Optional), + mdl.Bitrate, + mdl.Userlimit, + mdl.PerUserRateLimit, + mdl.RtcRegion.IfPresent(r => r?.Id), + mdl.QualityMode, + mdl.Type, + mdl.PermissionOverwrites, + mdl.Flags, + mdl.AvailableTags, + mdl.DefaultAutoArchiveDuration, + mdl.DefaultReaction, + mdl.DefaultThreadRateLimit, + mdl.DefaultSortOrder, + mdl.DefaultForumLayout, + mdl.AuditLogReason + ); + } + + /// + /// Gets a channel object + /// + /// Channel ID + /// + public async Task GetChannelAsync(ulong id) + => await this.ApiClient.GetChannelAsync(id); + + /// + /// Deletes a channel + /// + /// Channel ID + /// Reason why this channel was deleted + /// + public async Task DeleteChannelAsync(ulong id, string reason) + => await this.ApiClient.DeleteChannelAsync(id, reason); + + /// + /// Gets message in a channel + /// + /// Channel ID + /// Message ID + /// + public async Task GetMessageAsync(ulong channelId, ulong messageId) + => await this.ApiClient.GetMessageAsync(channelId, messageId); + + /// + /// Sends a message + /// + /// Channel ID + /// Message (text) content + /// + public async Task CreateMessageAsync(ulong channelId, string content) + => await this.ApiClient.CreateMessageAsync(channelId, content, null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false, suppressNotifications: false); + + /// + /// Sends a message + /// + /// Channel ID + /// Embed to attach + /// + public async Task CreateMessageAsync(ulong channelId, DiscordEmbed embed) + => await this.ApiClient.CreateMessageAsync(channelId, null, embed is not null ? new[] { embed } : null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false, suppressNotifications: false); + + /// + /// Sends a message + /// + /// Channel ID + /// Message (text) content + /// Embed to attach + /// + public async Task CreateMessageAsync(ulong channelId, string content, DiscordEmbed embed) + => await this.ApiClient.CreateMessageAsync(channelId, content, embed is not null ? new[] { embed } : null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false, suppressNotifications: false); + + /// + /// Sends a message + /// + /// Channel ID + /// The Discord Message builder. + /// + public async Task CreateMessageAsync(ulong channelId, DiscordMessageBuilder builder) + => await this.ApiClient.CreateMessageAsync(channelId, builder); + + /// + /// Sends a message + /// + /// Channel ID + /// The Discord Message builder. + /// + public async Task CreateMessageAsync(ulong channelId, Action action) + { + DiscordMessageBuilder builder = new(); + action(builder); + return await this.ApiClient.CreateMessageAsync(channelId, builder); + } + + /// + /// Gets channels from a guild + /// + /// Guild ID + /// + public async Task> GetGuildChannelsAsync(ulong guildId) + => await this.ApiClient.GetGuildChannelsAsync(guildId); + + /// + /// Gets messages from a channel + /// + /// Channel ID + /// Limit of messages to get + /// Gets messages before this ID + /// Gets messages after this ID + /// Gets messages around this ID + /// + public async Task> GetChannelMessagesAsync(ulong channelId, int limit, ulong? before, ulong? after, ulong? around) + => await this.ApiClient.GetChannelMessagesAsync(channelId, limit, before, after, around); + + /// + /// Gets a message from a channel + /// + /// Channel ID + /// Message ID + /// + public async Task GetChannelMessageAsync(ulong channelId, ulong messageId) + => await this.ApiClient.GetChannelMessageAsync(channelId, messageId); + + /// + /// Edits a message + /// + /// Channel ID + /// Message ID + /// New message content + /// + public async Task EditMessageAsync(ulong channelId, ulong messageId, Optional content) + => await this.ApiClient.EditMessageAsync(channelId, messageId, content, default, default, default, Array.Empty()); + + /// + /// Edits a message + /// + /// Channel ID + /// Message ID + /// New message embed + /// + public async Task EditMessageAsync(ulong channelId, ulong messageId, Optional embed) + => await this.ApiClient.EditMessageAsync(channelId, messageId, default, embed.HasValue ? [embed.Value] : Array.Empty(), default, default, Array.Empty()); + + /// + /// Edits a message + /// + /// Channel ID + /// Message ID + /// The builder of the message to edit. + /// Whether to suppress embeds on the message. + /// Attached files to keep. + /// + public async Task EditMessageAsync(ulong channelId, ulong messageId, DiscordMessageBuilder builder, bool suppressEmbeds = false, IEnumerable attachments = default) + { + builder.Validate(); + + return await this.ApiClient.EditMessageAsync(channelId, messageId, builder.Content, new Optional>(builder.Embeds), builder.mentions, builder.Components, builder.Files, suppressEmbeds ? DiscordMessageFlags.SuppressedEmbeds : null, attachments); + } + + /// + /// Modifies the visibility of embeds in a message. + /// + /// Channel ID + /// Message ID + /// Whether to hide all embeds. + public async Task ModifyEmbedSuppressionAsync(ulong channelId, ulong messageId, bool hideEmbeds) + => await this.ApiClient.EditMessageAsync(channelId, messageId, default, default, default, default, Array.Empty(), hideEmbeds ? DiscordMessageFlags.SuppressedEmbeds : null); + + /// + /// Deletes a message + /// + /// Channel ID + /// Message ID + /// Why this message was deleted + /// + public async Task DeleteMessageAsync(ulong channelId, ulong messageId, string reason) + => await this.ApiClient.DeleteMessageAsync(channelId, messageId, reason); + + /// + /// Deletes multiple messages + /// + /// Channel ID + /// Message IDs + /// Reason these messages were deleted + /// + public async Task DeleteMessagesAsync(ulong channelId, IEnumerable messageIds, string reason) + => await this.ApiClient.DeleteMessagesAsync(channelId, messageIds, reason); + + /// + /// Gets a channel's invites + /// + /// Channel ID + /// + public async Task> GetChannelInvitesAsync(ulong channelId) + => await this.ApiClient.GetChannelInvitesAsync(channelId); + + /// + /// Creates a channel invite + /// + /// Channel ID + /// For how long the invite should exist + /// How often the invite may be used + /// Whether this invite should be temporary + /// Whether this invite should be unique (false might return an existing invite) + /// Why you made an invite + /// The target type of the invite, for stream and embedded application invites. + /// The ID of the target user. + /// The ID of the target application. + /// + public async Task CreateChannelInviteAsync(ulong channelId, int maxAge, int maxUses, bool temporary, bool unique, string reason, DiscordInviteTargetType? targetType = null, ulong? targetUserId = null, ulong? targetApplicationId = null) + => await this.ApiClient.CreateChannelInviteAsync(channelId, maxAge, maxUses, temporary, unique, reason, targetType, targetUserId, targetApplicationId); + + /// + /// Deletes channel overwrite + /// + /// Channel ID + /// Overwrite ID + /// Reason it was deleted + /// + public async Task DeleteChannelPermissionAsync(ulong channelId, ulong overwriteId, string reason) + => await this.ApiClient.DeleteChannelPermissionAsync(channelId, overwriteId, reason); + + /// + /// Edits channel overwrite + /// + /// Channel ID + /// Overwrite ID + /// Permissions to allow + /// Permissions to deny + /// Overwrite type + /// Reason this overwrite was created + /// + public async Task EditChannelPermissionsAsync(ulong channelId, ulong overwriteId, DiscordPermissions allow, DiscordPermissions deny, string type, string reason) + => await this.ApiClient.EditChannelPermissionsAsync(channelId, overwriteId, allow, deny, type, reason); + + /// + /// Send a typing indicator to a channel + /// + /// Channel ID + /// + public async Task TriggerTypingAsync(ulong channelId) + => await this.ApiClient.TriggerTypingAsync(channelId); + + /// + /// Gets pinned messages + /// + /// Channel ID + /// + public async Task> GetPinnedMessagesAsync(ulong channelId) + => await this.ApiClient.GetPinnedMessagesAsync(channelId); + + /// + /// Unpins a message + /// + /// Channel ID + /// Message ID + /// + public async Task UnpinMessageAsync(ulong channelId, ulong messageId) + => await this.ApiClient.UnpinMessageAsync(channelId, messageId); + + /// + /// Joins a group DM + /// + /// Channel ID + /// DM nickname + /// + public async Task JoinGroupDmAsync(ulong channelId, string nickname) + => await this.ApiClient.AddGroupDmRecipientAsync(channelId, this.CurrentUser.Id, this.Token, nickname); + + /// + /// Adds a member to a group DM + /// + /// Channel ID + /// User ID + /// User's access token + /// Nickname for user + /// + public async Task GroupDmAddRecipientAsync(ulong channelId, ulong userId, string accessToken, string nickname) + => await this.ApiClient.AddGroupDmRecipientAsync(channelId, userId, accessToken, nickname); + + /// + /// Leaves a group DM + /// + /// Channel ID + /// + public async Task LeaveGroupDmAsync(ulong channelId) + => await this.ApiClient.RemoveGroupDmRecipientAsync(channelId, this.CurrentUser.Id); + + /// + /// Removes a member from a group DM + /// + /// Channel ID + /// User ID + /// + public async Task GroupDmRemoveRecipientAsync(ulong channelId, ulong userId) + => await this.ApiClient.RemoveGroupDmRecipientAsync(channelId, userId); + + /// + /// Creates a group DM + /// + /// Access tokens + /// Nicknames per user + /// + public async Task CreateGroupDmAsync(IEnumerable accessTokens, IDictionary nicks) + => await this.ApiClient.CreateGroupDmAsync(accessTokens, nicks); + + /// + /// Creates a group DM with current user + /// + /// Access tokens + /// Nicknames + /// + public async Task CreateGroupDmWithCurrentUserAsync(IEnumerable accessTokens, IDictionary nicks) + { + List a = accessTokens.ToList(); + a.Add(this.Token); + return await this.ApiClient.CreateGroupDmAsync(a, nicks); + } + + /// + /// Creates a DM + /// + /// Recipient user ID + /// + public async Task CreateDmAsync(ulong recipientId) + => await this.ApiClient.CreateDmAsync(recipientId); + + /// + /// Follows a news channel + /// + /// ID of the channel to follow + /// ID of the channel to crosspost messages to + /// Thrown when the current user doesn't have on the target channel + public async Task FollowChannelAsync(ulong channelId, ulong webhookChannelId) + => await this.ApiClient.FollowChannelAsync(channelId, webhookChannelId); + + /// + /// Publishes a message in a news channel to following channels + /// + /// ID of the news channel the message to crosspost belongs to + /// ID of the message to crosspost + /// + /// Thrown when the current user doesn't have and/or + /// + public async Task CrosspostMessageAsync(ulong channelId, ulong messageId) + => await this.ApiClient.CrosspostMessageAsync(channelId, messageId); + + /// + /// Creates a stage instance in a stage channel. + /// + /// The ID of the stage channel to create it in. + /// The topic of the stage instance. + /// The privacy level of the stage instance. + /// The reason the stage instance was created. + /// The created stage instance. + public async Task CreateStageInstanceAsync(ulong channelId, string topic, DiscordStagePrivacyLevel? privacyLevel = null, string reason = null) + => await this.ApiClient.CreateStageInstanceAsync(channelId, topic, privacyLevel, reason); + + /// + /// Gets a stage instance in a stage channel. + /// + /// The ID of the channel. + /// The stage instance in the channel. + public async Task GetStageInstanceAsync(ulong channelId) + => await this.ApiClient.GetStageInstanceAsync(channelId); + + /// + /// Modifies a stage instance in a stage channel. + /// + /// The ID of the channel to modify the stage instance of. + /// Action to perform. + /// The modified stage instance. + public async Task ModifyStageInstanceAsync(ulong channelId, Action action) + { + StageInstanceEditModel mdl = new(); + action(mdl); + return await this.ApiClient.ModifyStageInstanceAsync(channelId, mdl.Topic, mdl.PrivacyLevel, mdl.AuditLogReason); + } + + /// + /// Deletes a stage instance in a stage channel. + /// + /// The ID of the channel to delete the stage instance of. + /// The reason the stage instance was deleted. + public async Task DeleteStageInstanceAsync(ulong channelId, string reason = null) + => await this.ApiClient.DeleteStageInstanceAsync(channelId, reason); + + /// + /// Pins a message. + /// + /// The ID of the channel the message is in. + /// The ID of the message. + public async Task PinMessageAsync(ulong channelId, ulong messageId) + => await this.ApiClient.PinMessageAsync(channelId, messageId); + + #endregion + + #region Member + /// + /// Gets current user object + /// + /// + public async Task GetCurrentUserAsync() + => await this.ApiClient.GetCurrentUserAsync(); + + /// + /// Gets user object + /// + /// User ID + /// + public async Task GetUserAsync(ulong user) + => await this.ApiClient.GetUserAsync(user); + + /// + /// Gets guild member + /// + /// Guild ID + /// Member ID + /// + public async Task GetGuildMemberAsync(ulong guildId, ulong memberId) + => await this.ApiClient.GetGuildMemberAsync(guildId, memberId); + + /// + /// Removes guild member + /// + /// Guild ID + /// User ID + /// Why this user was removed + /// + public async Task RemoveGuildMemberAsync(ulong guildId, ulong userId, string reason) + => await this.ApiClient.RemoveGuildMemberAsync(guildId, userId, reason); + + /// + /// Modifies current user + /// + /// New username + /// New avatar (base64) + /// New banner (base64) + /// + public async Task ModifyCurrentUserAsync(string username, string base64Avatar, string base64Banner) + => new DiscordUser(await this.ApiClient.ModifyCurrentUserAsync(username, base64Avatar, base64Banner)) { Discord = this }; + + /// + /// Modifies current user + /// + /// username + /// avatar + /// New banner + /// + public async Task ModifyCurrentUserAsync(string username = null, Stream? avatar = null, Stream? banner = null) + { + string avatarBase64 = null; + if (avatar is not null) + { + using InlineMediaTool imgtool = new(avatar); + avatarBase64 = imgtool.GetBase64(); + } + + string bannerBase64 = null; + if (banner is not null) + { + using InlineMediaTool imgtool = new(banner); + bannerBase64 = imgtool.GetBase64(); + } + + return new DiscordUser(await this.ApiClient.ModifyCurrentUserAsync(username, avatarBase64, bannerBase64)) { Discord = this }; + } + + /// + /// Gets current user's guilds + /// + /// Limit of guilds to get + /// Gets guild before ID + /// Gets guilds after ID + /// + public async Task> GetCurrentUserGuildsAsync(int limit = 100, ulong? before = null, ulong? after = null) + => await this.ApiClient.GetCurrentUserGuildsAsync(limit, before, after); + + /// + /// Gets the guild member for the current user in the specified guild. Only works with bearer tokens with the guilds.members.read scope. + /// + /// Guild ID + /// + public async Task GetCurrentUserGuildMemberAsync(ulong guildId) + => await this.ApiClient.GetCurrentUserGuildMemberAsync(guildId); + + /// + /// Modifies guild member. + /// + /// Guild ID + /// User ID + /// New nickname + /// New roles + /// Whether this user should be muted + /// Whether this user should be deafened + /// Voice channel to move this user to + /// How long this member should be timed out for. Requires MODERATE_MEMBERS permission. + /// Flags for this guild member. + /// Reason this user was modified + /// + public async Task ModifyGuildMemberAsync(ulong guildId, ulong userId, Optional nick, + Optional> roleIds, Optional mute, Optional deaf, + Optional voiceChannelId, Optional communicationDisabledUntil, Optional memberFlags, string reason) + => await this.ApiClient.ModifyGuildMemberAsync(guildId, userId, nick, roleIds, mute, deaf, voiceChannelId, communicationDisabledUntil, memberFlags, reason); + + /// + /// Modifies a member + /// + /// Member ID + /// Guild ID + /// Modifications + /// + public async Task ModifyAsync(ulong memberId, ulong guildId, Action action) + { + MemberEditModel mdl = new(); + action(mdl); + + if (mdl.VoiceChannel.HasValue && mdl.VoiceChannel.Value is not null && mdl.VoiceChannel.Value.Type != DiscordChannelType.Voice && mdl.VoiceChannel.Value.Type != DiscordChannelType.Stage) + { + throw new ArgumentException($"{nameof(MemberEditModel)}.{mdl.VoiceChannel} must be a voice or stage channel.", nameof(action)); + } + + if (mdl.Nickname.HasValue && this.CurrentUser.Id == memberId) + { + await this.ApiClient.ModifyCurrentMemberAsync(guildId, mdl.Nickname.Value, + mdl.AuditLogReason); + await this.ApiClient.ModifyGuildMemberAsync(guildId, memberId, Optional.FromNoValue(), + mdl.Roles.IfPresent(e => e.Select(xr => xr.Id)), mdl.Muted, mdl.Deafened, + mdl.VoiceChannel.IfPresent(e => e?.Id), mdl.CommunicationDisabledUntil, mdl.MemberFlags, mdl.AuditLogReason); + } + else + { + await this.ApiClient.ModifyGuildMemberAsync(guildId, memberId, mdl.Nickname, + mdl.Roles.IfPresent(e => e.Select(xr => xr.Id)), mdl.Muted, mdl.Deafened, + mdl.VoiceChannel.IfPresent(e => e?.Id), mdl.CommunicationDisabledUntil, mdl.MemberFlags, mdl.AuditLogReason); + } + } + + /// + /// Changes the current user in a guild. + /// + /// Guild ID + /// Nickname to set + /// Audit log reason + /// + public async Task ModifyCurrentMemberAsync(ulong guildId, string nickname, string reason) + => await this.ApiClient.ModifyCurrentMemberAsync(guildId, nickname, reason); + + #endregion + + #region Roles + /// + /// Gets roles + /// + /// Guild ID + /// + public async Task> GetGuildRolesAsync(ulong guildId) + => await this.ApiClient.GetGuildRolesAsync(guildId); + + /// + /// Gets a guild. + /// + /// The guild ID to search for. + /// Whether to include approximate presence and member counts in the returned guild. + /// + public async Task GetGuildAsync(ulong guildId, bool? withCounts = null) + => await this.ApiClient.GetGuildAsync(guildId, withCounts); + + /// + /// Modifies a role + /// + /// Guild ID + /// Role ID + /// New role name + /// New role permissions + /// New role color + /// Whether this role should be hoisted + /// Whether this role should be mentionable + /// Why this role was modified + /// The icon to add to this role + /// The emoji to add to this role. Must be unicode. + /// + public async Task ModifyGuildRoleAsync(ulong guildId, ulong roleId, string name, DiscordPermissions? permissions, DiscordColor? color, bool? hoist, bool? mentionable, string reason, Stream icon, DiscordEmoji emoji) + => await this.ApiClient.ModifyGuildRoleAsync(guildId, roleId, name, permissions, color.HasValue ? color.Value.Value : null, hoist, mentionable, icon, emoji?.ToString(), reason); + + /// + /// Modifies a role + /// + /// Role ID + /// Guild ID + /// Modifications + /// + public async Task ModifyGuildRoleAsync(ulong roleId, ulong guildId, Action action) + { + RoleEditModel mdl = new(); + action(mdl); + + await ModifyGuildRoleAsync(guildId, roleId, mdl.Name, mdl.Permissions, mdl.Color, mdl.Hoist, mdl.Mentionable, mdl.AuditLogReason, mdl.Icon, mdl.Emoji); + } + + /// + /// Deletes a role + /// + /// Guild ID + /// Role ID + /// Reason why this role was deleted + /// + public async Task DeleteGuildRoleAsync(ulong guildId, ulong roleId, string reason) + => await this.ApiClient.DeleteRoleAsync(guildId, roleId, reason); + + /// + /// Creates a new role + /// + /// Guild ID + /// Role name + /// Role permissions + /// Role color + /// Whether this role should be hoisted + /// Whether this role should be mentionable + /// Reason why this role was created + /// The icon to add to this role + /// The emoji to add to this role. Must be unicode. + /// + public async Task CreateGuildRoleAsync(ulong guildId, string name, DiscordPermissions? permissions, int? color, bool? hoist, bool? mentionable, string reason, Stream icon = null, DiscordEmoji emoji = null) + => await this.ApiClient.CreateGuildRoleAsync(guildId, name, permissions, color, hoist, mentionable, icon, emoji?.ToString(), reason); + #endregion + + #region Prune + /// + /// Get a guild's prune count. + /// + /// Guild ID + /// Days to check for + /// The roles to be included in the prune. + /// + public async Task GetGuildPruneCountAsync(ulong guildId, int days, IEnumerable includeRoles) + => await this.ApiClient.GetGuildPruneCountAsync(guildId, days, includeRoles); + + /// + /// Begins a guild prune. + /// + /// Guild ID + /// Days to prune for + /// Whether to return the prune count after this method completes. This is discouraged for larger guilds. + /// The roles to be included in the prune. + /// Reason why this guild was pruned + /// + public async Task BeginGuildPruneAsync(ulong guildId, int days, bool computePruneCount, IEnumerable includeRoles, string reason) + => await this.ApiClient.BeginGuildPruneAsync(guildId, days, computePruneCount, includeRoles, reason); + #endregion + + #region GuildVarious + /// + /// Gets guild integrations + /// + /// Guild ID + /// + public async Task> GetGuildIntegrationsAsync(ulong guildId) + => await this.ApiClient.GetGuildIntegrationsAsync(guildId); + + /// + /// Creates guild integration + /// + /// Guild ID + /// Integration type + /// Integration id + /// + public async Task CreateGuildIntegrationAsync(ulong guildId, string type, ulong id) + => await this.ApiClient.CreateGuildIntegrationAsync(guildId, type, id); + + /// + /// Modifies a guild integration + /// + /// Guild ID + /// Integration ID + /// Expiration behaviour + /// Expiration grace period + /// Whether to enable emojis for this integration + /// + public async Task ModifyGuildIntegrationAsync(ulong guildId, ulong integrationId, int expireBehaviour, int expireGracePeriod, bool enableEmoticons) + => await this.ApiClient.ModifyGuildIntegrationAsync(guildId, integrationId, expireBehaviour, expireGracePeriod, enableEmoticons); + + /// + /// Removes a guild integration + /// + /// Guild ID + /// Integration to remove + /// Reason why this integration was removed + /// + public async Task DeleteGuildIntegrationAsync(ulong guildId, DiscordIntegration integration, string reason = null) + => await this.ApiClient.DeleteGuildIntegrationAsync(guildId, integration.Id, reason); + + /// + /// Syncs guild integration + /// + /// Guild ID + /// Integration ID + /// + public async Task SyncGuildIntegrationAsync(ulong guildId, ulong integrationId) + => await this.ApiClient.SyncGuildIntegrationAsync(guildId, integrationId); + + /// + /// Get a guild's voice region + /// + /// Guild ID + /// + public async Task> GetGuildVoiceRegionsAsync(ulong guildId) + => await this.ApiClient.GetGuildVoiceRegionsAsync(guildId); + + /// + /// Get a guild's invites + /// + /// Guild ID + /// + public async Task> GetGuildInvitesAsync(ulong guildId) + => await this.ApiClient.GetGuildInvitesAsync(guildId); + + /// + /// Gets a guild's templates. + /// + /// Guild ID + /// All of the guild's templates. + public async Task> GetGuildTemplatesAsync(ulong guildId) + => await this.ApiClient.GetGuildTemplatesAsync(guildId); + + /// + /// Creates a guild template. + /// + /// Guild ID + /// Name of the template. + /// Description of the template. + /// The template created. + public async Task CreateGuildTemplateAsync(ulong guildId, string name, string description = null) + => await this.ApiClient.CreateGuildTemplateAsync(guildId, name, description); + + /// + /// Syncs the template to the current guild's state. + /// + /// Guild ID + /// The code of the template to sync. + /// The template synced. + public async Task SyncGuildTemplateAsync(ulong guildId, string code) + => await this.ApiClient.SyncGuildTemplateAsync(guildId, code); + + /// + /// Modifies the template's metadata. + /// + /// Guild ID + /// The template's code. + /// Name of the template. + /// Description of the template. + /// The template modified. + public async Task ModifyGuildTemplateAsync(ulong guildId, string code, string name = null, string description = null) + => await this.ApiClient.ModifyGuildTemplateAsync(guildId, code, name, description); + + /// + /// Deletes the template. + /// + /// Guild ID + /// The code of the template to delete. + /// The deleted template. + public async Task DeleteGuildTemplateAsync(ulong guildId, string code) + => await this.ApiClient.DeleteGuildTemplateAsync(guildId, code); + + /// + /// Gets a guild's welcome screen. + /// + /// The guild's welcome screen object. + public async Task GetGuildWelcomeScreenAsync(ulong guildId) => + await this.ApiClient.GetGuildWelcomeScreenAsync(guildId); + + /// + /// Modifies a guild's welcome screen. + /// + /// The guild ID to modify. + /// Action to perform. + /// The audit log reason for this action. + /// The modified welcome screen. + public async Task ModifyGuildWelcomeScreenAsync(ulong guildId, Action action, string reason = null) + { + WelcomeScreenEditModel mdl = new(); + action(mdl); + return await this.ApiClient.ModifyGuildWelcomeScreenAsync(guildId, mdl.Enabled, mdl.WelcomeChannels, mdl.Description, reason); + } + + /// + /// Gets a guild preview. + /// + /// The ID of the guild. + public async Task GetGuildPreviewAsync(ulong guildId) + => await this.ApiClient.GetGuildPreviewAsync(guildId); + + #endregion + + #region Invites + /// + /// 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. + /// + public async Task GetInviteAsync(string inviteCode, bool? withCounts = null, bool? withExpiration = null) + => await this.ApiClient.GetInviteAsync(inviteCode, withCounts, withExpiration); + + /// + /// Removes an invite + /// + /// Invite code + /// Reason why this invite was removed + /// + public async Task DeleteInviteAsync(string inviteCode, string reason) + => await this.ApiClient.DeleteInviteAsync(inviteCode, reason); + #endregion + + #region Connections + /// + /// Gets current user's connections + /// + /// + public async Task> GetUsersConnectionsAsync() + => await this.ApiClient.GetUsersConnectionsAsync(); + #endregion + + #region Webhooks + /// + /// Creates a new webhook + /// + /// Channel ID + /// Webhook name + /// Webhook avatar (base64) + /// Reason why this webhook was created + /// + public async Task CreateWebhookAsync(ulong channelId, string name, string base64Avatar, string reason) + => await this.ApiClient.CreateWebhookAsync(channelId, name, base64Avatar, reason); + + /// + /// Creates a new webhook + /// + /// Channel ID + /// Webhook name + /// Webhook avatar + /// Reason why this webhook was created + /// + public async Task CreateWebhookAsync(ulong channelId, string name, Stream avatar = null, string reason = null) + { + string av64 = null; + if (avatar is not null) + { + using InlineMediaTool imgtool = new(avatar); + av64 = imgtool.GetBase64(); + } + + return await this.ApiClient.CreateWebhookAsync(channelId, name, av64, reason); + } + + /// + /// Gets all webhooks from a channel + /// + /// Channel ID + /// + public async Task> GetChannelWebhooksAsync(ulong channelId) + => await this.ApiClient.GetChannelWebhooksAsync(channelId); + + /// + /// Gets all webhooks from a guild + /// + /// Guild ID + /// + public async Task> GetGuildWebhooksAsync(ulong guildId) + => await this.ApiClient.GetGuildWebhooksAsync(guildId); + + /// + /// Gets a webhook + /// + /// Webhook ID + /// + public async Task GetWebhookAsync(ulong webhookId) + => await this.ApiClient.GetWebhookAsync(webhookId); + + /// + /// Gets a webhook with its token (when user is not in said guild) + /// + /// Webhook ID + /// Webhook token + /// + public async Task GetWebhookWithTokenAsync(ulong webhookId, string webhookToken) + => await this.ApiClient.GetWebhookWithTokenAsync(webhookId, webhookToken); + + /// + /// Modifies a webhook + /// + /// Webhook ID + /// The new channel ID the webhook should be moved to. + /// New webhook name + /// New webhook avatar (base64) + /// Reason why this webhook was modified + /// + public async Task ModifyWebhookAsync(ulong webhookId, ulong channelId, string name, string base64Avatar, string reason) + => await this.ApiClient.ModifyWebhookAsync(webhookId, channelId, name, base64Avatar, reason); + + /// + /// Modifies a webhook + /// + /// Webhook ID + /// The new channel ID the webhook should be moved to. + /// New webhook name + /// New webhook avatar + /// Reason why this webhook was modified + /// + public async Task ModifyWebhookAsync(ulong webhookId, ulong channelId, string name, Stream avatar, string reason) + { + string av64 = null; + if (avatar is not null) + { + using InlineMediaTool imgtool = new(avatar); + av64 = imgtool.GetBase64(); + } + + return await this.ApiClient.ModifyWebhookAsync(webhookId, channelId, name, av64, reason); + } + + /// + /// Modifies a webhook (when user is not in said guild) + /// + /// Webhook ID + /// New webhook name + /// New webhook avatar (base64) + /// Webhook token + /// Reason why this webhook was modified + /// + public async Task ModifyWebhookAsync(ulong webhookId, string name, string base64Avatar, string webhookToken, string reason) + => await this.ApiClient.ModifyWebhookAsync(webhookId, name, base64Avatar, webhookToken, reason); + + /// + /// Modifies a webhook (when user is not in said guild) + /// + /// Webhook ID + /// New webhook name + /// New webhook avatar + /// Webhook token + /// Reason why this webhook was modified + /// + public async Task ModifyWebhookAsync(ulong webhookId, string name, Stream avatar, string webhookToken, string reason) + { + string av64 = null; + if (avatar is not null) + { + using InlineMediaTool imgtool = new(avatar); + av64 = imgtool.GetBase64(); + } + + return await this.ApiClient.ModifyWebhookAsync(webhookId, name, av64, webhookToken, reason); + } + + /// + /// Deletes a webhook + /// + /// Webhook ID + /// Reason this webhook was deleted + /// + public async Task DeleteWebhookAsync(ulong webhookId, string reason) + => await this.ApiClient.DeleteWebhookAsync(webhookId, reason); + + /// + /// Deletes a webhook (when user is not in said guild) + /// + /// Webhook ID + /// Reason this webhook was removed + /// Webhook token + /// + public async Task DeleteWebhookAsync(ulong webhookId, string reason, string webhookToken) + => await this.ApiClient.DeleteWebhookAsync(webhookId, webhookToken, reason); + + /// + /// Sends a message to a webhook + /// + /// Webhook ID + /// Webhook token + /// Webhook builder filled with data to send. + /// + public async Task ExecuteWebhookAsync(ulong webhookId, string webhookToken, DiscordWebhookBuilder builder) + => await this.ApiClient.ExecuteWebhookAsync(webhookId, webhookToken, builder); + + /// + /// Edits a previously-sent webhook message. + /// + /// Webhook ID + /// Webhook token + /// The ID of the message to edit. + /// The builder of the message to edit. + /// Attached files to keep. + /// The modified + public async Task EditWebhookMessageAsync(ulong webhookId, string webhookToken, ulong messageId, DiscordWebhookBuilder builder, IEnumerable attachments = default) + { + builder.Validate(true); + + return await this.ApiClient.EditWebhookMessageAsync(webhookId, webhookToken, messageId, builder, attachments); + } + + /// + /// Deletes a message that was created by a webhook. + /// + /// Webhook ID + /// Webhook token + /// The ID of the message to delete + /// + public async Task DeleteWebhookMessageAsync(ulong webhookId, string webhookToken, ulong messageId) + => await this.ApiClient.DeleteWebhookMessageAsync(webhookId, webhookToken, messageId); + #endregion + + #region Reactions + /// + /// Creates a new reaction + /// + /// Channel ID + /// Message ID + /// Emoji to react + /// + public async Task CreateReactionAsync(ulong channelId, ulong messageId, string emoji) + => await this.ApiClient.CreateReactionAsync(channelId, messageId, emoji); + + /// + /// Deletes own reaction + /// + /// Channel ID + /// Message ID + /// Emoji to remove from reaction + /// + public async Task DeleteOwnReactionAsync(ulong channelId, ulong messageId, string emoji) + => await this.ApiClient.DeleteOwnReactionAsync(channelId, messageId, emoji); + + /// + /// Deletes someone elses reaction + /// + /// Channel ID + /// Message ID + /// User ID + /// Emoji to remove + /// Reason why this reaction was removed + /// + public async Task DeleteUserReactionAsync(ulong channelId, ulong messageId, ulong userId, string emoji, string reason) + => await this.ApiClient.DeleteUserReactionAsync(channelId, messageId, userId, emoji, reason); + + /// + /// Gets all users that reacted with a specific emoji to a message + /// + /// Channel ID + /// Message ID + /// Emoji to check for + /// Whether to search for reactions after this message id. + /// The maximum amount of reactions to fetch. + /// + public async Task> GetReactionsAsync(ulong channelId, ulong messageId, string emoji, ulong? afterId = null, int limit = 25) + => await this.ApiClient.GetReactionsAsync(channelId, messageId, emoji, afterId, limit); + + /// + /// Gets all users that reacted with a specific emoji to a message + /// + /// Channel ID + /// Message ID + /// Emoji to check for + /// Whether to search for reactions after this message id. + /// The maximum amount of reactions to fetch. + /// + public async Task> GetReactionsAsync(ulong channelId, ulong messageId, DiscordEmoji emoji, ulong? afterId = null, int limit = 25) + => await this.ApiClient.GetReactionsAsync(channelId, messageId, emoji.ToReactionString(), afterId, limit); + + /// + /// Deletes all reactions from a message + /// + /// Channel ID + /// Message ID + /// Reason why all reactions were removed + /// + public async Task DeleteAllReactionsAsync(ulong channelId, ulong messageId, string reason) + => await this.ApiClient.DeleteAllReactionsAsync(channelId, messageId, reason); + + /// + /// Deletes all reactions of a specific reaction for a message. + /// + /// The ID of the channel. + /// The ID of the message. + /// The emoji to clear. + /// + public async Task DeleteReactionsEmojiAsync(ulong channelid, ulong messageId, string emoji) + => await this.ApiClient.DeleteReactionsEmojiAsync(channelid, messageId, emoji); + + #endregion + + #region Application Commands + /// + /// Gets all the global application commands for this application. + /// + /// A list of global application commands. + public async Task> GetGlobalApplicationCommandsAsync() => + await 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 async Task> BulkOverwriteGlobalApplicationCommandsAsync(IEnumerable commands) => + await this.ApiClient.BulkOverwriteGlobalApplicationCommandsAsync(this.CurrentApplication.Id, commands); + + /// + /// Creates or overwrites a global application command. + /// + /// The command to create. + /// The created command. + public async Task CreateGlobalApplicationCommandAsync(DiscordApplicationCommand command) => + await 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 async Task GetGlobalApplicationCommandAsync(ulong commandId) => + await 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) + { + ApplicationCommandEditModel mdl = new(); + action(mdl); + ulong applicationId = this.CurrentApplication?.Id ?? (await GetCurrentApplicationAsync()).Id; + return await this.ApiClient.EditGlobalApplicationCommandAsync(applicationId, commandId, mdl.Name, mdl.Description, mdl.Options, mdl.DefaultPermission, mdl.NSFW, default, default, mdl.AllowDMUsage, mdl.DefaultMemberPermissions); + } + + /// + /// Deletes a global application command. + /// + /// The ID of the command to delete. + public async Task DeleteGlobalApplicationCommandAsync(ulong commandId) => + await 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 async Task> GetGuildApplicationCommandsAsync(ulong guildId) => + await 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 async Task> BulkOverwriteGuildApplicationCommandsAsync(ulong guildId, IEnumerable commands) => + await 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 async Task CreateGuildApplicationCommandAsync(ulong guildId, DiscordApplicationCommand command) => + await 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 async Task GetGuildApplicationCommandAsync(ulong guildId, ulong commandId) => + await 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) + { + ApplicationCommandEditModel mdl = new(); + action(mdl); + ulong applicationId = this.CurrentApplication?.Id ?? (await GetCurrentApplicationAsync()).Id; + return await this.ApiClient.EditGuildApplicationCommandAsync(applicationId, guildId, commandId, mdl.Name, mdl.Description, mdl.Options, mdl.DefaultPermission, mdl.NSFW, default, default, mdl.AllowDMUsage, mdl.DefaultMemberPermissions); + } + + /// + /// Deletes a application command in a guild. + /// + /// The ID of the guild to delete the application command in. + /// The ID of the command. + public async Task DeleteGuildApplicationCommandAsync(ulong guildId, ulong commandId) => + await this.ApiClient.DeleteGuildApplicationCommandAsync(this.CurrentApplication.Id, guildId, commandId); + + /// + /// Creates a response to an interaction. + /// + /// The ID of the interaction. + /// The token of the interaction + /// The type of the response. + /// The data, if any, to send. + public async Task CreateInteractionResponseAsync(ulong interactionId, string interactionToken, DiscordInteractionResponseType type, DiscordInteractionResponseBuilder builder = null) => + await this.ApiClient.CreateInteractionResponseAsync(interactionId, interactionToken, type, builder); + + /// + /// Gets the original interaction response. + /// + /// The original message that was sent. This does not work on ephemeral messages. + public async Task GetOriginalInteractionResponseAsync(string interactionToken) => + await this.ApiClient.GetOriginalInteractionResponseAsync(this.CurrentApplication.Id, interactionToken); + + /// + /// Edits the original interaction response. + /// + /// The token of the interaction. + /// The webhook builder. + /// Attached files to keep. + /// The edited. + public async Task EditOriginalInteractionResponseAsync(string interactionToken, DiscordWebhookBuilder builder, IEnumerable attachments = default) + { + builder.Validate(isInteractionResponse: true); + + return await this.ApiClient.EditOriginalInteractionResponseAsync(this.CurrentApplication.Id, interactionToken, builder, attachments); + } + + /// + /// Deletes the original interaction response. + /// The token of the interaction. + /// > + public async Task DeleteOriginalInteractionResponseAsync(string interactionToken) => + await this.ApiClient.DeleteOriginalInteractionResponseAsync(this.CurrentApplication.Id, interactionToken); + + /// + /// Creates a follow up message to an interaction. + /// + /// The token of the interaction. + /// The webhook builder. + /// The created. + public async Task CreateFollowupMessageAsync(string interactionToken, DiscordFollowupMessageBuilder builder) + { + builder.Validate(); + + return await this.ApiClient.CreateFollowupMessageAsync(this.CurrentApplication.Id, interactionToken, builder); + } + + /// + /// Edits a follow up message. + /// + /// The token of the interaction. + /// The ID of the follow up message. + /// The webhook builder. + /// Attached files to keep. + /// The edited. + public async Task EditFollowupMessageAsync(string interactionToken, ulong messageId, DiscordWebhookBuilder builder, IEnumerable attachments = default) + { + builder.Validate(isFollowup: true); + + return await this.ApiClient.EditFollowupMessageAsync(this.CurrentApplication.Id, interactionToken, messageId, builder, attachments); + } + + /// + /// Deletes a follow up message. + /// + /// The token of the interaction. + /// The ID of the follow up message. + public async Task DeleteFollowupMessageAsync(string interactionToken, ulong messageId) => + await this.ApiClient.DeleteFollowupMessageAsync(this.CurrentApplication.Id, interactionToken, messageId); + + /// + /// Gets all application command permissions in a guild. + /// + /// The guild ID. + /// A list of permissions. + public async Task> GetGuildApplicationCommandsPermissionsAsync(ulong guildId) + => await this.ApiClient.GetGuildApplicationCommandPermissionsAsync(this.CurrentApplication.Id, guildId); + + /// + /// Gets permissions for a application command in a guild. + /// + /// The guild ID. + /// The ID of the command to get them for. + /// The permissions. + public async Task GetGuildApplicationCommandPermissionsAsync(ulong guildId, ulong commandId) + => await this.ApiClient.GetApplicationCommandPermissionsAsync(this.CurrentApplication.Id, guildId, commandId); + + /// + /// Edits permissions for a application command in a guild. + /// + /// The guild ID. + /// The ID of the command to edit permissions for. + /// The list of permissions to use. + /// The edited permissions. + public async Task EditApplicationCommandPermissionsAsync(ulong guildId, ulong commandId, IEnumerable permissions) + => await this.ApiClient.EditApplicationCommandPermissionsAsync(this.CurrentApplication.Id, guildId, commandId, permissions); + + /// + /// Batch edits permissions for a application command in a guild. + /// + /// The guild ID. + /// The list of permissions to use. + /// A list of edited permissions. + public async Task> BatchEditApplicationCommandPermissionsAsync(ulong guildId, IEnumerable permissions) + => await this.ApiClient.BatchEditApplicationCommandPermissionsAsync(this.CurrentApplication.Id, guildId, permissions); + + public async Task GetFollowupMessageAsync(string interactionToken, ulong messageId) + => await this.ApiClient.GetFollowupMessageAsync(this.CurrentApplication.Id, interactionToken, messageId); + + #endregion + + #region Stickers + + /// + /// Gets a sticker from a guild. + /// + /// The ID of the guild. + /// The ID of the sticker. + public async Task GetGuildStickerAsync(ulong guildId, ulong stickerId) + => await this.ApiClient.GetGuildStickerAsync(guildId, stickerId); + + /// + /// Gets a sticker by its ID. + /// + /// The ID of the sticker. + public async Task GetStickerAsync(ulong stickerId) + => await this.ApiClient.GetStickerAsync(stickerId); + + /// + /// Gets a collection of sticker packs that may be used by nitro users. + /// + public async Task> GetStickerPacksAsync() + => await this.ApiClient.GetStickerPacksAsync(); + + /// + /// Gets a list of stickers from a guild. + /// + /// The ID of the guild. + public async Task> GetGuildStickersAsync(ulong guildId) + => await this.ApiClient.GetGuildStickersAsync(guildId); + + /// + /// Creates a sticker in a guild. + /// + /// The ID of the guild. + /// The name of the sticker. + /// The description of the sticker. + /// The tags of the sticker. + /// The image content of the sticker. + /// The image format of the sticker. + /// The reason this sticker is being created. + + public async Task CreateGuildStickerAsync(ulong guildId, string name, string description, string tags, Stream imageContents, DiscordStickerFormat format, string reason = null) + { + string contentType, extension; + + if (format is DiscordStickerFormat.PNG or DiscordStickerFormat.APNG) + { + contentType = "image/png"; + extension = "png"; + } + else + { + contentType = "application/json"; + extension = "json"; + } + + return await this.ApiClient.CreateGuildStickerAsync(guildId, name, description ?? string.Empty, tags, new DiscordMessageFile(null, imageContents, null, extension, contentType), reason); + } + + /// + /// Modifies a sticker in a guild. + /// + /// The ID of the guild. + /// The ID of the sticker. + /// Action to perform. + /// Reason for audit log. + public async Task ModifyGuildStickerAsync(ulong guildId, ulong stickerId, Action action, string reason = null) + { + StickerEditModel mdl = new(); + action(mdl); + return await this.ApiClient.ModifyStickerAsync(guildId, stickerId, mdl.Name, mdl.Description, mdl.Tags, reason); + } + + /// + /// Deletes a sticker in a guild. + /// + /// The ID of the guild. + /// The ID of the sticker. + /// Reason for audit log. + /// + public async Task DeleteGuildStickerAsync(ulong guildId, ulong stickerId, string reason = null) + => await this.ApiClient.DeleteStickerAsync(guildId, stickerId, reason); + + #endregion + + #region Threads + + /// + /// Creates a thread from a message. + /// + /// The ID of the channel. + /// The ID of the message + /// The name of the thread. + /// The auto archive duration. + /// Reason for audit logs. + public async Task CreateThreadFromMessageAsync(ulong channelId, ulong messageId, string name, DiscordAutoArchiveDuration archiveAfter, string reason = null) + => await this.ApiClient.CreateThreadFromMessageAsync(channelId, messageId, name, archiveAfter, reason); + + /// + /// Creates a thread. + /// + /// The ID of the channel. + /// The name of the thread. + /// The auto archive duration. + /// The type of the thread. + /// Reason for audit logs. + /// + public async Task CreateThreadAsync(ulong channelId, string name, DiscordAutoArchiveDuration archiveAfter, DiscordChannelType threadType, string reason = null) + => await this.ApiClient.CreateThreadAsync(channelId, name, archiveAfter, threadType, reason); + + /// + /// Joins a thread. + /// + /// The ID of the thread. + public async Task JoinThreadAsync(ulong threadId) + => await this.ApiClient.JoinThreadAsync(threadId); + + /// + /// Leaves a thread. + /// + /// The ID of the thread. + public async Task LeaveThreadAsync(ulong threadId) + => await this.ApiClient.LeaveThreadAsync(threadId); + + /// + /// Adds a member to a thread. + /// + /// The ID of the thread. + /// The ID of the member. + public async Task AddThreadMemberAsync(ulong threadId, ulong userId) + => await this.ApiClient.AddThreadMemberAsync(threadId, userId); + + /// + /// Removes a member from a thread. + /// + /// The ID of the thread. + /// The ID of the member. + public async Task RemoveThreadMemberAsync(ulong threadId, ulong userId) + => await this.ApiClient.RemoveThreadMemberAsync(threadId, userId); + + /// + /// Lists the members of a thread. + /// + /// The ID of the thread. + public async Task> ListThreadMembersAsync(ulong threadId) + => await this.ApiClient.ListThreadMembersAsync(threadId); + + /// + /// Lists the active threads of a guild. + /// + /// The ID of the guild. + public async Task ListActiveThreadAsync(ulong guildId) + => await this.ApiClient.ListActiveThreadsAsync(guildId); + + /// + /// Gets the threads that are public and archived for a channel. + /// + /// The ID of the guild. + /// The ID of the channel. + /// Date to filter by. + /// Limit. + public async Task ListPublicArchivedThreadsAsync(ulong guildId, ulong channelId, DateTimeOffset? before = null, int limit = 0) + => await this.ApiClient.ListPublicArchivedThreadsAsync(guildId, channelId, before?.ToString("o"), limit); + + /// + /// Gets the threads that are public and archived for a channel. + /// + /// The ID of the guild. + /// The ID of the channel. + /// Date to filter by. + /// Limit. + public async Task ListPrivateArchivedThreadAsync(ulong guildId, ulong channelId, DateTimeOffset? before = null, int limit = 0) + => await this.ApiClient.ListPrivateArchivedThreadsAsync(guildId, channelId, limit, before?.ToString("o")); + + /// + /// Gets the private archived threads the user has joined for a channel. + /// + /// The ID of the guild. + /// The ID of the channel. + /// Date to filter by. + /// Limit. + public async Task ListJoinedPrivateArchivedThreadsAsync(ulong guildId, ulong channelId, DateTimeOffset? before = null, int limit = 0) + => await this.ApiClient.ListJoinedPrivateArchivedThreadsAsync(guildId, channelId, limit, (ulong?)before?.ToUnixTimeSeconds()); + + #endregion + + #region Emoji + + /// + /// Gets a guild's emojis. + /// + /// The ID of the guild. + public async Task> GetGuildEmojisAsync(ulong guildId) + => await this.ApiClient.GetGuildEmojisAsync(guildId); + + /// + /// Gets a guild emoji. + /// + /// The ID of the guild. + /// The ID of the emoji. + public async Task GetGuildEmojiAsync(ulong guildId, ulong emojiId) + => await this.ApiClient.GetGuildEmojiAsync(guildId, emojiId); + + /// + /// Creates an emoji in a guild. + /// + /// Name of the emoji. + /// The ID of the guild. + /// Image to use as the emoji. + /// Roles for which the emoji will be available. + /// Reason for audit logs. + public async Task CreateEmojiAsync(ulong guildId, string name, Stream image, IEnumerable roles = null, string reason = null) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentNullException(nameof(name)); + } + + name = name.Trim(); + if (name.Length is < 2 or > 50) + { + throw new ArgumentException("Emoji name needs to be between 2 and 50 characters long."); + } + + ArgumentNullException.ThrowIfNull(image); + string image64; + using InlineMediaTool imgtool = new(image); + image64 = imgtool.GetBase64(); + + return await this.ApiClient.CreateGuildEmojiAsync(guildId, name, image64, roles, reason); + } + + /// + /// Modifies a guild's emoji. + /// + /// The ID of the guild. + /// The ID of the emoji. + /// New name of the emoji. + /// Roles for which the emoji will be available. + /// Reason for audit logs. + public async Task ModifyGuildEmojiAsync(ulong guildId, ulong emojiId, string name, IEnumerable roles = null, string reason = null) + => await this.ApiClient.ModifyGuildEmojiAsync(guildId, emojiId, name, roles, reason); + + /// + /// Deletes a guild's emoji. + /// + /// The ID of the guild. + /// The ID of the emoji. + /// Reason for audit logs. + public async Task DeleteGuildEmojiAsync(ulong guildId, ulong emojiId, string reason = null) + => await this.ApiClient.DeleteGuildEmojiAsync(guildId, emojiId, reason); + + #endregion + + #region Misc + /// + /// Gets assets from an application + /// + /// Application to get assets from + /// + public async Task> GetApplicationAssetsAsync(DiscordApplication application) + => await this.ApiClient.GetApplicationAssetsAsync(application); + + /// + /// Gets a guild template by the code. + /// + /// The code of the template. + /// The guild template for the code.\ + public async Task GetTemplateAsync(string code) + => await this.ApiClient.GetTemplateAsync(code); + + protected virtual void Dispose(bool disposing) + { + if (!this.disposedValue) + { + if (disposing) + { + this.guilds = null; + this.ApiClient?.rest?.Dispose(); + } + + this.disposedValue = true; + } + } + + public override void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + #endregion +} diff --git a/DSharpPlus.Tests/Commands/.editorconfig b/DSharpPlus.Tests/Commands/.editorconfig index 833e59b99c..74d448ba24 100644 --- a/DSharpPlus.Tests/Commands/.editorconfig +++ b/DSharpPlus.Tests/Commands/.editorconfig @@ -1,5 +1,5 @@ -root = false - -[*.cs] -# IDE0060: Remove unused parameter +root = false + +[*.cs] +# IDE0060: Remove unused parameter dotnet_diagnostic.IDE0060.severity = none \ No newline at end of file diff --git a/DSharpPlus.Tests/Commands/Cases/Commands/TestMultiLevelSubCommands.cs b/DSharpPlus.Tests/Commands/Cases/Commands/TestMultiLevelSubCommands.cs index 5c4e42fdcf..8f6163533a 100644 --- a/DSharpPlus.Tests/Commands/Cases/Commands/TestMultiLevelSubCommands.cs +++ b/DSharpPlus.Tests/Commands/Cases/Commands/TestMultiLevelSubCommands.cs @@ -1,35 +1,35 @@ -using System.Threading.Tasks; -using DSharpPlus.Commands; -using DSharpPlus.Entities; - -namespace DSharpPlus.Tests.Commands.Cases.Commands; - -public class TestMultiLevelSubCommands -{ - [Command("info")] - public class InfoCommand - { - [Command("user")] - public class UserCommand - { - [Command("avatar")] - public static ValueTask AvatarAsync(CommandContext context, DiscordUser user) => default; - - [Command("roles")] - public static ValueTask RolesAsync(CommandContext context, DiscordUser user) => default; - - [Command("permissions")] - public static ValueTask PermissionsAsync(CommandContext context, DiscordUser user, DiscordChannel? channel = null) => default; - } - - [Command("channel")] - public class ChannelCommand - { - [Command("created")] - public static ValueTask PermissionsAsync(CommandContext context, DiscordChannel channel) => default; - - [Command("members")] - public static ValueTask MembersAsync(CommandContext context, DiscordChannel channel) => default; - } - } -} +using System.Threading.Tasks; +using DSharpPlus.Commands; +using DSharpPlus.Entities; + +namespace DSharpPlus.Tests.Commands.Cases.Commands; + +public class TestMultiLevelSubCommands +{ + [Command("info")] + public class InfoCommand + { + [Command("user")] + public class UserCommand + { + [Command("avatar")] + public static ValueTask AvatarAsync(CommandContext context, DiscordUser user) => default; + + [Command("roles")] + public static ValueTask RolesAsync(CommandContext context, DiscordUser user) => default; + + [Command("permissions")] + public static ValueTask PermissionsAsync(CommandContext context, DiscordUser user, DiscordChannel? channel = null) => default; + } + + [Command("channel")] + public class ChannelCommand + { + [Command("created")] + public static ValueTask PermissionsAsync(CommandContext context, DiscordChannel channel) => default; + + [Command("members")] + public static ValueTask MembersAsync(CommandContext context, DiscordChannel channel) => default; + } + } +} diff --git a/DSharpPlus.Tests/Commands/Cases/Commands/TestSingleLevelSubCommands.cs b/DSharpPlus.Tests/Commands/Cases/Commands/TestSingleLevelSubCommands.cs index e402be9a7c..6e24d3321b 100644 --- a/DSharpPlus.Tests/Commands/Cases/Commands/TestSingleLevelSubCommands.cs +++ b/DSharpPlus.Tests/Commands/Cases/Commands/TestSingleLevelSubCommands.cs @@ -1,22 +1,22 @@ -using System.Threading.Tasks; -using DSharpPlus.Commands; -using DSharpPlus.Commands.ArgumentModifiers; -using DSharpPlus.Commands.Processors.TextCommands; - -namespace DSharpPlus.Tests.Commands.Cases.Commands; - -public class TestSingleLevelSubCommands -{ - [Command("tag")] - public class TagCommand - { - [Command("add")] - public static ValueTask AddAsync(TextCommandContext context, string name, [RemainingText] string content) => default; - - [Command("get")] - public static ValueTask GetAsync(CommandContext context, string name) => default; - } - - [Command("empty")] - public class EmptyCommand { } -} +using System.Threading.Tasks; +using DSharpPlus.Commands; +using DSharpPlus.Commands.ArgumentModifiers; +using DSharpPlus.Commands.Processors.TextCommands; + +namespace DSharpPlus.Tests.Commands.Cases.Commands; + +public class TestSingleLevelSubCommands +{ + [Command("tag")] + public class TagCommand + { + [Command("add")] + public static ValueTask AddAsync(TextCommandContext context, string name, [RemainingText] string content) => default; + + [Command("get")] + public static ValueTask GetAsync(CommandContext context, string name) => default; + } + + [Command("empty")] + public class EmptyCommand { } +} diff --git a/DSharpPlus.Tests/Commands/Cases/Commands/TestTopLevelCommands.cs b/DSharpPlus.Tests/Commands/Cases/Commands/TestTopLevelCommands.cs index e2d9ff89b0..270d9533a7 100644 --- a/DSharpPlus.Tests/Commands/Cases/Commands/TestTopLevelCommands.cs +++ b/DSharpPlus.Tests/Commands/Cases/Commands/TestTopLevelCommands.cs @@ -1,21 +1,21 @@ -using System.Threading.Tasks; -using DSharpPlus.Commands; -using DSharpPlus.Commands.ArgumentModifiers; -using DSharpPlus.Entities; - -namespace DSharpPlus.Tests.Commands.Cases.Commands; - -public class TestTopLevelCommands -{ - [Command("oops")] - public static ValueTask OopsAsync() => default; - - [Command("ping")] - public static ValueTask PingAsync(CommandContext context) => default; - - [Command("echo")] - public static ValueTask EchoAsync(CommandContext context, [RemainingText] string message) => default; - - [Command("user_info")] - public static ValueTask UserInfoAsync(CommandContext context, DiscordUser? user = null) => default; -} +using System.Threading.Tasks; +using DSharpPlus.Commands; +using DSharpPlus.Commands.ArgumentModifiers; +using DSharpPlus.Entities; + +namespace DSharpPlus.Tests.Commands.Cases.Commands; + +public class TestTopLevelCommands +{ + [Command("oops")] + public static ValueTask OopsAsync() => default; + + [Command("ping")] + public static ValueTask PingAsync(CommandContext context) => default; + + [Command("echo")] + public static ValueTask EchoAsync(CommandContext context, [RemainingText] string message) => default; + + [Command("user_info")] + public static ValueTask UserInfoAsync(CommandContext context, DiscordUser? user = null) => default; +} diff --git a/DSharpPlus.Tests/Commands/Cases/UserInput.cs b/DSharpPlus.Tests/Commands/Cases/UserInput.cs index f332a5cb3f..2f555f1d78 100644 --- a/DSharpPlus.Tests/Commands/Cases/UserInput.cs +++ b/DSharpPlus.Tests/Commands/Cases/UserInput.cs @@ -1,50 +1,50 @@ -using System.Collections.Generic; -using NUnit.Framework; - -namespace DSharpPlus.Tests.Commands.Cases; - -public static class UserInput -{ - public static readonly List ExpectedNormal = - [ - new TestCaseData("Hello", new[] { "Hello" }), - new TestCaseData("Hello World", new[] { "Hello", "World" }), - new TestCaseData("Hello World Hello World", new[] { "Hello", "World", "Hello", "World" }), - new TestCaseData("Jeff Bezos has 121 BILLION dollars. The population of earth is 7 billion people. He could give every person 1 BILLION dollars and end poverty, and he would still have 114 billion dollars left over but he would do it. This is what capitalist greed looks like!", new[] { "Jeff", "Bezos", "has", "121", "BILLION", "dollars.", "The", "population", "of", "earth", "is", "7", "billion", "people.", "He", "could", "give", "every", "person", "1", "BILLION", "dollars", "and", "end", "poverty,", "and", "he", "would", "still", "have", "114", "billion", "dollars", "left", "over", "but", "he", "would", "do", "it.", "This", "is", "what", "capitalist", "greed", "looks", "like!" }) - ]; - - public static readonly List ExpectedQuoted = - [ - new TestCaseData("'Hello'", new[] { "Hello" }), - new TestCaseData("'Hello World'", new[] { "Hello World" }), - new TestCaseData("'Hello World' 'Hello World'", new[] { "Hello World", "Hello World" }), - new TestCaseData("'Hello World' Hello World", new[] { "Hello World", "Hello", "World" }), - new TestCaseData("\"Hello 'world'\"", new[] { "Hello 'world'" }), - new TestCaseData("\"'Hello world'\"", new[] { "'Hello world'" }), - new TestCaseData("I'm so sick of all these people who think they're gamers. No, you're not. Most of you are not even close to being gamers. I see these people saying \"I put well over 100hrs in this game and it's great!\" That's nothing, most of us can easily put 300+ in all of our games. I see people who only have the Nintendo switch and claim to be gamers. Come talk to me when you pick up a PS4 controller then we'll be friends.", new[] { "I'm", "so", "sick", "of", "all", "these", "people", "who", "think", "they're", "gamers.", "No,", "you're", "not.", "Most", "of", "you", "are", "not", "even", "close", "to", "being", "gamers.", "I", "see", "these", "people", "saying", "I put well over 100hrs in this game and it's great!", "That's", "nothing,", "most", "of", "us", "can", "easily", "put", "300+", "in", "all", "of", "our", "games.", "I", "see", "people", "who", "only", "have", "the", "Nintendo", "switch", "and", "claim", "to", "be", "gamers.", "Come", "talk", "to", "me", "when", "you", "pick", "up", "a", "PS4", "controller", "then", "we'll", "be", "friends." }) - ]; - - public static readonly List ExpectedInlineCode = - [ - new TestCaseData("`Hello`", new[] { "`Hello`" }), - new TestCaseData("`Hello World`", new[] { "`Hello World`" }), - new TestCaseData("`Hello World` `Hello World`", new[] { "`Hello World`", "`Hello World`" }), - new TestCaseData("`Hello World` Hello World", new[] { "`Hello World`", "Hello", "World" }), - new TestCaseData("`ɴᴏᴡ ᴘʟᴀʏɪɴɢ: Who asked (Feat: No one) ───────────⚪────── ◄◄⠀▐▐ ⠀►► 5:12/ 7:𝟻𝟼 ───○ 🔊⠀ ᴴᴰ ⚙️`", new[] { "`ɴᴏᴡ ᴘʟᴀʏɪɴɢ: Who asked (Feat: No one) ───────────⚪────── ◄◄⠀▐▐ ⠀►► 5:12/ 7:𝟻𝟼 ───○ 🔊⠀ ᴴᴰ ⚙️`" }) - ]; - - public static readonly List ExpectedCodeBlock = - [ - new TestCaseData("```Hello```", new[] { "```Hello```" }), - new TestCaseData("```Hello World```", new[] { "```Hello World```" }), - new TestCaseData("```\nHello world\n```", new[] { "```\nHello world\n```" }), - new TestCaseData("```Hello``````Hello World```", new[] { "```Hello```", "```Hello World```" }), - ]; - - public static readonly List ExpectedEscaped = - [ - new TestCaseData("Hello\\ World", new[] { "Hello World" }), - new TestCaseData("'Hello\\' World'", new[] { "Hello' World" }), - new TestCaseData("\\'Hello 'World'", new[] { "'Hello", "World" }), - ]; -} +using System.Collections.Generic; +using NUnit.Framework; + +namespace DSharpPlus.Tests.Commands.Cases; + +public static class UserInput +{ + public static readonly List ExpectedNormal = + [ + new TestCaseData("Hello", new[] { "Hello" }), + new TestCaseData("Hello World", new[] { "Hello", "World" }), + new TestCaseData("Hello World Hello World", new[] { "Hello", "World", "Hello", "World" }), + new TestCaseData("Jeff Bezos has 121 BILLION dollars. The population of earth is 7 billion people. He could give every person 1 BILLION dollars and end poverty, and he would still have 114 billion dollars left over but he would do it. This is what capitalist greed looks like!", new[] { "Jeff", "Bezos", "has", "121", "BILLION", "dollars.", "The", "population", "of", "earth", "is", "7", "billion", "people.", "He", "could", "give", "every", "person", "1", "BILLION", "dollars", "and", "end", "poverty,", "and", "he", "would", "still", "have", "114", "billion", "dollars", "left", "over", "but", "he", "would", "do", "it.", "This", "is", "what", "capitalist", "greed", "looks", "like!" }) + ]; + + public static readonly List ExpectedQuoted = + [ + new TestCaseData("'Hello'", new[] { "Hello" }), + new TestCaseData("'Hello World'", new[] { "Hello World" }), + new TestCaseData("'Hello World' 'Hello World'", new[] { "Hello World", "Hello World" }), + new TestCaseData("'Hello World' Hello World", new[] { "Hello World", "Hello", "World" }), + new TestCaseData("\"Hello 'world'\"", new[] { "Hello 'world'" }), + new TestCaseData("\"'Hello world'\"", new[] { "'Hello world'" }), + new TestCaseData("I'm so sick of all these people who think they're gamers. No, you're not. Most of you are not even close to being gamers. I see these people saying \"I put well over 100hrs in this game and it's great!\" That's nothing, most of us can easily put 300+ in all of our games. I see people who only have the Nintendo switch and claim to be gamers. Come talk to me when you pick up a PS4 controller then we'll be friends.", new[] { "I'm", "so", "sick", "of", "all", "these", "people", "who", "think", "they're", "gamers.", "No,", "you're", "not.", "Most", "of", "you", "are", "not", "even", "close", "to", "being", "gamers.", "I", "see", "these", "people", "saying", "I put well over 100hrs in this game and it's great!", "That's", "nothing,", "most", "of", "us", "can", "easily", "put", "300+", "in", "all", "of", "our", "games.", "I", "see", "people", "who", "only", "have", "the", "Nintendo", "switch", "and", "claim", "to", "be", "gamers.", "Come", "talk", "to", "me", "when", "you", "pick", "up", "a", "PS4", "controller", "then", "we'll", "be", "friends." }) + ]; + + public static readonly List ExpectedInlineCode = + [ + new TestCaseData("`Hello`", new[] { "`Hello`" }), + new TestCaseData("`Hello World`", new[] { "`Hello World`" }), + new TestCaseData("`Hello World` `Hello World`", new[] { "`Hello World`", "`Hello World`" }), + new TestCaseData("`Hello World` Hello World", new[] { "`Hello World`", "Hello", "World" }), + new TestCaseData("`ɴᴏᴡ ᴘʟᴀʏɪɴɢ: Who asked (Feat: No one) ───────────⚪────── ◄◄⠀▐▐ ⠀►► 5:12/ 7:𝟻𝟼 ───○ 🔊⠀ ᴴᴰ ⚙️`", new[] { "`ɴᴏᴡ ᴘʟᴀʏɪɴɢ: Who asked (Feat: No one) ───────────⚪────── ◄◄⠀▐▐ ⠀►► 5:12/ 7:𝟻𝟼 ───○ 🔊⠀ ᴴᴰ ⚙️`" }) + ]; + + public static readonly List ExpectedCodeBlock = + [ + new TestCaseData("```Hello```", new[] { "```Hello```" }), + new TestCaseData("```Hello World```", new[] { "```Hello World```" }), + new TestCaseData("```\nHello world\n```", new[] { "```\nHello world\n```" }), + new TestCaseData("```Hello``````Hello World```", new[] { "```Hello```", "```Hello World```" }), + ]; + + public static readonly List ExpectedEscaped = + [ + new TestCaseData("Hello\\ World", new[] { "Hello World" }), + new TestCaseData("'Hello\\' World'", new[] { "Hello' World" }), + new TestCaseData("\\'Hello 'World'", new[] { "'Hello", "World" }), + ]; +} diff --git a/DSharpPlus.Tests/Commands/CommandFiltering/Tests.cs b/DSharpPlus.Tests/Commands/CommandFiltering/Tests.cs index 1031834f67..193b6d2bad 100644 --- a/DSharpPlus.Tests/Commands/CommandFiltering/Tests.cs +++ b/DSharpPlus.Tests/Commands/CommandFiltering/Tests.cs @@ -1,197 +1,197 @@ -using System.Collections.Generic; -using System.Linq; -using DSharpPlus.Commands; -using DSharpPlus.Commands.Processors.MessageCommands; -using DSharpPlus.Commands.Processors.SlashCommands; -using DSharpPlus.Commands.Processors.TextCommands; -using DSharpPlus.Commands.Processors.UserCommands; -using DSharpPlus.Commands.Trees; -using Microsoft.Extensions.DependencyInjection; -using NUnit.Framework; - -namespace DSharpPlus.Tests.Commands.CommandFiltering; - -public class Tests -{ - private static readonly SlashCommandProcessor slashCommandProcessor = - new(new() { RegisterCommands = false }); - - private static CommandsExtension extension = null!; - private static readonly TextCommandProcessor textCommandProcessor = new(); - private static readonly UserCommandProcessor userCommandProcessor = new(); - private static readonly MessageCommandProcessor messageCommandProcessor = new(); - - [OneTimeSetUp] - public static void CreateExtension() - { - DiscordClientBuilder builder = DiscordClientBuilder.CreateDefault( - "faketoken", - DiscordIntents.None - ); - - builder.UseCommands( - async (_, extension) => - { - extension.AddProcessor(textCommandProcessor); - extension.AddProcessor(slashCommandProcessor); - extension.AddProcessor(userCommandProcessor); - extension.AddProcessor(messageCommandProcessor); - - extension.AddCommands( - [ - typeof(TestMultiLevelSubCommandsFiltered.RootCommand), - typeof(TestMultiLevelSubCommandsFiltered.ContextMenues), - typeof(TestMultiLevelSubCommandsFiltered.ContextMenuesInGroup), - ] - ); - await extension.BuildCommandsAsync(); - await userCommandProcessor.ConfigureAsync(extension); - await messageCommandProcessor.ConfigureAsync(extension); - }, - new CommandsConfiguration() { RegisterDefaultCommandProcessors = false } - ); - - DiscordClient client = builder.Build(); - - extension = client.ServiceProvider.GetRequiredService(); - } - - [Test] - public static void TestSubGroupTextProcessor() - { - IReadOnlyList commands = extension.GetCommandsForProcessor(textCommandProcessor); - - Command? root = commands.FirstOrDefault(x => x.Name == "root"); - Assert.That(root, Is.Not.Null); - - Assert.That(root.Subcommands, Has.Count.EqualTo(2)); - Assert.That(root.Subcommands[0].Name, Is.EqualTo("subgroup")); - Assert.That(root.Subcommands[1].Name, Is.EqualTo("subgroup-text-only")); - - Command generalGroup = root.Subcommands[0]; - Assert.That(generalGroup.Subcommands, Has.Count.EqualTo(2)); - Assert.That(generalGroup.Subcommands[0].Name, Is.EqualTo("command-text-only-attribute")); - Assert.That(generalGroup.Subcommands[1].Name, Is.EqualTo("command-text-only-parameter")); - - Command textGroup = root.Subcommands[1]; - Assert.That(textGroup.Subcommands, Has.Count.EqualTo(2)); - Assert.That(textGroup.Subcommands[0].Name, Is.EqualTo("text-only-group")); - Assert.That(textGroup.Subcommands[1].Name, Is.EqualTo("text-only-group2")); - } - - [Test] - public static void TestSubGroupSlashProcessor() - { - IReadOnlyList commands = extension.GetCommandsForProcessor(slashCommandProcessor); - - //toplevel command "root" - Command? root = commands.FirstOrDefault(x => x.Name == "root"); - Assert.That(root, Is.Not.Null); - - Assert.That(root.Subcommands, Has.Count.EqualTo(2)); - Assert.That(root.Subcommands[0].Name, Is.EqualTo("subgroup")); - Assert.That(root.Subcommands[1].Name, Is.EqualTo("subgroup-slash-only")); - - Command generalGroup = root.Subcommands[0]; - Command slashGroup = root.Subcommands[1]; - - Assert.That(generalGroup.Subcommands, Has.Count.EqualTo(2)); - Assert.That(generalGroup.Subcommands[0].Name, Is.EqualTo("command-slash-only-attribute")); - Assert.That(generalGroup.Subcommands[1].Name, Is.EqualTo("command-slash-only-parameter")); - - Assert.That(slashGroup.Subcommands, Has.Count.EqualTo(2)); - Assert.That(slashGroup.Subcommands[0].Name, Is.EqualTo("slash-only-group")); - Assert.That(slashGroup.Subcommands[1].Name, Is.EqualTo("slash-only-group2")); - } - - [Test] - public static void TestUserContextMenu() - { - IReadOnlyList userContextCommands = userCommandProcessor.Commands; - - Command? contextOnlyCommand = userContextCommands.FirstOrDefault(x => - x.Name == "UserContextOnly" - ); - Assert.That(contextOnlyCommand, Is.Not.Null); - - Command? bothCommand = userContextCommands.FirstOrDefault(x => - x.Name == "SlashUserContext" - ); - Assert.That(bothCommand, Is.Not.Null); - - IReadOnlyList slashCommands = extension.GetCommandsForProcessor( - slashCommandProcessor - ); - Assert.That(slashCommands.FirstOrDefault(x => x.Name == "SlashUserContext"), Is.Not.Null); - } - - [Test] - public static void TestMessageContextMenu() - { - IReadOnlyList messageContextCommands = messageCommandProcessor.Commands; - - Command? contextOnlyCommand = messageContextCommands.FirstOrDefault(x => - x.Name == "MessageContextOnly" - ); - Assert.That(contextOnlyCommand, Is.Not.Null); - - Command? bothCommand = messageContextCommands.FirstOrDefault(x => - x.Name == "SlashMessageContext" - ); - Assert.That(bothCommand, Is.Not.Null); - - IReadOnlyList slashCommands = extension.GetCommandsForProcessor( - slashCommandProcessor - ); - Assert.That( - slashCommands.FirstOrDefault(x => x.Name == "SlashMessageContext"), - Is.Not.Null - ); - } - - [Test] - public static void TestUserContextMenuInGroup() - { - IReadOnlyList userContextCommands = userCommandProcessor.Commands; - - Command? contextOnlyCommand = userContextCommands.FirstOrDefault(x => - x.FullName == "group UserContextOnly" - ); - Assert.That(contextOnlyCommand, Is.Not.Null); - - Command? bothCommand = userContextCommands.FirstOrDefault(x => - x.FullName == "group SlashUserContext" - ); - Assert.That(bothCommand, Is.Not.Null); - - IReadOnlyList slashCommands = extension.GetCommandsForProcessor( - slashCommandProcessor - ); - Command? group = slashCommands.FirstOrDefault(x => x.Name == "group"); - Assert.That(group, Is.Not.Null); - Assert.That(group.Subcommands.Any(x => x.Name == "SlashUserContext")); - } - - [Test] - public static void TestMessageContextMenuInGroup() - { - IReadOnlyList messageContextCommands = messageCommandProcessor.Commands; - - Command? contextOnlyCommand = messageContextCommands.FirstOrDefault(x => - x.FullName == "group MessageContextOnly" - ); - Assert.That(contextOnlyCommand, Is.Not.Null); - - Command? bothCommand = messageContextCommands.FirstOrDefault(x => - x.FullName == "group SlashMessageContext" - ); - Assert.That(bothCommand, Is.Not.Null); - - IReadOnlyList slashCommands = extension.GetCommandsForProcessor( - slashCommandProcessor - ); - Command? group = slashCommands.FirstOrDefault(x => x.Name == "group"); - Assert.That(group, Is.Not.Null); - Assert.That(group.Subcommands.Any(x => x.Name == "SlashMessageContext")); - } -} +using System.Collections.Generic; +using System.Linq; +using DSharpPlus.Commands; +using DSharpPlus.Commands.Processors.MessageCommands; +using DSharpPlus.Commands.Processors.SlashCommands; +using DSharpPlus.Commands.Processors.TextCommands; +using DSharpPlus.Commands.Processors.UserCommands; +using DSharpPlus.Commands.Trees; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; + +namespace DSharpPlus.Tests.Commands.CommandFiltering; + +public class Tests +{ + private static readonly SlashCommandProcessor slashCommandProcessor = + new(new() { RegisterCommands = false }); + + private static CommandsExtension extension = null!; + private static readonly TextCommandProcessor textCommandProcessor = new(); + private static readonly UserCommandProcessor userCommandProcessor = new(); + private static readonly MessageCommandProcessor messageCommandProcessor = new(); + + [OneTimeSetUp] + public static void CreateExtension() + { + DiscordClientBuilder builder = DiscordClientBuilder.CreateDefault( + "faketoken", + DiscordIntents.None + ); + + builder.UseCommands( + async (_, extension) => + { + extension.AddProcessor(textCommandProcessor); + extension.AddProcessor(slashCommandProcessor); + extension.AddProcessor(userCommandProcessor); + extension.AddProcessor(messageCommandProcessor); + + extension.AddCommands( + [ + typeof(TestMultiLevelSubCommandsFiltered.RootCommand), + typeof(TestMultiLevelSubCommandsFiltered.ContextMenues), + typeof(TestMultiLevelSubCommandsFiltered.ContextMenuesInGroup), + ] + ); + await extension.BuildCommandsAsync(); + await userCommandProcessor.ConfigureAsync(extension); + await messageCommandProcessor.ConfigureAsync(extension); + }, + new CommandsConfiguration() { RegisterDefaultCommandProcessors = false } + ); + + DiscordClient client = builder.Build(); + + extension = client.ServiceProvider.GetRequiredService(); + } + + [Test] + public static void TestSubGroupTextProcessor() + { + IReadOnlyList commands = extension.GetCommandsForProcessor(textCommandProcessor); + + Command? root = commands.FirstOrDefault(x => x.Name == "root"); + Assert.That(root, Is.Not.Null); + + Assert.That(root.Subcommands, Has.Count.EqualTo(2)); + Assert.That(root.Subcommands[0].Name, Is.EqualTo("subgroup")); + Assert.That(root.Subcommands[1].Name, Is.EqualTo("subgroup-text-only")); + + Command generalGroup = root.Subcommands[0]; + Assert.That(generalGroup.Subcommands, Has.Count.EqualTo(2)); + Assert.That(generalGroup.Subcommands[0].Name, Is.EqualTo("command-text-only-attribute")); + Assert.That(generalGroup.Subcommands[1].Name, Is.EqualTo("command-text-only-parameter")); + + Command textGroup = root.Subcommands[1]; + Assert.That(textGroup.Subcommands, Has.Count.EqualTo(2)); + Assert.That(textGroup.Subcommands[0].Name, Is.EqualTo("text-only-group")); + Assert.That(textGroup.Subcommands[1].Name, Is.EqualTo("text-only-group2")); + } + + [Test] + public static void TestSubGroupSlashProcessor() + { + IReadOnlyList commands = extension.GetCommandsForProcessor(slashCommandProcessor); + + //toplevel command "root" + Command? root = commands.FirstOrDefault(x => x.Name == "root"); + Assert.That(root, Is.Not.Null); + + Assert.That(root.Subcommands, Has.Count.EqualTo(2)); + Assert.That(root.Subcommands[0].Name, Is.EqualTo("subgroup")); + Assert.That(root.Subcommands[1].Name, Is.EqualTo("subgroup-slash-only")); + + Command generalGroup = root.Subcommands[0]; + Command slashGroup = root.Subcommands[1]; + + Assert.That(generalGroup.Subcommands, Has.Count.EqualTo(2)); + Assert.That(generalGroup.Subcommands[0].Name, Is.EqualTo("command-slash-only-attribute")); + Assert.That(generalGroup.Subcommands[1].Name, Is.EqualTo("command-slash-only-parameter")); + + Assert.That(slashGroup.Subcommands, Has.Count.EqualTo(2)); + Assert.That(slashGroup.Subcommands[0].Name, Is.EqualTo("slash-only-group")); + Assert.That(slashGroup.Subcommands[1].Name, Is.EqualTo("slash-only-group2")); + } + + [Test] + public static void TestUserContextMenu() + { + IReadOnlyList userContextCommands = userCommandProcessor.Commands; + + Command? contextOnlyCommand = userContextCommands.FirstOrDefault(x => + x.Name == "UserContextOnly" + ); + Assert.That(contextOnlyCommand, Is.Not.Null); + + Command? bothCommand = userContextCommands.FirstOrDefault(x => + x.Name == "SlashUserContext" + ); + Assert.That(bothCommand, Is.Not.Null); + + IReadOnlyList slashCommands = extension.GetCommandsForProcessor( + slashCommandProcessor + ); + Assert.That(slashCommands.FirstOrDefault(x => x.Name == "SlashUserContext"), Is.Not.Null); + } + + [Test] + public static void TestMessageContextMenu() + { + IReadOnlyList messageContextCommands = messageCommandProcessor.Commands; + + Command? contextOnlyCommand = messageContextCommands.FirstOrDefault(x => + x.Name == "MessageContextOnly" + ); + Assert.That(contextOnlyCommand, Is.Not.Null); + + Command? bothCommand = messageContextCommands.FirstOrDefault(x => + x.Name == "SlashMessageContext" + ); + Assert.That(bothCommand, Is.Not.Null); + + IReadOnlyList slashCommands = extension.GetCommandsForProcessor( + slashCommandProcessor + ); + Assert.That( + slashCommands.FirstOrDefault(x => x.Name == "SlashMessageContext"), + Is.Not.Null + ); + } + + [Test] + public static void TestUserContextMenuInGroup() + { + IReadOnlyList userContextCommands = userCommandProcessor.Commands; + + Command? contextOnlyCommand = userContextCommands.FirstOrDefault(x => + x.FullName == "group UserContextOnly" + ); + Assert.That(contextOnlyCommand, Is.Not.Null); + + Command? bothCommand = userContextCommands.FirstOrDefault(x => + x.FullName == "group SlashUserContext" + ); + Assert.That(bothCommand, Is.Not.Null); + + IReadOnlyList slashCommands = extension.GetCommandsForProcessor( + slashCommandProcessor + ); + Command? group = slashCommands.FirstOrDefault(x => x.Name == "group"); + Assert.That(group, Is.Not.Null); + Assert.That(group.Subcommands.Any(x => x.Name == "SlashUserContext")); + } + + [Test] + public static void TestMessageContextMenuInGroup() + { + IReadOnlyList messageContextCommands = messageCommandProcessor.Commands; + + Command? contextOnlyCommand = messageContextCommands.FirstOrDefault(x => + x.FullName == "group MessageContextOnly" + ); + Assert.That(contextOnlyCommand, Is.Not.Null); + + Command? bothCommand = messageContextCommands.FirstOrDefault(x => + x.FullName == "group SlashMessageContext" + ); + Assert.That(bothCommand, Is.Not.Null); + + IReadOnlyList slashCommands = extension.GetCommandsForProcessor( + slashCommandProcessor + ); + Command? group = slashCommands.FirstOrDefault(x => x.Name == "group"); + Assert.That(group, Is.Not.Null); + Assert.That(group.Subcommands.Any(x => x.Name == "SlashMessageContext")); + } +} diff --git a/DSharpPlus.Tests/Commands/Processors/TextCommands/Parsing/DefaultTextArgumentSplicerTests.cs b/DSharpPlus.Tests/Commands/Processors/TextCommands/Parsing/DefaultTextArgumentSplicerTests.cs index ba957734f8..98d7ba4695 100644 --- a/DSharpPlus.Tests/Commands/Processors/TextCommands/Parsing/DefaultTextArgumentSplicerTests.cs +++ b/DSharpPlus.Tests/Commands/Processors/TextCommands/Parsing/DefaultTextArgumentSplicerTests.cs @@ -1,131 +1,131 @@ -using System.Collections.Generic; -using DSharpPlus.Commands; -using DSharpPlus.Commands.Processors.TextCommands; -using DSharpPlus.Commands.Processors.TextCommands.Parsing; -using DSharpPlus.Tests.Commands.Cases; -using Microsoft.Extensions.DependencyInjection; -using NUnit.Framework; - -namespace DSharpPlus.Tests.Commands.Processors.TextCommands.Parsing; - -public sealed class DefaultTextArgumentSplicerTests -{ - private static CommandsExtension extension = null!; - - [OneTimeSetUp] - public static void CreateExtension() - { - DiscordClientBuilder builder = DiscordClientBuilder.CreateDefault( - "faketoken", - DiscordIntents.None - ); - - builder.UseCommands( - (_, extension) => extension.AddProcessor(new TextCommandProcessor()), - new() { RegisterDefaultCommandProcessors = false } - ); - - DiscordClient client = builder.Build(); - extension = client.ServiceProvider.GetRequiredService(); - } - - [TestCaseSource(typeof(UserInput), nameof(UserInput.ExpectedNormal), null)] - public static void ParseNormalArguments(string input, string[] expectedArguments) - { - List arguments = []; - int position = 0; - while (true) - { - string? argument = DefaultTextArgumentSplicer.Splice(extension, input, ref position); - if (argument is null) - { - break; - } - - arguments.Add(argument); - } - - Assert.That(arguments, Has.Count.EqualTo(expectedArguments.Length)); - Assert.That(arguments, Is.EqualTo(expectedArguments)); - } - - [TestCaseSource(typeof(UserInput), nameof(UserInput.ExpectedQuoted), null)] - public static void ParseQuotedArguments(string input, string[] expectedArguments) - { - List arguments = []; - int position = 0; - while (true) - { - string? argument = DefaultTextArgumentSplicer.Splice(extension, input, ref position); - if (argument is null) - { - break; - } - - arguments.Add(argument); - } - - Assert.That(arguments, Has.Count.EqualTo(expectedArguments.Length)); - Assert.That(arguments, Is.EqualTo(expectedArguments)); - } - - [TestCaseSource(typeof(UserInput), nameof(UserInput.ExpectedInlineCode), null)] - public static void ParseInlineCodeArguments(string input, string[] expectedArguments) - { - List arguments = []; - int position = 0; - while (true) - { - string? argument = DefaultTextArgumentSplicer.Splice(extension, input, ref position); - if (argument is null) - { - break; - } - - arguments.Add(argument); - } - - Assert.That(arguments, Has.Count.EqualTo(expectedArguments.Length)); - Assert.That(arguments, Is.EqualTo(expectedArguments)); - } - - [TestCaseSource(typeof(UserInput), nameof(UserInput.ExpectedCodeBlock), null)] - public static void ParseCodeBlockArguments(string input, string[] expectedArguments) - { - List arguments = []; - int position = 0; - while (true) - { - string? argument = DefaultTextArgumentSplicer.Splice(extension, input, ref position); - if (argument is null) - { - break; - } - - arguments.Add(argument); - } - - Assert.That(arguments, Has.Count.EqualTo(expectedArguments.Length)); - Assert.That(arguments, Is.EqualTo(expectedArguments)); - } - - [TestCaseSource(typeof(UserInput), nameof(UserInput.ExpectedEscaped), null)] - public static void ParseEscapedArguments(string input, string[] expectedArguments) - { - List arguments = []; - int position = 0; - while (true) - { - string? argument = DefaultTextArgumentSplicer.Splice(extension, input, ref position); - if (argument is null) - { - break; - } - - arguments.Add(argument); - } - - Assert.That(arguments, Has.Count.EqualTo(expectedArguments.Length)); - Assert.That(arguments, Is.EqualTo(expectedArguments)); - } -} +using System.Collections.Generic; +using DSharpPlus.Commands; +using DSharpPlus.Commands.Processors.TextCommands; +using DSharpPlus.Commands.Processors.TextCommands.Parsing; +using DSharpPlus.Tests.Commands.Cases; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; + +namespace DSharpPlus.Tests.Commands.Processors.TextCommands.Parsing; + +public sealed class DefaultTextArgumentSplicerTests +{ + private static CommandsExtension extension = null!; + + [OneTimeSetUp] + public static void CreateExtension() + { + DiscordClientBuilder builder = DiscordClientBuilder.CreateDefault( + "faketoken", + DiscordIntents.None + ); + + builder.UseCommands( + (_, extension) => extension.AddProcessor(new TextCommandProcessor()), + new() { RegisterDefaultCommandProcessors = false } + ); + + DiscordClient client = builder.Build(); + extension = client.ServiceProvider.GetRequiredService(); + } + + [TestCaseSource(typeof(UserInput), nameof(UserInput.ExpectedNormal), null)] + public static void ParseNormalArguments(string input, string[] expectedArguments) + { + List arguments = []; + int position = 0; + while (true) + { + string? argument = DefaultTextArgumentSplicer.Splice(extension, input, ref position); + if (argument is null) + { + break; + } + + arguments.Add(argument); + } + + Assert.That(arguments, Has.Count.EqualTo(expectedArguments.Length)); + Assert.That(arguments, Is.EqualTo(expectedArguments)); + } + + [TestCaseSource(typeof(UserInput), nameof(UserInput.ExpectedQuoted), null)] + public static void ParseQuotedArguments(string input, string[] expectedArguments) + { + List arguments = []; + int position = 0; + while (true) + { + string? argument = DefaultTextArgumentSplicer.Splice(extension, input, ref position); + if (argument is null) + { + break; + } + + arguments.Add(argument); + } + + Assert.That(arguments, Has.Count.EqualTo(expectedArguments.Length)); + Assert.That(arguments, Is.EqualTo(expectedArguments)); + } + + [TestCaseSource(typeof(UserInput), nameof(UserInput.ExpectedInlineCode), null)] + public static void ParseInlineCodeArguments(string input, string[] expectedArguments) + { + List arguments = []; + int position = 0; + while (true) + { + string? argument = DefaultTextArgumentSplicer.Splice(extension, input, ref position); + if (argument is null) + { + break; + } + + arguments.Add(argument); + } + + Assert.That(arguments, Has.Count.EqualTo(expectedArguments.Length)); + Assert.That(arguments, Is.EqualTo(expectedArguments)); + } + + [TestCaseSource(typeof(UserInput), nameof(UserInput.ExpectedCodeBlock), null)] + public static void ParseCodeBlockArguments(string input, string[] expectedArguments) + { + List arguments = []; + int position = 0; + while (true) + { + string? argument = DefaultTextArgumentSplicer.Splice(extension, input, ref position); + if (argument is null) + { + break; + } + + arguments.Add(argument); + } + + Assert.That(arguments, Has.Count.EqualTo(expectedArguments.Length)); + Assert.That(arguments, Is.EqualTo(expectedArguments)); + } + + [TestCaseSource(typeof(UserInput), nameof(UserInput.ExpectedEscaped), null)] + public static void ParseEscapedArguments(string input, string[] expectedArguments) + { + List arguments = []; + int position = 0; + while (true) + { + string? argument = DefaultTextArgumentSplicer.Splice(extension, input, ref position); + if (argument is null) + { + break; + } + + arguments.Add(argument); + } + + Assert.That(arguments, Has.Count.EqualTo(expectedArguments.Length)); + Assert.That(arguments, Is.EqualTo(expectedArguments)); + } +} diff --git a/DSharpPlus.Tests/Commands/Trees/CommandBuilderTests.cs b/DSharpPlus.Tests/Commands/Trees/CommandBuilderTests.cs index 294f8b31e0..449c097f38 100644 --- a/DSharpPlus.Tests/Commands/Trees/CommandBuilderTests.cs +++ b/DSharpPlus.Tests/Commands/Trees/CommandBuilderTests.cs @@ -1,110 +1,110 @@ -using System; -using DSharpPlus.Commands.Trees; -using DSharpPlus.Entities; -using DSharpPlus.Tests.Commands.Cases.Commands; -using NUnit.Framework; - -namespace DSharpPlus.Tests.Commands.Trees; - -public class CommandBuilderTests -{ - [Test] - public void TopLevelEmptyCommand() => Assert.Throws(() => CommandBuilder.From()); - - [Test] - public void TopLevelCommandMissingContext() => Assert.Throws(() => CommandBuilder.From(TestTopLevelCommands.OopsAsync)); - - [Test] - public void TopLevelCommandNoParameters() - { - CommandBuilder commandBuilder = CommandBuilder.From(TestTopLevelCommands.PingAsync); - Command command = commandBuilder.Build(); - Assert.Multiple(() => - { - Assert.That(command.Name, Is.EqualTo("ping")); - Assert.That(command.Description, Is.Null); - Assert.That(command.Parent, Is.Null); - Assert.That(command.Target, Is.Null); - Assert.That(command.Method, Is.EqualTo(((Delegate)TestTopLevelCommands.PingAsync).Method)); - Assert.That(command.Attributes, Is.Not.Empty); - Assert.That(command.Subcommands, Is.Empty); - Assert.That(command.Parameters, Is.Empty); - }); - } - - [Test] - public void TopLevelCommandOneOptionalParameter() - { - CommandBuilder commandBuilder = CommandBuilder.From(TestTopLevelCommands.UserInfoAsync); - Command command = commandBuilder.Build(); - Assert.That(command.Parameters, Has.Count.EqualTo(1)); - Assert.Multiple(() => - { - Assert.That(command.Parameters[0].Name, Is.EqualTo("user")); - Assert.That(command.Parameters[0].Description, Is.EqualTo("No description provided.")); - Assert.That(command.Parameters[0].Type, Is.EqualTo(typeof(DiscordUser))); - Assert.That(command.Parameters[0].DefaultValue.HasValue, Is.True); - Assert.That(command.Parameters[0].DefaultValue.Value, Is.Null); - }); - } - - [Test] - public void SingleLevelSubCommands() - { - CommandBuilder commandBuilder = CommandBuilder.From(); - Assert.That(commandBuilder.Subcommands, Has.Count.EqualTo(2)); - - Command command = commandBuilder.Build(); - Assert.Multiple(() => - { - Assert.That(command.Parent, Is.Null); - Assert.That(command.Subcommands, Has.Count.EqualTo(2)); - }); - - // Will not execute if the subcommand count fails - Assert.Multiple(() => - { - Assert.That(command.Subcommands[0].Name, Is.EqualTo("add")); - Assert.That(command.Subcommands[0].Parameters, Has.Count.EqualTo(2)); - Assert.That(command.Subcommands[1].Name, Is.EqualTo("get")); - Assert.That(command.Subcommands[1].Parameters, Has.Count.EqualTo(1)); - }); - } - - [Test] - public void MultiLevelSubCommands() - { - CommandBuilder commandBuilder = CommandBuilder.From(); - Assert.That(commandBuilder.Subcommands, Has.Count.EqualTo(2)); - - Command command = commandBuilder.Build(); - Assert.Multiple(() => - { - Assert.That(command.Parent, Is.Null); - Assert.That(command.Subcommands, Has.Count.EqualTo(2)); - }); - - Assert.Multiple(() => - { - Assert.That(command.Subcommands[0].Name, Is.EqualTo("user")); - Assert.That(command.Subcommands[0].Parent, Is.EqualTo(command)); - Assert.That(command.Subcommands[1].Name, Is.EqualTo("channel")); - Assert.That(command.Subcommands[1].Parent, Is.EqualTo(command)); - }); - - Assert.That(command.Subcommands[0].Subcommands, Has.Count.EqualTo(3)); - Assert.Multiple(() => - { - Assert.That(command.Subcommands[0].Subcommands[0].Parameters, Has.Count.EqualTo(1)); - Assert.That(command.Subcommands[0].Subcommands[1].Parameters, Has.Count.EqualTo(1)); - Assert.That(command.Subcommands[0].Subcommands[2].Parameters, Has.Count.EqualTo(2)); - }); - - Assert.That(command.Subcommands[1].Subcommands, Has.Count.EqualTo(2)); - Assert.Multiple(() => - { - Assert.That(command.Subcommands[1].Subcommands[0].Parameters, Has.Count.EqualTo(1)); - Assert.That(command.Subcommands[1].Subcommands[1].Parameters, Has.Count.EqualTo(1)); - }); - } -} +using System; +using DSharpPlus.Commands.Trees; +using DSharpPlus.Entities; +using DSharpPlus.Tests.Commands.Cases.Commands; +using NUnit.Framework; + +namespace DSharpPlus.Tests.Commands.Trees; + +public class CommandBuilderTests +{ + [Test] + public void TopLevelEmptyCommand() => Assert.Throws(() => CommandBuilder.From()); + + [Test] + public void TopLevelCommandMissingContext() => Assert.Throws(() => CommandBuilder.From(TestTopLevelCommands.OopsAsync)); + + [Test] + public void TopLevelCommandNoParameters() + { + CommandBuilder commandBuilder = CommandBuilder.From(TestTopLevelCommands.PingAsync); + Command command = commandBuilder.Build(); + Assert.Multiple(() => + { + Assert.That(command.Name, Is.EqualTo("ping")); + Assert.That(command.Description, Is.Null); + Assert.That(command.Parent, Is.Null); + Assert.That(command.Target, Is.Null); + Assert.That(command.Method, Is.EqualTo(((Delegate)TestTopLevelCommands.PingAsync).Method)); + Assert.That(command.Attributes, Is.Not.Empty); + Assert.That(command.Subcommands, Is.Empty); + Assert.That(command.Parameters, Is.Empty); + }); + } + + [Test] + public void TopLevelCommandOneOptionalParameter() + { + CommandBuilder commandBuilder = CommandBuilder.From(TestTopLevelCommands.UserInfoAsync); + Command command = commandBuilder.Build(); + Assert.That(command.Parameters, Has.Count.EqualTo(1)); + Assert.Multiple(() => + { + Assert.That(command.Parameters[0].Name, Is.EqualTo("user")); + Assert.That(command.Parameters[0].Description, Is.EqualTo("No description provided.")); + Assert.That(command.Parameters[0].Type, Is.EqualTo(typeof(DiscordUser))); + Assert.That(command.Parameters[0].DefaultValue.HasValue, Is.True); + Assert.That(command.Parameters[0].DefaultValue.Value, Is.Null); + }); + } + + [Test] + public void SingleLevelSubCommands() + { + CommandBuilder commandBuilder = CommandBuilder.From(); + Assert.That(commandBuilder.Subcommands, Has.Count.EqualTo(2)); + + Command command = commandBuilder.Build(); + Assert.Multiple(() => + { + Assert.That(command.Parent, Is.Null); + Assert.That(command.Subcommands, Has.Count.EqualTo(2)); + }); + + // Will not execute if the subcommand count fails + Assert.Multiple(() => + { + Assert.That(command.Subcommands[0].Name, Is.EqualTo("add")); + Assert.That(command.Subcommands[0].Parameters, Has.Count.EqualTo(2)); + Assert.That(command.Subcommands[1].Name, Is.EqualTo("get")); + Assert.That(command.Subcommands[1].Parameters, Has.Count.EqualTo(1)); + }); + } + + [Test] + public void MultiLevelSubCommands() + { + CommandBuilder commandBuilder = CommandBuilder.From(); + Assert.That(commandBuilder.Subcommands, Has.Count.EqualTo(2)); + + Command command = commandBuilder.Build(); + Assert.Multiple(() => + { + Assert.That(command.Parent, Is.Null); + Assert.That(command.Subcommands, Has.Count.EqualTo(2)); + }); + + Assert.Multiple(() => + { + Assert.That(command.Subcommands[0].Name, Is.EqualTo("user")); + Assert.That(command.Subcommands[0].Parent, Is.EqualTo(command)); + Assert.That(command.Subcommands[1].Name, Is.EqualTo("channel")); + Assert.That(command.Subcommands[1].Parent, Is.EqualTo(command)); + }); + + Assert.That(command.Subcommands[0].Subcommands, Has.Count.EqualTo(3)); + Assert.Multiple(() => + { + Assert.That(command.Subcommands[0].Subcommands[0].Parameters, Has.Count.EqualTo(1)); + Assert.That(command.Subcommands[0].Subcommands[1].Parameters, Has.Count.EqualTo(1)); + Assert.That(command.Subcommands[0].Subcommands[2].Parameters, Has.Count.EqualTo(2)); + }); + + Assert.That(command.Subcommands[1].Subcommands, Has.Count.EqualTo(2)); + Assert.Multiple(() => + { + Assert.That(command.Subcommands[1].Subcommands[0].Parameters, Has.Count.EqualTo(1)); + Assert.That(command.Subcommands[1].Subcommands[1].Parameters, Has.Count.EqualTo(1)); + }); + } +} diff --git a/DSharpPlus.Tests/DSharpPlus.Tests.csproj b/DSharpPlus.Tests/DSharpPlus.Tests.csproj index 9b276ec1ee..227e78d8de 100644 --- a/DSharpPlus.Tests/DSharpPlus.Tests.csproj +++ b/DSharpPlus.Tests/DSharpPlus.Tests.csproj @@ -1,19 +1,19 @@ - - - net9.0 - enable - false - true - false - - - - - - - - - - - + + + net9.0 + enable + false + true + false + + + + + + + + + + + \ No newline at end of file diff --git a/DSharpPlus.VoiceNext/AudioFormat.cs b/DSharpPlus.VoiceNext/AudioFormat.cs index aa1d231303..9c107ae61b 100644 --- a/DSharpPlus.VoiceNext/AudioFormat.cs +++ b/DSharpPlus.VoiceNext/AudioFormat.cs @@ -1,126 +1,126 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; -using System.Runtime.CompilerServices; - -namespace DSharpPlus.VoiceNext; - -/// -/// Defines the format of PCM data consumed or produced by Opus. -/// -public readonly struct AudioFormat -{ - /// - /// Gets the collection of sampling rates (in Hz) the Opus encoder can use. - /// - public static IReadOnlyCollection AllowedSampleRates { get; } = new ReadOnlyCollection(new[] { 8000, 12000, 16000, 24000, 48000 }); - - /// - /// Gets the collection of channel counts the Opus encoder can use. - /// - public static IReadOnlyCollection AllowedChannelCounts { get; } = new ReadOnlyCollection(new[] { 1, 2 }); - - /// - /// Gets the collection of sample durations (in ms) the Opus encoder can use. - /// - public static IReadOnlyCollection AllowedSampleDurations { get; } = new ReadOnlyCollection(new[] { 5, 10, 20, 40, 60 }); - - /// - /// Gets the default audio format. This is a format configured for 48kHz sampling rate, 2 channels, with music quality preset. - /// - public static AudioFormat Default { get; } = new AudioFormat(48000, 2, VoiceApplication.Music); - - /// - /// Gets the audio sampling rate in Hz. - /// - public int SampleRate { get; } - - /// - /// Gets the audio channel count. - /// - public int ChannelCount { get; } - - /// - /// Gets the voice application, which dictates the quality preset. - /// - public VoiceApplication VoiceApplication { get; } - - /// - /// Creates a new audio format for use with Opus encoder. - /// - /// Audio sampling rate in Hz. - /// Number of audio channels in the data. - /// Encoder preset to use. - public AudioFormat(int sampleRate = 48000, int channelCount = 2, VoiceApplication voiceApplication = VoiceApplication.Music) - { - if (!AllowedSampleRates.Contains(sampleRate)) - { - throw new ArgumentOutOfRangeException(nameof(sampleRate), "Invalid sample rate specified."); - } - - if (!AllowedChannelCounts.Contains(channelCount)) - { - throw new ArgumentOutOfRangeException(nameof(channelCount), "Invalid channel count specified."); - } - - if (voiceApplication is not VoiceApplication.Music and not VoiceApplication.Voice and not VoiceApplication.LowLatency) - { - throw new ArgumentOutOfRangeException(nameof(voiceApplication), "Invalid voice application specified."); - } - - this.SampleRate = sampleRate; - this.ChannelCount = channelCount; - this.VoiceApplication = voiceApplication; - } - - /// - /// Calculates a sample size in bytes. - /// - /// Millisecond duration of a sample. - /// Calculated sample size in bytes. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly int CalculateSampleSize(int sampleDuration) - { - if (!AllowedSampleDurations.Contains(sampleDuration)) - { - throw new ArgumentOutOfRangeException(nameof(sampleDuration), "Invalid sample duration specified."); - } - - // Sample size in bytes is a product of the following: - // - duration in milliseconds - // - number of channels - // - sample rate in kHz - // - size of data (in this case, sizeof(int16_t)) - // which comes down to below: - return sampleDuration * this.ChannelCount * (this.SampleRate / 1000) * 2; - } - - /// - /// Gets the maximum buffer size for decoding. This method should be called when decoding Opus data to PCM, to ensure sufficient buffer size. - /// - /// Buffer size required to decode data. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public int GetMaximumBufferSize() - => CalculateMaximumFrameSize(); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal readonly int CalculateSampleDuration(int sampleSize) - => sampleSize / (this.SampleRate / 1000) / this.ChannelCount / 2 /* sizeof(int16_t) */; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal readonly int CalculateFrameSize(int sampleDuration) - => sampleDuration * (this.SampleRate / 1000); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal readonly int CalculateMaximumFrameSize() - => 120 * (this.SampleRate / 1000); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal readonly int SampleCountToSampleSize(int sampleCount) - => sampleCount * this.ChannelCount * 2 /* sizeof(int16_t) */; - - internal readonly bool IsValid() - => AllowedSampleRates.Contains(this.SampleRate) && AllowedChannelCounts.Contains(this.ChannelCount) && - (this.VoiceApplication == VoiceApplication.Music || this.VoiceApplication == VoiceApplication.Voice || this.VoiceApplication == VoiceApplication.LowLatency); -} +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Runtime.CompilerServices; + +namespace DSharpPlus.VoiceNext; + +/// +/// Defines the format of PCM data consumed or produced by Opus. +/// +public readonly struct AudioFormat +{ + /// + /// Gets the collection of sampling rates (in Hz) the Opus encoder can use. + /// + public static IReadOnlyCollection AllowedSampleRates { get; } = new ReadOnlyCollection(new[] { 8000, 12000, 16000, 24000, 48000 }); + + /// + /// Gets the collection of channel counts the Opus encoder can use. + /// + public static IReadOnlyCollection AllowedChannelCounts { get; } = new ReadOnlyCollection(new[] { 1, 2 }); + + /// + /// Gets the collection of sample durations (in ms) the Opus encoder can use. + /// + public static IReadOnlyCollection AllowedSampleDurations { get; } = new ReadOnlyCollection(new[] { 5, 10, 20, 40, 60 }); + + /// + /// Gets the default audio format. This is a format configured for 48kHz sampling rate, 2 channels, with music quality preset. + /// + public static AudioFormat Default { get; } = new AudioFormat(48000, 2, VoiceApplication.Music); + + /// + /// Gets the audio sampling rate in Hz. + /// + public int SampleRate { get; } + + /// + /// Gets the audio channel count. + /// + public int ChannelCount { get; } + + /// + /// Gets the voice application, which dictates the quality preset. + /// + public VoiceApplication VoiceApplication { get; } + + /// + /// Creates a new audio format for use with Opus encoder. + /// + /// Audio sampling rate in Hz. + /// Number of audio channels in the data. + /// Encoder preset to use. + public AudioFormat(int sampleRate = 48000, int channelCount = 2, VoiceApplication voiceApplication = VoiceApplication.Music) + { + if (!AllowedSampleRates.Contains(sampleRate)) + { + throw new ArgumentOutOfRangeException(nameof(sampleRate), "Invalid sample rate specified."); + } + + if (!AllowedChannelCounts.Contains(channelCount)) + { + throw new ArgumentOutOfRangeException(nameof(channelCount), "Invalid channel count specified."); + } + + if (voiceApplication is not VoiceApplication.Music and not VoiceApplication.Voice and not VoiceApplication.LowLatency) + { + throw new ArgumentOutOfRangeException(nameof(voiceApplication), "Invalid voice application specified."); + } + + this.SampleRate = sampleRate; + this.ChannelCount = channelCount; + this.VoiceApplication = voiceApplication; + } + + /// + /// Calculates a sample size in bytes. + /// + /// Millisecond duration of a sample. + /// Calculated sample size in bytes. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly int CalculateSampleSize(int sampleDuration) + { + if (!AllowedSampleDurations.Contains(sampleDuration)) + { + throw new ArgumentOutOfRangeException(nameof(sampleDuration), "Invalid sample duration specified."); + } + + // Sample size in bytes is a product of the following: + // - duration in milliseconds + // - number of channels + // - sample rate in kHz + // - size of data (in this case, sizeof(int16_t)) + // which comes down to below: + return sampleDuration * this.ChannelCount * (this.SampleRate / 1000) * 2; + } + + /// + /// Gets the maximum buffer size for decoding. This method should be called when decoding Opus data to PCM, to ensure sufficient buffer size. + /// + /// Buffer size required to decode data. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int GetMaximumBufferSize() + => CalculateMaximumFrameSize(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal readonly int CalculateSampleDuration(int sampleSize) + => sampleSize / (this.SampleRate / 1000) / this.ChannelCount / 2 /* sizeof(int16_t) */; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal readonly int CalculateFrameSize(int sampleDuration) + => sampleDuration * (this.SampleRate / 1000); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal readonly int CalculateMaximumFrameSize() + => 120 * (this.SampleRate / 1000); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal readonly int SampleCountToSampleSize(int sampleCount) + => sampleCount * this.ChannelCount * 2 /* sizeof(int16_t) */; + + internal readonly bool IsValid() + => AllowedSampleRates.Contains(this.SampleRate) && AllowedChannelCounts.Contains(this.ChannelCount) && + (this.VoiceApplication == VoiceApplication.Music || this.VoiceApplication == VoiceApplication.Voice || this.VoiceApplication == VoiceApplication.LowLatency); +} diff --git a/DSharpPlus.VoiceNext/Codec/Helpers.cs b/DSharpPlus.VoiceNext/Codec/Helpers.cs index cb62d3c639..c60c3e96c1 100644 --- a/DSharpPlus.VoiceNext/Codec/Helpers.cs +++ b/DSharpPlus.VoiceNext/Codec/Helpers.cs @@ -1,30 +1,30 @@ -using System; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -namespace DSharpPlus.VoiceNext.Codec; - -internal static class Helpers -{ - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ZeroFill(Span buff) - { - int zero = 0; - int i = 0; - for (; i < buff.Length / 4; i++) - { - MemoryMarshal.Write(buff, zero); - } - - int remainder = buff.Length % 4; - if (remainder == 0) - { - return; - } - - for (; i < buff.Length; i++) - { - buff[i] = 0; - } - } -} +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace DSharpPlus.VoiceNext.Codec; + +internal static class Helpers +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ZeroFill(Span buff) + { + int zero = 0; + int i = 0; + for (; i < buff.Length / 4; i++) + { + MemoryMarshal.Write(buff, zero); + } + + int remainder = buff.Length % 4; + if (remainder == 0) + { + return; + } + + for (; i < buff.Length; i++) + { + buff[i] = 0; + } + } +} diff --git a/DSharpPlus.VoiceNext/Codec/Interop.cs b/DSharpPlus.VoiceNext/Codec/Interop.cs index 3a9dd44771..e5f5b9b741 100644 --- a/DSharpPlus.VoiceNext/Codec/Interop.cs +++ b/DSharpPlus.VoiceNext/Codec/Interop.cs @@ -1,262 +1,262 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using System.Runtime.InteropServices; - -namespace DSharpPlus.VoiceNext.Codec; - -/// -/// This is an interop class. It contains wrapper methods for Opus and Sodium. -/// -internal static unsafe partial class Interop -{ - #region Sodium wrapper - private const string SodiumLibraryName = "libsodium"; - - /// - /// Gets the Sodium key size for xsalsa20_poly1305 algorithm. - /// - public static int SodiumKeySize { get; } = (int)crypto_aead_aes256gcm_keybytes(); - - /// - /// Gets the Sodium nonce size for xsalsa20_poly1305 algorithm. - /// - public static int SodiumNonceSize { get; } = (int)crypto_aead_aes256gcm_npubbytes(); - - public static int SodiumMacSize { get; } = (int)crypto_aead_aes256gcm_abytes(); - - /// - /// Indicates whether the current hardware is AEAD AES-256 GCM compatible. - /// - /// - public static bool IsAeadAes256GcmCompatible() - => crypto_aead_aes256gcm_is_available() == 1; - - public static void InitializeLibsodium() - { - // sodium_init returns 1 if sodium was already initialized, but that doesn't ~really~ matter for us. - if (sodium_init() < 0) - { - throw new InvalidOperationException("Libsodium failed to initialize."); - } - } - - [LibraryImport(SodiumLibraryName)] - private static partial int sodium_init(); - - [LibraryImport(SodiumLibraryName)] - private static partial int crypto_aead_aes256gcm_is_available(); - - [LibraryImport(SodiumLibraryName)] - private static partial nuint crypto_aead_aes256gcm_npubbytes(); - - [LibraryImport(SodiumLibraryName)] - private static partial nuint crypto_aead_aes256gcm_abytes(); - - [LibraryImport(SodiumLibraryName)] - private static partial nuint crypto_aead_aes256gcm_keybytes(); - - [LibraryImport(SodiumLibraryName)] - private static partial int crypto_aead_aes256gcm_encrypt - ( - byte* encrypted, // unsigned char *c - ulong *encryptedLength, // unsigned long long *clen_p - byte* message, // const unsigned char *m - ulong messageLength, // unsigned long long mlen - // non-confidential data appended to the message - byte* ad, // const unsigned char *ad - ulong adLength, // unsigned long long adlen - // unused, should be null - byte* nonceSecret, // const unsigned char *nsec - byte* noncePublic, // const unsigned char *npub - byte* key // const unsigned char *k - ); - - [LibraryImport(SodiumLibraryName)] - private static partial int crypto_aead_aes256gcm_decrypt - ( - byte* message, // unsigned char *m - ulong* messageLength, // unsigned long long *mlen_p - // unused, should be null - byte* nonceSecret, // unsigned char *nsec - byte* encrypted, // const unsigned char *c - ulong encryptedLength, // unsigned long long clen - // non-confidential data appended to the message - byte* ad, // const unsigned char *ad - ulong adLength, // unsigned long long adlen - byte* noncePublic, // const unsigned char *npub - byte* key // const unsigned char *p - ); - - /// - /// Encrypts supplied buffer using xsalsa20_poly1305 algorithm, using supplied key and nonce to perform encryption. - /// - /// Contents to encrypt. - /// Buffer to encrypt to. - /// Key to use for encryption. - /// Nonce to use for encryption. - /// Encryption status. - public static unsafe void Encrypt(ReadOnlySpan source, Span target, ReadOnlySpan key, ReadOnlySpan nonce) - { - ulong targetLength = (ulong)target.Length; - - fixed (byte* pSource = source) - fixed (byte* pTarget = target) - fixed (byte* pKey = key) - fixed (byte* pNonce = nonce) - { - crypto_aead_aes256gcm_encrypt(pTarget, &targetLength, pSource, (ulong)source.Length, null, 0, null, pNonce, pKey); - } - } - - /// - /// Decrypts supplied buffer using xsalsa20_poly1305 algorithm, using supplied key and nonce to perform decryption. - /// - /// Buffer to decrypt from. - /// Decrypted message buffer. - /// Key to use for decryption. - /// Nonce to use for decryption. - /// Decryption status. - public static unsafe int Decrypt(ReadOnlySpan source, Span target, ReadOnlySpan key, ReadOnlySpan nonce) - { - ulong targetLength = (ulong)target.Length; - - fixed (byte* pSource = source) - fixed (byte* pTarget = target) - fixed (byte* pKey = key) - fixed (byte* pNonce = nonce) - { - return crypto_aead_aes256gcm_decrypt(pTarget, &targetLength, null, pSource, (ulong)source.Length, null, 0, pNonce, pKey); - } - } - #endregion - - #region Opus wrapper - private const string OpusLibraryName = "libopus"; - - [LibraryImport(OpusLibraryName, EntryPoint = "opus_encoder_create")] - private static partial IntPtr OpusCreateEncoder(int sampleRate, int channels, int application, out OpusError error); - - [LibraryImport(OpusLibraryName, EntryPoint = "opus_encoder_destroy")] - public static partial void OpusDestroyEncoder(IntPtr encoder); - - [LibraryImport(OpusLibraryName, EntryPoint = "opus_encode")] - private static unsafe partial int OpusEncode(IntPtr encoder, byte* pcmData, int frameSize, byte* data, int maxDataBytes); - - [LibraryImport(OpusLibraryName, EntryPoint = "opus_encoder_ctl")] - private static partial OpusError OpusEncoderControl(IntPtr encoder, OpusControl request, int value); - - [LibraryImport(OpusLibraryName, EntryPoint = "opus_decoder_create")] - private static partial IntPtr OpusCreateDecoder(int sampleRate, int channels, out OpusError error); - - [LibraryImport(OpusLibraryName, EntryPoint = "opus_decoder_destroy")] - public static partial void OpusDestroyDecoder(IntPtr decoder); - - [LibraryImport(OpusLibraryName, EntryPoint = "opus_decode")] - private static unsafe partial int OpusDecode(IntPtr decoder, byte* opusData, int opusDataLength, byte* data, int frameSize, int decodeFec); - - [LibraryImport(OpusLibraryName, EntryPoint = "opus_packet_get_nb_channels")] - private static unsafe partial int OpusGetPacketChanelCount(byte* opusData); - - [LibraryImport(OpusLibraryName, EntryPoint = "opus_packet_get_nb_frames")] - private static unsafe partial int OpusGetPacketFrameCount(byte* opusData, int length); - - [LibraryImport(OpusLibraryName, EntryPoint = "opus_packet_get_samples_per_frame")] - private static unsafe partial int OpusGetPacketSamplePerFrameCount(byte* opusData, int samplingRate); - - [LibraryImport(OpusLibraryName, EntryPoint = "opus_decoder_ctl")] - private static partial int OpusDecoderControl(IntPtr decoder, OpusControl request, out int value); - - public static IntPtr OpusCreateEncoder(AudioFormat audioFormat) - { - nint encoder = OpusCreateEncoder(audioFormat.SampleRate, audioFormat.ChannelCount, (int)audioFormat.VoiceApplication, out OpusError error); - return error != OpusError.Ok ? throw new Exception($"Could not instantiate Opus encoder: {error} ({(int)error}).") : encoder; - } - - public static void OpusSetEncoderOption(IntPtr encoder, OpusControl option, int value) - { - OpusError error; - if ((error = OpusEncoderControl(encoder, option, value)) != OpusError.Ok) - { - throw new Exception($"Could not set Opus encoder option: {error} ({(int)error})."); - } - } - - public static unsafe void OpusEncode(IntPtr encoder, ReadOnlySpan pcm, int frameSize, ref Span opus) - { - int len = 0; - - fixed (byte* pcmPtr = &pcm.GetPinnableReference()) - fixed (byte* opusPtr = &opus.GetPinnableReference()) - { - len = OpusEncode(encoder, pcmPtr, frameSize, opusPtr, opus.Length); - } - - if (len < 0) - { - OpusError error = (OpusError)len; - throw new Exception($"Could not encode PCM data to Opus: {error} ({(int)error})."); - } - - opus = opus[..len]; - } - - public static IntPtr OpusCreateDecoder(AudioFormat audioFormat) - { - nint decoder = OpusCreateDecoder(audioFormat.SampleRate, audioFormat.ChannelCount, out OpusError error); - return error != OpusError.Ok ? throw new Exception($"Could not instantiate Opus decoder: {error} ({(int)error}).") : decoder; - } - - public static unsafe int OpusDecode(IntPtr decoder, ReadOnlySpan opus, int frameSize, Span pcm, bool useFec) - { - int len = 0; - - fixed (byte* opusPtr = &opus.GetPinnableReference()) - fixed (byte* pcmPtr = &pcm.GetPinnableReference()) - { - len = OpusDecode(decoder, opusPtr, opus.Length, pcmPtr, frameSize, useFec ? 1 : 0); - } - - if (len < 0) - { - OpusError error = (OpusError)len; - throw new Exception($"Could not decode PCM data from Opus: {error} ({(int)error})."); - } - - return len; - } - - public static unsafe int OpusDecode(IntPtr decoder, int frameSize, Span pcm) - { - int len = 0; - - fixed (byte* pcmPtr = &pcm.GetPinnableReference()) - { - len = OpusDecode(decoder, null, 0, pcmPtr, frameSize, 1); - } - - if (len < 0) - { - OpusError error = (OpusError)len; - throw new Exception($"Could not decode PCM data from Opus: {error} ({(int)error})."); - } - - return len; - } - - public static unsafe void OpusGetPacketMetrics(ReadOnlySpan opus, int samplingRate, out int channels, out int frames, out int samplesPerFrame, out int frameSize) - { - fixed (byte* opusPtr = &opus.GetPinnableReference()) - { - frames = OpusGetPacketFrameCount(opusPtr, opus.Length); - samplesPerFrame = OpusGetPacketSamplePerFrameCount(opusPtr, samplingRate); - channels = OpusGetPacketChanelCount(opusPtr); - } - - frameSize = frames * samplesPerFrame; - } - - [SuppressMessage("Quality Assurance", "CA1806:OpusGetLastPacketDuration calls OpusDecoderControl but does not use the HRESULT or error code that the method returns. This could lead to unexpected behavior in error conditions or low-resource situations. Use the result in a conditional statement, assign the result to a variable, or pass it as an argument to another method.", - Justification = "It's VoiceNext and I don't care - Lunar")] - public static void OpusGetLastPacketDuration(IntPtr decoder, out int sampleCount) => OpusDecoderControl(decoder, OpusControl.GetLastPacketDuration, out sampleCount); - #endregion -} +using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; + +namespace DSharpPlus.VoiceNext.Codec; + +/// +/// This is an interop class. It contains wrapper methods for Opus and Sodium. +/// +internal static unsafe partial class Interop +{ + #region Sodium wrapper + private const string SodiumLibraryName = "libsodium"; + + /// + /// Gets the Sodium key size for xsalsa20_poly1305 algorithm. + /// + public static int SodiumKeySize { get; } = (int)crypto_aead_aes256gcm_keybytes(); + + /// + /// Gets the Sodium nonce size for xsalsa20_poly1305 algorithm. + /// + public static int SodiumNonceSize { get; } = (int)crypto_aead_aes256gcm_npubbytes(); + + public static int SodiumMacSize { get; } = (int)crypto_aead_aes256gcm_abytes(); + + /// + /// Indicates whether the current hardware is AEAD AES-256 GCM compatible. + /// + /// + public static bool IsAeadAes256GcmCompatible() + => crypto_aead_aes256gcm_is_available() == 1; + + public static void InitializeLibsodium() + { + // sodium_init returns 1 if sodium was already initialized, but that doesn't ~really~ matter for us. + if (sodium_init() < 0) + { + throw new InvalidOperationException("Libsodium failed to initialize."); + } + } + + [LibraryImport(SodiumLibraryName)] + private static partial int sodium_init(); + + [LibraryImport(SodiumLibraryName)] + private static partial int crypto_aead_aes256gcm_is_available(); + + [LibraryImport(SodiumLibraryName)] + private static partial nuint crypto_aead_aes256gcm_npubbytes(); + + [LibraryImport(SodiumLibraryName)] + private static partial nuint crypto_aead_aes256gcm_abytes(); + + [LibraryImport(SodiumLibraryName)] + private static partial nuint crypto_aead_aes256gcm_keybytes(); + + [LibraryImport(SodiumLibraryName)] + private static partial int crypto_aead_aes256gcm_encrypt + ( + byte* encrypted, // unsigned char *c + ulong *encryptedLength, // unsigned long long *clen_p + byte* message, // const unsigned char *m + ulong messageLength, // unsigned long long mlen + // non-confidential data appended to the message + byte* ad, // const unsigned char *ad + ulong adLength, // unsigned long long adlen + // unused, should be null + byte* nonceSecret, // const unsigned char *nsec + byte* noncePublic, // const unsigned char *npub + byte* key // const unsigned char *k + ); + + [LibraryImport(SodiumLibraryName)] + private static partial int crypto_aead_aes256gcm_decrypt + ( + byte* message, // unsigned char *m + ulong* messageLength, // unsigned long long *mlen_p + // unused, should be null + byte* nonceSecret, // unsigned char *nsec + byte* encrypted, // const unsigned char *c + ulong encryptedLength, // unsigned long long clen + // non-confidential data appended to the message + byte* ad, // const unsigned char *ad + ulong adLength, // unsigned long long adlen + byte* noncePublic, // const unsigned char *npub + byte* key // const unsigned char *p + ); + + /// + /// Encrypts supplied buffer using xsalsa20_poly1305 algorithm, using supplied key and nonce to perform encryption. + /// + /// Contents to encrypt. + /// Buffer to encrypt to. + /// Key to use for encryption. + /// Nonce to use for encryption. + /// Encryption status. + public static unsafe void Encrypt(ReadOnlySpan source, Span target, ReadOnlySpan key, ReadOnlySpan nonce) + { + ulong targetLength = (ulong)target.Length; + + fixed (byte* pSource = source) + fixed (byte* pTarget = target) + fixed (byte* pKey = key) + fixed (byte* pNonce = nonce) + { + crypto_aead_aes256gcm_encrypt(pTarget, &targetLength, pSource, (ulong)source.Length, null, 0, null, pNonce, pKey); + } + } + + /// + /// Decrypts supplied buffer using xsalsa20_poly1305 algorithm, using supplied key and nonce to perform decryption. + /// + /// Buffer to decrypt from. + /// Decrypted message buffer. + /// Key to use for decryption. + /// Nonce to use for decryption. + /// Decryption status. + public static unsafe int Decrypt(ReadOnlySpan source, Span target, ReadOnlySpan key, ReadOnlySpan nonce) + { + ulong targetLength = (ulong)target.Length; + + fixed (byte* pSource = source) + fixed (byte* pTarget = target) + fixed (byte* pKey = key) + fixed (byte* pNonce = nonce) + { + return crypto_aead_aes256gcm_decrypt(pTarget, &targetLength, null, pSource, (ulong)source.Length, null, 0, pNonce, pKey); + } + } + #endregion + + #region Opus wrapper + private const string OpusLibraryName = "libopus"; + + [LibraryImport(OpusLibraryName, EntryPoint = "opus_encoder_create")] + private static partial IntPtr OpusCreateEncoder(int sampleRate, int channels, int application, out OpusError error); + + [LibraryImport(OpusLibraryName, EntryPoint = "opus_encoder_destroy")] + public static partial void OpusDestroyEncoder(IntPtr encoder); + + [LibraryImport(OpusLibraryName, EntryPoint = "opus_encode")] + private static unsafe partial int OpusEncode(IntPtr encoder, byte* pcmData, int frameSize, byte* data, int maxDataBytes); + + [LibraryImport(OpusLibraryName, EntryPoint = "opus_encoder_ctl")] + private static partial OpusError OpusEncoderControl(IntPtr encoder, OpusControl request, int value); + + [LibraryImport(OpusLibraryName, EntryPoint = "opus_decoder_create")] + private static partial IntPtr OpusCreateDecoder(int sampleRate, int channels, out OpusError error); + + [LibraryImport(OpusLibraryName, EntryPoint = "opus_decoder_destroy")] + public static partial void OpusDestroyDecoder(IntPtr decoder); + + [LibraryImport(OpusLibraryName, EntryPoint = "opus_decode")] + private static unsafe partial int OpusDecode(IntPtr decoder, byte* opusData, int opusDataLength, byte* data, int frameSize, int decodeFec); + + [LibraryImport(OpusLibraryName, EntryPoint = "opus_packet_get_nb_channels")] + private static unsafe partial int OpusGetPacketChanelCount(byte* opusData); + + [LibraryImport(OpusLibraryName, EntryPoint = "opus_packet_get_nb_frames")] + private static unsafe partial int OpusGetPacketFrameCount(byte* opusData, int length); + + [LibraryImport(OpusLibraryName, EntryPoint = "opus_packet_get_samples_per_frame")] + private static unsafe partial int OpusGetPacketSamplePerFrameCount(byte* opusData, int samplingRate); + + [LibraryImport(OpusLibraryName, EntryPoint = "opus_decoder_ctl")] + private static partial int OpusDecoderControl(IntPtr decoder, OpusControl request, out int value); + + public static IntPtr OpusCreateEncoder(AudioFormat audioFormat) + { + nint encoder = OpusCreateEncoder(audioFormat.SampleRate, audioFormat.ChannelCount, (int)audioFormat.VoiceApplication, out OpusError error); + return error != OpusError.Ok ? throw new Exception($"Could not instantiate Opus encoder: {error} ({(int)error}).") : encoder; + } + + public static void OpusSetEncoderOption(IntPtr encoder, OpusControl option, int value) + { + OpusError error; + if ((error = OpusEncoderControl(encoder, option, value)) != OpusError.Ok) + { + throw new Exception($"Could not set Opus encoder option: {error} ({(int)error})."); + } + } + + public static unsafe void OpusEncode(IntPtr encoder, ReadOnlySpan pcm, int frameSize, ref Span opus) + { + int len = 0; + + fixed (byte* pcmPtr = &pcm.GetPinnableReference()) + fixed (byte* opusPtr = &opus.GetPinnableReference()) + { + len = OpusEncode(encoder, pcmPtr, frameSize, opusPtr, opus.Length); + } + + if (len < 0) + { + OpusError error = (OpusError)len; + throw new Exception($"Could not encode PCM data to Opus: {error} ({(int)error})."); + } + + opus = opus[..len]; + } + + public static IntPtr OpusCreateDecoder(AudioFormat audioFormat) + { + nint decoder = OpusCreateDecoder(audioFormat.SampleRate, audioFormat.ChannelCount, out OpusError error); + return error != OpusError.Ok ? throw new Exception($"Could not instantiate Opus decoder: {error} ({(int)error}).") : decoder; + } + + public static unsafe int OpusDecode(IntPtr decoder, ReadOnlySpan opus, int frameSize, Span pcm, bool useFec) + { + int len = 0; + + fixed (byte* opusPtr = &opus.GetPinnableReference()) + fixed (byte* pcmPtr = &pcm.GetPinnableReference()) + { + len = OpusDecode(decoder, opusPtr, opus.Length, pcmPtr, frameSize, useFec ? 1 : 0); + } + + if (len < 0) + { + OpusError error = (OpusError)len; + throw new Exception($"Could not decode PCM data from Opus: {error} ({(int)error})."); + } + + return len; + } + + public static unsafe int OpusDecode(IntPtr decoder, int frameSize, Span pcm) + { + int len = 0; + + fixed (byte* pcmPtr = &pcm.GetPinnableReference()) + { + len = OpusDecode(decoder, null, 0, pcmPtr, frameSize, 1); + } + + if (len < 0) + { + OpusError error = (OpusError)len; + throw new Exception($"Could not decode PCM data from Opus: {error} ({(int)error})."); + } + + return len; + } + + public static unsafe void OpusGetPacketMetrics(ReadOnlySpan opus, int samplingRate, out int channels, out int frames, out int samplesPerFrame, out int frameSize) + { + fixed (byte* opusPtr = &opus.GetPinnableReference()) + { + frames = OpusGetPacketFrameCount(opusPtr, opus.Length); + samplesPerFrame = OpusGetPacketSamplePerFrameCount(opusPtr, samplingRate); + channels = OpusGetPacketChanelCount(opusPtr); + } + + frameSize = frames * samplesPerFrame; + } + + [SuppressMessage("Quality Assurance", "CA1806:OpusGetLastPacketDuration calls OpusDecoderControl but does not use the HRESULT or error code that the method returns. This could lead to unexpected behavior in error conditions or low-resource situations. Use the result in a conditional statement, assign the result to a variable, or pass it as an argument to another method.", + Justification = "It's VoiceNext and I don't care - Lunar")] + public static void OpusGetLastPacketDuration(IntPtr decoder, out int sampleCount) => OpusDecoderControl(decoder, OpusControl.GetLastPacketDuration, out sampleCount); + #endregion +} diff --git a/DSharpPlus.VoiceNext/Codec/Opus.cs b/DSharpPlus.VoiceNext/Codec/Opus.cs index 11cd696a2f..edd90b8574 100644 --- a/DSharpPlus.VoiceNext/Codec/Opus.cs +++ b/DSharpPlus.VoiceNext/Codec/Opus.cs @@ -1,215 +1,215 @@ -using System; -using System.Collections.Generic; - -namespace DSharpPlus.VoiceNext.Codec; - -internal sealed class Opus : IDisposable -{ - public AudioFormat AudioFormat { get; } - - private IntPtr Encoder { get; } - - private List ManagedDecoders { get; } - - public Opus(AudioFormat audioFormat) - { - if (!audioFormat.IsValid()) - { - throw new ArgumentException("Invalid audio format specified.", nameof(audioFormat)); - } - - this.AudioFormat = audioFormat; - this.Encoder = Interop.OpusCreateEncoder(this.AudioFormat); - - // Set appropriate encoder options - OpusSignal sig = OpusSignal.Auto; - switch (this.AudioFormat.VoiceApplication) - { - case VoiceApplication.Music: - sig = OpusSignal.Music; - break; - - case VoiceApplication.Voice: - sig = OpusSignal.Voice; - break; - } - Interop.OpusSetEncoderOption(this.Encoder, OpusControl.SetSignal, (int)sig); - Interop.OpusSetEncoderOption(this.Encoder, OpusControl.SetPacketLossPercent, 15); - Interop.OpusSetEncoderOption(this.Encoder, OpusControl.SetInBandFec, 1); - Interop.OpusSetEncoderOption(this.Encoder, OpusControl.SetBitrate, 131072); - - this.ManagedDecoders = []; - } - - 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)); - } - - int duration = this.AudioFormat.CalculateSampleDuration(pcm.Length); - int frameSize = this.AudioFormat.CalculateFrameSize(duration); - int 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); - } - - 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 int channels, out _, out _, out int 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); - } - - int sampleCount = Interop.OpusDecode(decoder.Decoder, opus, frameSize, target, useFec); - - int sampleSize = outputFormat.SampleCountToSampleSize(sampleCount); - target = target[..sampleSize]; - } - - public static void ProcessPacketLoss(OpusDecoder decoder, int frameSize, ref Span target) => Interop.OpusDecode(decoder.Decoder, frameSize, target); - - public static int GetLastPacketSampleCount(OpusDecoder decoder) - { - Interop.OpusGetLastPacketDuration(decoder.Decoder, out int sampleCount); - return sampleCount; - } - - public OpusDecoder CreateDecoder() - { - lock (this.ManagedDecoders) - { - OpusDecoder managedDecoder = new(this); - this.ManagedDecoders.Add(managedDecoder); - return managedDecoder; - } - } - - public void DestroyDecoder(OpusDecoder decoder) - { - lock (this.ManagedDecoders) - { - if (!this.ManagedDecoders.Contains(decoder)) - { - return; - } - - this.ManagedDecoders.Remove(decoder); - decoder.Dispose(); - } - } - - public void Dispose() - { - Interop.OpusDestroyEncoder(this.Encoder); - - lock (this.ManagedDecoders) - { - foreach (OpusDecoder 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; } - - internal Opus Opus { get; } - internal IntPtr Decoder { get; private set; } - private bool disposedValue; - - 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); - } - - protected virtual void Dispose(bool disposing) - { - if (!this.disposedValue) - { - if (this.Decoder != IntPtr.Zero) - { - Interop.OpusDestroyDecoder(this.Decoder); - } - - this.disposedValue = true; - } - } - - /// - /// Disposes of this Opus decoder. - /// - public void Dispose() - { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(disposing: true); - GC.SuppressFinalize(this); - } -} - -[Flags] -internal enum OpusError -{ - Ok = 0, - BadArgument = -1, - BufferTooSmall = -2, - InternalError = -3, - InvalidPacket = -4, - Unimplemented = -5, - InvalidState = -6, - AllocationFailure = -7 -} - -internal enum OpusControl : int -{ - SetBitrate = 4002, - SetBandwidth = 4008, - SetInBandFec = 4012, - SetPacketLossPercent = 4014, - SetSignal = 4024, - ResetState = 4028, - GetLastPacketDuration = 4039 -} - -internal enum OpusSignal : int -{ - Auto = -1000, - Voice = 3001, - Music = 3002, -} +using System; +using System.Collections.Generic; + +namespace DSharpPlus.VoiceNext.Codec; + +internal sealed class Opus : IDisposable +{ + public AudioFormat AudioFormat { get; } + + private IntPtr Encoder { get; } + + private List ManagedDecoders { get; } + + public Opus(AudioFormat audioFormat) + { + if (!audioFormat.IsValid()) + { + throw new ArgumentException("Invalid audio format specified.", nameof(audioFormat)); + } + + this.AudioFormat = audioFormat; + this.Encoder = Interop.OpusCreateEncoder(this.AudioFormat); + + // Set appropriate encoder options + OpusSignal sig = OpusSignal.Auto; + switch (this.AudioFormat.VoiceApplication) + { + case VoiceApplication.Music: + sig = OpusSignal.Music; + break; + + case VoiceApplication.Voice: + sig = OpusSignal.Voice; + break; + } + Interop.OpusSetEncoderOption(this.Encoder, OpusControl.SetSignal, (int)sig); + Interop.OpusSetEncoderOption(this.Encoder, OpusControl.SetPacketLossPercent, 15); + Interop.OpusSetEncoderOption(this.Encoder, OpusControl.SetInBandFec, 1); + Interop.OpusSetEncoderOption(this.Encoder, OpusControl.SetBitrate, 131072); + + this.ManagedDecoders = []; + } + + 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)); + } + + int duration = this.AudioFormat.CalculateSampleDuration(pcm.Length); + int frameSize = this.AudioFormat.CalculateFrameSize(duration); + int 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); + } + + 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 int channels, out _, out _, out int 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); + } + + int sampleCount = Interop.OpusDecode(decoder.Decoder, opus, frameSize, target, useFec); + + int sampleSize = outputFormat.SampleCountToSampleSize(sampleCount); + target = target[..sampleSize]; + } + + public static void ProcessPacketLoss(OpusDecoder decoder, int frameSize, ref Span target) => Interop.OpusDecode(decoder.Decoder, frameSize, target); + + public static int GetLastPacketSampleCount(OpusDecoder decoder) + { + Interop.OpusGetLastPacketDuration(decoder.Decoder, out int sampleCount); + return sampleCount; + } + + public OpusDecoder CreateDecoder() + { + lock (this.ManagedDecoders) + { + OpusDecoder managedDecoder = new(this); + this.ManagedDecoders.Add(managedDecoder); + return managedDecoder; + } + } + + public void DestroyDecoder(OpusDecoder decoder) + { + lock (this.ManagedDecoders) + { + if (!this.ManagedDecoders.Contains(decoder)) + { + return; + } + + this.ManagedDecoders.Remove(decoder); + decoder.Dispose(); + } + } + + public void Dispose() + { + Interop.OpusDestroyEncoder(this.Encoder); + + lock (this.ManagedDecoders) + { + foreach (OpusDecoder 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; } + + internal Opus Opus { get; } + internal IntPtr Decoder { get; private set; } + private bool disposedValue; + + 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); + } + + protected virtual void Dispose(bool disposing) + { + if (!this.disposedValue) + { + if (this.Decoder != IntPtr.Zero) + { + Interop.OpusDestroyDecoder(this.Decoder); + } + + this.disposedValue = true; + } + } + + /// + /// Disposes of this Opus decoder. + /// + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } +} + +[Flags] +internal enum OpusError +{ + Ok = 0, + BadArgument = -1, + BufferTooSmall = -2, + InternalError = -3, + InvalidPacket = -4, + Unimplemented = -5, + InvalidState = -6, + AllocationFailure = -7 +} + +internal enum OpusControl : int +{ + SetBitrate = 4002, + SetBandwidth = 4008, + SetInBandFec = 4012, + SetPacketLossPercent = 4014, + SetSignal = 4024, + ResetState = 4028, + GetLastPacketDuration = 4039 +} + +internal enum OpusSignal : int +{ + Auto = -1000, + Voice = 3001, + Music = 3002, +} diff --git a/DSharpPlus.VoiceNext/Codec/Rtp.cs b/DSharpPlus.VoiceNext/Codec/Rtp.cs index db15596cce..ecff090b59 100644 --- a/DSharpPlus.VoiceNext/Codec/Rtp.cs +++ b/DSharpPlus.VoiceNext/Codec/Rtp.cs @@ -1,78 +1,78 @@ -using System; -using System.Buffers.Binary; - -namespace DSharpPlus.VoiceNext.Codec; - -internal sealed class Rtp : IDisposable -{ - public const int HeaderSize = 12; - - private const byte RtpNoExtension = 0x80; - private const byte RtpExtension = 0x90; - private const byte RtpVersion = 0x78; - - public Rtp() - { } - - public static void EncodeHeader(ushort sequence, uint timestamp, uint ssrc, Span target) - { - if (target.Length < HeaderSize) - { - throw new ArgumentException("Header buffer is too short.", nameof(target)); - } - - target[0] = RtpNoExtension; - target[1] = RtpVersion; - - // Write data big endian - BinaryPrimitives.WriteUInt16BigEndian(target[2..], sequence); // header + magic - BinaryPrimitives.WriteUInt32BigEndian(target[4..], timestamp); // header + magic + sizeof(sequence) - BinaryPrimitives.WriteUInt32BigEndian(target[8..], ssrc); // header + magic + sizeof(sequence) + sizeof(timestamp) - } - - public static bool IsRtpHeader(ReadOnlySpan source) => source.Length >= HeaderSize && (source[0] == RtpNoExtension || source[0] == RtpExtension) && source[1] == RtpVersion; - - public static void DecodeHeader(ReadOnlySpan source, out ushort sequence, out uint timestamp, out uint ssrc, out bool hasExtension) - { - if (source.Length < HeaderSize) - { - throw new ArgumentException("Header buffer is too short.", nameof(source)); - } - - if ((source[0] != RtpNoExtension && source[0] != RtpExtension) || source[1] != RtpVersion) - { - throw new ArgumentException("Invalid RTP header.", nameof(source)); - } - - hasExtension = source[0] == RtpExtension; - - // Read data big endian - sequence = BinaryPrimitives.ReadUInt16BigEndian(source[2..]); - timestamp = BinaryPrimitives.ReadUInt32BigEndian(source[4..]); - ssrc = BinaryPrimitives.ReadUInt32BigEndian(source[8..]); - } - - public static int CalculatePacketSize(int encryptedLength, EncryptionMode encryptionMode) => encryptionMode switch - { - EncryptionMode.AeadAes256GcmRtpSize => HeaderSize + encryptedLength + Interop.SodiumNonceSize, - _ => throw new ArgumentException("Unsupported encryption mode.", nameof(encryptionMode)), - }; - - public static void GetDataFromPacket(ReadOnlySpan packet, out ReadOnlySpan data, EncryptionMode encryptionMode) - { - switch (encryptionMode) - { - case EncryptionMode.AeadAes256GcmRtpSize: - data = packet.Slice(HeaderSize, packet.Length - HeaderSize - Interop.SodiumNonceSize); - return; - - default: - throw new ArgumentException("Unsupported encryption mode.", nameof(encryptionMode)); - } - } - - public void Dispose() - { - - } -} +using System; +using System.Buffers.Binary; + +namespace DSharpPlus.VoiceNext.Codec; + +internal sealed class Rtp : IDisposable +{ + public const int HeaderSize = 12; + + private const byte RtpNoExtension = 0x80; + private const byte RtpExtension = 0x90; + private const byte RtpVersion = 0x78; + + public Rtp() + { } + + public static void EncodeHeader(ushort sequence, uint timestamp, uint ssrc, Span target) + { + if (target.Length < HeaderSize) + { + throw new ArgumentException("Header buffer is too short.", nameof(target)); + } + + target[0] = RtpNoExtension; + target[1] = RtpVersion; + + // Write data big endian + BinaryPrimitives.WriteUInt16BigEndian(target[2..], sequence); // header + magic + BinaryPrimitives.WriteUInt32BigEndian(target[4..], timestamp); // header + magic + sizeof(sequence) + BinaryPrimitives.WriteUInt32BigEndian(target[8..], ssrc); // header + magic + sizeof(sequence) + sizeof(timestamp) + } + + public static bool IsRtpHeader(ReadOnlySpan source) => source.Length >= HeaderSize && (source[0] == RtpNoExtension || source[0] == RtpExtension) && source[1] == RtpVersion; + + public static void DecodeHeader(ReadOnlySpan source, out ushort sequence, out uint timestamp, out uint ssrc, out bool hasExtension) + { + if (source.Length < HeaderSize) + { + throw new ArgumentException("Header buffer is too short.", nameof(source)); + } + + if ((source[0] != RtpNoExtension && source[0] != RtpExtension) || source[1] != RtpVersion) + { + throw new ArgumentException("Invalid RTP header.", nameof(source)); + } + + hasExtension = source[0] == RtpExtension; + + // Read data big endian + sequence = BinaryPrimitives.ReadUInt16BigEndian(source[2..]); + timestamp = BinaryPrimitives.ReadUInt32BigEndian(source[4..]); + ssrc = BinaryPrimitives.ReadUInt32BigEndian(source[8..]); + } + + public static int CalculatePacketSize(int encryptedLength, EncryptionMode encryptionMode) => encryptionMode switch + { + EncryptionMode.AeadAes256GcmRtpSize => HeaderSize + encryptedLength + Interop.SodiumNonceSize, + _ => throw new ArgumentException("Unsupported encryption mode.", nameof(encryptionMode)), + }; + + public static void GetDataFromPacket(ReadOnlySpan packet, out ReadOnlySpan data, EncryptionMode encryptionMode) + { + switch (encryptionMode) + { + case EncryptionMode.AeadAes256GcmRtpSize: + data = packet.Slice(HeaderSize, packet.Length - HeaderSize - Interop.SodiumNonceSize); + return; + + default: + throw new ArgumentException("Unsupported encryption mode.", nameof(encryptionMode)); + } + } + + public void Dispose() + { + + } +} diff --git a/DSharpPlus.VoiceNext/Codec/Sodium.cs b/DSharpPlus.VoiceNext/Codec/Sodium.cs index fb27d50ae0..b09549bfbc 100644 --- a/DSharpPlus.VoiceNext/Codec/Sodium.cs +++ b/DSharpPlus.VoiceNext/Codec/Sodium.cs @@ -1,181 +1,181 @@ -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 DSharpPlus.VoiceNext.Codec; - -internal sealed class Sodium : IDisposable -{ - public static IReadOnlyDictionary SupportedModes { get; } = new ReadOnlyDictionary(new Dictionary() - { - ["aead_aes256_gcm_rtpsize"] = EncryptionMode.AeadAes256GcmRtpSize, - }); - - public static int NonceSize => Interop.SodiumNonceSize; - - private RandomNumberGenerator CSPRNG { get; } - private byte[] Buffer { get; } - private ReadOnlyMemory Key { get; } - - 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.CSPRNG = RandomNumberGenerator.Create(); - this.Buffer = new byte[Interop.SodiumNonceSize]; - } - - public static void GenerateNonce(ReadOnlySpan rtpHeader, Span target) - { - if (rtpHeader.Length != Rtp.HeaderSize) - { - throw new ArgumentException($"RTP header needs to have a length of exactly {Rtp.HeaderSize} 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..]); - } - - 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); - } - - public static 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..]); - } - - public static void AppendNonce(ReadOnlySpan nonce, Span target, EncryptionMode encryptionMode) - { - switch (encryptionMode) - { - case EncryptionMode.AeadAes256GcmRtpSize: - nonce[..4].CopyTo(target[^12..]); - return; - - default: - throw new ArgumentException("Unsupported encryption mode.", nameof(encryptionMode)); - } - } - - public static 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.AeadAes256GcmRtpSize: - source[..12].CopyTo(target); - return; - - default: - throw new ArgumentException("Unsupported encryption mode.", nameof(encryptionMode)); - } - } - - 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)); - } - - Interop.Encrypt(source, target, this.Key.Span, 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) - { - throw new CryptographicException($"Could not decrypt the buffer. Sodium returned code {result}."); - } - } - - public void Dispose() => this.CSPRNG.Dispose(); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static KeyValuePair SelectMode(IEnumerable availableModes) - { - string[] availableModesArray = availableModes.ToArray(); - foreach (KeyValuePair kvMode in SupportedModes) - { - if (availableModesArray.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."); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int CalculateTargetSize(ReadOnlySpan source) - => source.Length + Interop.SodiumMacSize; - - [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 only currently supported encryption mode. Uses a 32-bit incremental nonce. - /// - AeadAes256GcmRtpSize -} +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 DSharpPlus.VoiceNext.Codec; + +internal sealed class Sodium : IDisposable +{ + public static IReadOnlyDictionary SupportedModes { get; } = new ReadOnlyDictionary(new Dictionary() + { + ["aead_aes256_gcm_rtpsize"] = EncryptionMode.AeadAes256GcmRtpSize, + }); + + public static int NonceSize => Interop.SodiumNonceSize; + + private RandomNumberGenerator CSPRNG { get; } + private byte[] Buffer { get; } + private ReadOnlyMemory Key { get; } + + 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.CSPRNG = RandomNumberGenerator.Create(); + this.Buffer = new byte[Interop.SodiumNonceSize]; + } + + public static void GenerateNonce(ReadOnlySpan rtpHeader, Span target) + { + if (rtpHeader.Length != Rtp.HeaderSize) + { + throw new ArgumentException($"RTP header needs to have a length of exactly {Rtp.HeaderSize} 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..]); + } + + 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); + } + + public static 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..]); + } + + public static void AppendNonce(ReadOnlySpan nonce, Span target, EncryptionMode encryptionMode) + { + switch (encryptionMode) + { + case EncryptionMode.AeadAes256GcmRtpSize: + nonce[..4].CopyTo(target[^12..]); + return; + + default: + throw new ArgumentException("Unsupported encryption mode.", nameof(encryptionMode)); + } + } + + public static 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.AeadAes256GcmRtpSize: + source[..12].CopyTo(target); + return; + + default: + throw new ArgumentException("Unsupported encryption mode.", nameof(encryptionMode)); + } + } + + 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)); + } + + Interop.Encrypt(source, target, this.Key.Span, 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) + { + throw new CryptographicException($"Could not decrypt the buffer. Sodium returned code {result}."); + } + } + + public void Dispose() => this.CSPRNG.Dispose(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static KeyValuePair SelectMode(IEnumerable availableModes) + { + string[] availableModesArray = availableModes.ToArray(); + foreach (KeyValuePair kvMode in SupportedModes) + { + if (availableModesArray.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."); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int CalculateTargetSize(ReadOnlySpan source) + => source.Length + Interop.SodiumMacSize; + + [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 only currently supported encryption mode. Uses a 32-bit incremental nonce. + /// + AeadAes256GcmRtpSize +} diff --git a/DSharpPlus.VoiceNext/DiscordClientExtensions.cs b/DSharpPlus.VoiceNext/DiscordClientExtensions.cs index 6154d3214e..5d6d3f9ba1 100644 --- a/DSharpPlus.VoiceNext/DiscordClientExtensions.cs +++ b/DSharpPlus.VoiceNext/DiscordClientExtensions.cs @@ -1,93 +1,93 @@ -using System; -using System.Threading.Tasks; - -using DSharpPlus.Entities; -using DSharpPlus.Extensions; -using DSharpPlus.VoiceNext.Codec; -using Microsoft.Extensions.DependencyInjection; - -namespace DSharpPlus.VoiceNext; - -public static class DiscordClientExtensions -{ - /// - /// Registers a new VoiceNext client to the service collection. - /// - /// The service collection to register to. - /// Configuration for this VoiceNext client. - /// The same service collection for chaining - public static IServiceCollection AddVoiceNextExtension - ( - this IServiceCollection services, - VoiceNextConfiguration configuration - ) - { - Interop.InitializeLibsodium(); - - if (!Interop.IsAeadAes256GcmCompatible()) - { - throw new InvalidOperationException("The current hardware is not compatible with AEAD AES-256 GCM, a requirement for VoiceNext support."); - } - - services.ConfigureEventHandlers(b => b.AddEventHandlers()) - .AddSingleton(provider => - { - DiscordClient client = provider.GetRequiredService(); - - VoiceNextExtension extension = new(configuration ?? new()); - extension.Setup(client); - - return extension; - }); - - return services; - } - - /// - /// Registers a new VoiceNext client to the specified client builder. - /// - /// The builder to register to. - /// Configuration for this VoiceNext client. - /// The same builder for chaining - public static DiscordClientBuilder UseVoiceNext - ( - this DiscordClientBuilder builder, - VoiceNextConfiguration configuration - ) - => builder.ConfigureServices(services => services.AddVoiceNextExtension(configuration)); - - /// - /// Connects to this voice channel using VoiceNext. - /// - /// Channel to connect to. - /// If successful, the VoiceNext connection. - public static Task ConnectAsync(this DiscordChannel channel) - { - if (channel == null) - { - throw new NullReferenceException(); - } - - if (channel.Guild == null) - { - throw new InvalidOperationException("VoiceNext can only be used with guild channels."); - } - - if (channel.Type is not DiscordChannelType.Voice and not DiscordChannelType.Stage) - { - throw new InvalidOperationException("You can only connect to voice or stage channels."); - } - - if (channel.Discord is not DiscordClient discord || discord == null) - { - throw new NullReferenceException(); - } - - VoiceNextExtension vnext = discord.ServiceProvider.GetService() - ?? throw new InvalidOperationException("VoiceNext is not initialized for this Discord client."); - VoiceNextConnection? vnc = vnext.GetConnection(channel.Guild); - return vnc != null - ? throw new InvalidOperationException("VoiceNext is already connected in this guild.") - : vnext.ConnectAsync(channel); - } -} +using System; +using System.Threading.Tasks; + +using DSharpPlus.Entities; +using DSharpPlus.Extensions; +using DSharpPlus.VoiceNext.Codec; +using Microsoft.Extensions.DependencyInjection; + +namespace DSharpPlus.VoiceNext; + +public static class DiscordClientExtensions +{ + /// + /// Registers a new VoiceNext client to the service collection. + /// + /// The service collection to register to. + /// Configuration for this VoiceNext client. + /// The same service collection for chaining + public static IServiceCollection AddVoiceNextExtension + ( + this IServiceCollection services, + VoiceNextConfiguration configuration + ) + { + Interop.InitializeLibsodium(); + + if (!Interop.IsAeadAes256GcmCompatible()) + { + throw new InvalidOperationException("The current hardware is not compatible with AEAD AES-256 GCM, a requirement for VoiceNext support."); + } + + services.ConfigureEventHandlers(b => b.AddEventHandlers()) + .AddSingleton(provider => + { + DiscordClient client = provider.GetRequiredService(); + + VoiceNextExtension extension = new(configuration ?? new()); + extension.Setup(client); + + return extension; + }); + + return services; + } + + /// + /// Registers a new VoiceNext client to the specified client builder. + /// + /// The builder to register to. + /// Configuration for this VoiceNext client. + /// The same builder for chaining + public static DiscordClientBuilder UseVoiceNext + ( + this DiscordClientBuilder builder, + VoiceNextConfiguration configuration + ) + => builder.ConfigureServices(services => services.AddVoiceNextExtension(configuration)); + + /// + /// Connects to this voice channel using VoiceNext. + /// + /// Channel to connect to. + /// If successful, the VoiceNext connection. + public static Task ConnectAsync(this DiscordChannel channel) + { + if (channel == null) + { + throw new NullReferenceException(); + } + + if (channel.Guild == null) + { + throw new InvalidOperationException("VoiceNext can only be used with guild channels."); + } + + if (channel.Type is not DiscordChannelType.Voice and not DiscordChannelType.Stage) + { + throw new InvalidOperationException("You can only connect to voice or stage channels."); + } + + if (channel.Discord is not DiscordClient discord || discord == null) + { + throw new NullReferenceException(); + } + + VoiceNextExtension vnext = discord.ServiceProvider.GetService() + ?? throw new InvalidOperationException("VoiceNext is not initialized for this Discord client."); + VoiceNextConnection? vnc = vnext.GetConnection(channel.Guild); + return vnc != null + ? throw new InvalidOperationException("VoiceNext is already connected in this guild.") + : vnext.ConnectAsync(channel); + } +} diff --git a/DSharpPlus.VoiceNext/Entities/AudioSender.cs b/DSharpPlus.VoiceNext/Entities/AudioSender.cs index 914c4ab021..ae43e1635b 100644 --- a/DSharpPlus.VoiceNext/Entities/AudioSender.cs +++ b/DSharpPlus.VoiceNext/Entities/AudioSender.cs @@ -1,112 +1,112 @@ -using System; -using DSharpPlus.Entities; -using DSharpPlus.VoiceNext.Codec; - -namespace DSharpPlus.VoiceNext.Entities; - -internal class AudioSender : IDisposable -{ - // starting the counter a full wrap ahead handles an edge case where the VERY first packets - // we see are right around the wraparound line. - private ulong sequenceBase = 1 << 16; - private SequenceWrapState currentSequenceWrapState = SequenceWrapState.AssumeNextHighSequenceIsOutOfOrder; - - private enum SequenceWrapState - { - Normal, - AssumeNextLowSequenceIsOverflow, - AssumeNextHighSequenceIsOutOfOrder, - } - - public uint SSRC { get; } - public ulong Id => this.User?.Id ?? 0; - public OpusDecoder Decoder { get; } - public DiscordUser User { get; set; } = null; - public ulong? LastTrueSequence { get; set; } = null; - - public AudioSender(uint ssrc, OpusDecoder decoder) - { - this.SSRC = ssrc; - this.Decoder = decoder; - } - - public void Dispose() => this.Decoder?.Dispose(); - - /// - /// Accepts the 16-bit sequence number from the next RTP header in the associated stream and - /// uses heuristics to (attempt to) convert it into a 64-bit counter that takes into account - /// overflow wrapping around to zero. - /// - /// This method only works properly if it is called for every sequence number that we - /// see in the stream. - /// - /// - /// The 16-bit sequence number from the next RTP header. - /// - /// - /// Our best-effort guess of the value that would - /// have been, if the server had given us a 64-bit integer instead of a 16-bit one. - /// - public ulong GetTrueSequenceAfterWrapping(ushort originalSequence) - { - // section off a smallish zone at either end of the 16-bit integer range. whenever the - // sequence numbers creep into the higher zone, we start keeping an eye out for when - // sequence numbers suddenly start showing up in the lower zone. we expect this to mean - // that the sequence numbers overflowed and wrapped around. there's a bit of a balance - // when determining an appropriate size for the buffer zone: if it's too small, then a - // brief (but recoverable) network interruption could cause us to miss the lead-up to - // the overflow. on the other hand, if it's too large, then such a network interruption - // could cause us to misinterpret a normal sequence for one that's out-of-order. - // - // at 20 milliseconds per packet, 3,000 packets means that the buffer zone is one minute - // on either side. in other words, as long as we're getting packets delivered within a - // minute or so of when they should be, the 64-bit sequence numbers coming out of this - // method will be perfectly consistent with reality. - const ushort OverflowBufferZone = 3_000; - const ushort LowThreshold = OverflowBufferZone; - const ushort HighThreshold = ushort.MaxValue - OverflowBufferZone; - - ulong wrappingAdjustment = 0; - switch (this.currentSequenceWrapState) - { - case SequenceWrapState.Normal when originalSequence > HighThreshold: - // we were going about our business up to this point. the sequence numbers have - // gotten a bit high, so let's start looking out for any sequence numbers that - // are suddenly WAY lower than where they are right now. - this.currentSequenceWrapState = SequenceWrapState.AssumeNextLowSequenceIsOverflow; - break; - - case SequenceWrapState.AssumeNextLowSequenceIsOverflow when originalSequence < LowThreshold: - // we had seen some sequence numbers that got a bit high, and now we see this - // sequence number that's WAY lower than before. this is a classic sign that - // the sequence numbers have wrapped around. in order to present a consistently - // increasing "true" sequence number, add another 65,536 and keep counting. if - // we see another high sequence number in the near future, assume that it's a - // packet coming in out of order. - this.sequenceBase += 1 << 16; - this.currentSequenceWrapState = SequenceWrapState.AssumeNextHighSequenceIsOutOfOrder; - break; - - case SequenceWrapState.AssumeNextHighSequenceIsOutOfOrder when originalSequence > HighThreshold: - // we're seeing some high sequence numbers EITHER at the beginning of the stream - // OR very close to the time when we saw some very low sequence numbers. in the - // latter case, it happened because the packets came in out of order, right when - // the sequence numbers wrapped around. in the former case, we MIGHT be in the - // same kind of situation (we can't tell yet), so we err on the side of caution - // and burn a full cycle before we start counting so that we can handle both - // cases with the exact same adjustment. - wrappingAdjustment = 1 << 16; - break; - - case SequenceWrapState.AssumeNextHighSequenceIsOutOfOrder when originalSequence > LowThreshold: - // EITHER we're at the very beginning of the stream OR very close to the time - // when we saw some very low sequence numbers. either way, we're out of the - // zones where we should consider very low sequence numbers to come AFTER very - // high ones, so we can go back to normal now. - this.currentSequenceWrapState = SequenceWrapState.Normal; - break; - } - - return this.sequenceBase + originalSequence - wrappingAdjustment; - } -} +using System; +using DSharpPlus.Entities; +using DSharpPlus.VoiceNext.Codec; + +namespace DSharpPlus.VoiceNext.Entities; + +internal class AudioSender : IDisposable +{ + // starting the counter a full wrap ahead handles an edge case where the VERY first packets + // we see are right around the wraparound line. + private ulong sequenceBase = 1 << 16; + private SequenceWrapState currentSequenceWrapState = SequenceWrapState.AssumeNextHighSequenceIsOutOfOrder; + + private enum SequenceWrapState + { + Normal, + AssumeNextLowSequenceIsOverflow, + AssumeNextHighSequenceIsOutOfOrder, + } + + public uint SSRC { get; } + public ulong Id => this.User?.Id ?? 0; + public OpusDecoder Decoder { get; } + public DiscordUser User { get; set; } = null; + public ulong? LastTrueSequence { get; set; } = null; + + public AudioSender(uint ssrc, OpusDecoder decoder) + { + this.SSRC = ssrc; + this.Decoder = decoder; + } + + public void Dispose() => this.Decoder?.Dispose(); + + /// + /// Accepts the 16-bit sequence number from the next RTP header in the associated stream and + /// uses heuristics to (attempt to) convert it into a 64-bit counter that takes into account + /// overflow wrapping around to zero. + /// + /// This method only works properly if it is called for every sequence number that we + /// see in the stream. + /// + /// + /// The 16-bit sequence number from the next RTP header. + /// + /// + /// Our best-effort guess of the value that would + /// have been, if the server had given us a 64-bit integer instead of a 16-bit one. + /// + public ulong GetTrueSequenceAfterWrapping(ushort originalSequence) + { + // section off a smallish zone at either end of the 16-bit integer range. whenever the + // sequence numbers creep into the higher zone, we start keeping an eye out for when + // sequence numbers suddenly start showing up in the lower zone. we expect this to mean + // that the sequence numbers overflowed and wrapped around. there's a bit of a balance + // when determining an appropriate size for the buffer zone: if it's too small, then a + // brief (but recoverable) network interruption could cause us to miss the lead-up to + // the overflow. on the other hand, if it's too large, then such a network interruption + // could cause us to misinterpret a normal sequence for one that's out-of-order. + // + // at 20 milliseconds per packet, 3,000 packets means that the buffer zone is one minute + // on either side. in other words, as long as we're getting packets delivered within a + // minute or so of when they should be, the 64-bit sequence numbers coming out of this + // method will be perfectly consistent with reality. + const ushort OverflowBufferZone = 3_000; + const ushort LowThreshold = OverflowBufferZone; + const ushort HighThreshold = ushort.MaxValue - OverflowBufferZone; + + ulong wrappingAdjustment = 0; + switch (this.currentSequenceWrapState) + { + case SequenceWrapState.Normal when originalSequence > HighThreshold: + // we were going about our business up to this point. the sequence numbers have + // gotten a bit high, so let's start looking out for any sequence numbers that + // are suddenly WAY lower than where they are right now. + this.currentSequenceWrapState = SequenceWrapState.AssumeNextLowSequenceIsOverflow; + break; + + case SequenceWrapState.AssumeNextLowSequenceIsOverflow when originalSequence < LowThreshold: + // we had seen some sequence numbers that got a bit high, and now we see this + // sequence number that's WAY lower than before. this is a classic sign that + // the sequence numbers have wrapped around. in order to present a consistently + // increasing "true" sequence number, add another 65,536 and keep counting. if + // we see another high sequence number in the near future, assume that it's a + // packet coming in out of order. + this.sequenceBase += 1 << 16; + this.currentSequenceWrapState = SequenceWrapState.AssumeNextHighSequenceIsOutOfOrder; + break; + + case SequenceWrapState.AssumeNextHighSequenceIsOutOfOrder when originalSequence > HighThreshold: + // we're seeing some high sequence numbers EITHER at the beginning of the stream + // OR very close to the time when we saw some very low sequence numbers. in the + // latter case, it happened because the packets came in out of order, right when + // the sequence numbers wrapped around. in the former case, we MIGHT be in the + // same kind of situation (we can't tell yet), so we err on the side of caution + // and burn a full cycle before we start counting so that we can handle both + // cases with the exact same adjustment. + wrappingAdjustment = 1 << 16; + break; + + case SequenceWrapState.AssumeNextHighSequenceIsOutOfOrder when originalSequence > LowThreshold: + // EITHER we're at the very beginning of the stream OR very close to the time + // when we saw some very low sequence numbers. either way, we're out of the + // zones where we should consider very low sequence numbers to come AFTER very + // high ones, so we can go back to normal now. + this.currentSequenceWrapState = SequenceWrapState.Normal; + break; + } + + return this.sequenceBase + originalSequence - wrappingAdjustment; + } +} diff --git a/DSharpPlus.VoiceNext/Entities/VoiceDispatch.cs b/DSharpPlus.VoiceNext/Entities/VoiceDispatch.cs index cd86defd34..fae292c112 100644 --- a/DSharpPlus.VoiceNext/Entities/VoiceDispatch.cs +++ b/DSharpPlus.VoiceNext/Entities/VoiceDispatch.cs @@ -1,18 +1,18 @@ -using Newtonsoft.Json; - -namespace DSharpPlus.VoiceNext.Entities; - -internal sealed class VoiceDispatch -{ - [JsonProperty("op")] - public int OpCode { get; set; } - - [JsonProperty("d")] - public object Payload { get; set; } - - [JsonProperty("s", NullValueHandling = NullValueHandling.Ignore)] - public int? Sequence { get; set; } - - [JsonProperty("t", NullValueHandling = NullValueHandling.Ignore)] - public string EventName { get; set; } -} +using Newtonsoft.Json; + +namespace DSharpPlus.VoiceNext.Entities; + +internal sealed class VoiceDispatch +{ + [JsonProperty("op")] + public int OpCode { get; set; } + + [JsonProperty("d")] + public object Payload { get; set; } + + [JsonProperty("s", NullValueHandling = NullValueHandling.Ignore)] + public int? Sequence { get; set; } + + [JsonProperty("t", NullValueHandling = NullValueHandling.Ignore)] + public string EventName { get; set; } +} diff --git a/DSharpPlus.VoiceNext/Entities/VoiceIdentifyPayload.cs b/DSharpPlus.VoiceNext/Entities/VoiceIdentifyPayload.cs index 5ce0ebd279..4ccd8b1703 100644 --- a/DSharpPlus.VoiceNext/Entities/VoiceIdentifyPayload.cs +++ b/DSharpPlus.VoiceNext/Entities/VoiceIdentifyPayload.cs @@ -1,18 +1,18 @@ -using Newtonsoft.Json; - -namespace DSharpPlus.VoiceNext.Entities; - -internal sealed class VoiceIdentifyPayload -{ - [JsonProperty("server_id")] - public ulong ServerId { get; set; } - - [JsonProperty("user_id", NullValueHandling = NullValueHandling.Ignore)] - public ulong? UserId { get; set; } - - [JsonProperty("session_id")] - public string SessionId { get; set; } - - [JsonProperty("token")] - public string Token { get; set; } -} +using Newtonsoft.Json; + +namespace DSharpPlus.VoiceNext.Entities; + +internal sealed class VoiceIdentifyPayload +{ + [JsonProperty("server_id")] + public ulong ServerId { get; set; } + + [JsonProperty("user_id", NullValueHandling = NullValueHandling.Ignore)] + public ulong? UserId { get; set; } + + [JsonProperty("session_id")] + public string SessionId { get; set; } + + [JsonProperty("token")] + public string Token { get; set; } +} diff --git a/DSharpPlus.VoiceNext/Entities/VoicePacket.cs b/DSharpPlus.VoiceNext/Entities/VoicePacket.cs index 9d746032da..25002444b8 100644 --- a/DSharpPlus.VoiceNext/Entities/VoicePacket.cs +++ b/DSharpPlus.VoiceNext/Entities/VoicePacket.cs @@ -1,17 +1,17 @@ -using System; - -namespace DSharpPlus.VoiceNext.Entities; - -internal struct VoicePacket -{ - public ReadOnlyMemory Bytes { get; } - public int MillisecondDuration { get; } - public bool IsSilence { get; set; } - - public VoicePacket(ReadOnlyMemory bytes, int msDuration, bool isSilence = false) - { - this.Bytes = bytes; - this.MillisecondDuration = msDuration; - this.IsSilence = isSilence; - } -} +using System; + +namespace DSharpPlus.VoiceNext.Entities; + +internal struct VoicePacket +{ + public ReadOnlyMemory Bytes { get; } + public int MillisecondDuration { get; } + public bool IsSilence { get; set; } + + public VoicePacket(ReadOnlyMemory bytes, int msDuration, bool isSilence = false) + { + this.Bytes = bytes; + this.MillisecondDuration = msDuration; + this.IsSilence = isSilence; + } +} diff --git a/DSharpPlus.VoiceNext/Entities/VoiceReadyPayload.cs b/DSharpPlus.VoiceNext/Entities/VoiceReadyPayload.cs index 69a883610a..e3f0e4c737 100644 --- a/DSharpPlus.VoiceNext/Entities/VoiceReadyPayload.cs +++ b/DSharpPlus.VoiceNext/Entities/VoiceReadyPayload.cs @@ -1,22 +1,22 @@ -using System.Collections.Generic; -using Newtonsoft.Json; - -namespace DSharpPlus.VoiceNext.Entities; - -internal sealed class VoiceReadyPayload -{ - [JsonProperty("ssrc")] - public uint SSRC { get; set; } - - [JsonProperty("ip")] - public string Address { get; set; } - - [JsonProperty("port")] - public ushort Port { get; set; } - - [JsonProperty("modes")] - public IReadOnlyList Modes { get; set; } - - [JsonProperty("heartbeat_interval")] - public int HeartbeatInterval { get; set; } -} +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace DSharpPlus.VoiceNext.Entities; + +internal sealed class VoiceReadyPayload +{ + [JsonProperty("ssrc")] + public uint SSRC { get; set; } + + [JsonProperty("ip")] + public string Address { get; set; } + + [JsonProperty("port")] + public ushort Port { get; set; } + + [JsonProperty("modes")] + public IReadOnlyList Modes { get; set; } + + [JsonProperty("heartbeat_interval")] + public int HeartbeatInterval { get; set; } +} diff --git a/DSharpPlus.VoiceNext/Entities/VoiceSelectProtocolPayload.cs b/DSharpPlus.VoiceNext/Entities/VoiceSelectProtocolPayload.cs index 0b6008130d..06c5f89adc 100644 --- a/DSharpPlus.VoiceNext/Entities/VoiceSelectProtocolPayload.cs +++ b/DSharpPlus.VoiceNext/Entities/VoiceSelectProtocolPayload.cs @@ -1,12 +1,12 @@ -using Newtonsoft.Json; - -namespace DSharpPlus.VoiceNext.Entities; - -internal sealed class VoiceSelectProtocolPayload -{ - [JsonProperty("protocol")] - public string Protocol { get; set; } - - [JsonProperty("data")] - public VoiceSelectProtocolPayloadData Data { get; set; } -} +using Newtonsoft.Json; + +namespace DSharpPlus.VoiceNext.Entities; + +internal sealed class VoiceSelectProtocolPayload +{ + [JsonProperty("protocol")] + public string Protocol { get; set; } + + [JsonProperty("data")] + public VoiceSelectProtocolPayloadData Data { get; set; } +} diff --git a/DSharpPlus.VoiceNext/Entities/VoiceSelectProtocolPayloadData.cs b/DSharpPlus.VoiceNext/Entities/VoiceSelectProtocolPayloadData.cs index 21ef63d7b5..5c4d6a7596 100644 --- a/DSharpPlus.VoiceNext/Entities/VoiceSelectProtocolPayloadData.cs +++ b/DSharpPlus.VoiceNext/Entities/VoiceSelectProtocolPayloadData.cs @@ -1,15 +1,15 @@ -using Newtonsoft.Json; - -namespace DSharpPlus.VoiceNext.Entities; - -internal class VoiceSelectProtocolPayloadData -{ - [JsonProperty("address")] - public string Address { get; set; } - - [JsonProperty("port")] - public ushort Port { get; set; } - - [JsonProperty("mode")] - public string Mode { get; set; } -} +using Newtonsoft.Json; + +namespace DSharpPlus.VoiceNext.Entities; + +internal class VoiceSelectProtocolPayloadData +{ + [JsonProperty("address")] + public string Address { get; set; } + + [JsonProperty("port")] + public ushort Port { get; set; } + + [JsonProperty("mode")] + public string Mode { get; set; } +} diff --git a/DSharpPlus.VoiceNext/Entities/VoiceServerUpdatePayload.cs b/DSharpPlus.VoiceNext/Entities/VoiceServerUpdatePayload.cs index 6fe4a65a79..56dd5af890 100644 --- a/DSharpPlus.VoiceNext/Entities/VoiceServerUpdatePayload.cs +++ b/DSharpPlus.VoiceNext/Entities/VoiceServerUpdatePayload.cs @@ -1,15 +1,15 @@ -using Newtonsoft.Json; - -namespace DSharpPlus.VoiceNext.Entities; - -internal sealed class VoiceServerUpdatePayload -{ - [JsonProperty("token")] - public string Token { get; set; } - - [JsonProperty("guild_id")] - public ulong GuildId { get; set; } - - [JsonProperty("endpoint")] - public string Endpoint { get; set; } -} +using Newtonsoft.Json; + +namespace DSharpPlus.VoiceNext.Entities; + +internal sealed class VoiceServerUpdatePayload +{ + [JsonProperty("token")] + public string Token { get; set; } + + [JsonProperty("guild_id")] + public ulong GuildId { get; set; } + + [JsonProperty("endpoint")] + public string Endpoint { get; set; } +} diff --git a/DSharpPlus.VoiceNext/Entities/VoiceSessionDescriptionPayload.cs b/DSharpPlus.VoiceNext/Entities/VoiceSessionDescriptionPayload.cs index 95e230f8fd..5fb658c010 100644 --- a/DSharpPlus.VoiceNext/Entities/VoiceSessionDescriptionPayload.cs +++ b/DSharpPlus.VoiceNext/Entities/VoiceSessionDescriptionPayload.cs @@ -1,12 +1,12 @@ -using Newtonsoft.Json; - -namespace DSharpPlus.VoiceNext.Entities; - -internal sealed class VoiceSessionDescriptionPayload -{ - [JsonProperty("secret_key")] - public byte[] SecretKey { get; set; } - - [JsonProperty("mode")] - public string Mode { get; set; } -} +using Newtonsoft.Json; + +namespace DSharpPlus.VoiceNext.Entities; + +internal sealed class VoiceSessionDescriptionPayload +{ + [JsonProperty("secret_key")] + public byte[] SecretKey { get; set; } + + [JsonProperty("mode")] + public string Mode { get; set; } +} diff --git a/DSharpPlus.VoiceNext/Entities/VoiceSpeakingPayload.cs b/DSharpPlus.VoiceNext/Entities/VoiceSpeakingPayload.cs index 3ce3b034c2..522437339a 100644 --- a/DSharpPlus.VoiceNext/Entities/VoiceSpeakingPayload.cs +++ b/DSharpPlus.VoiceNext/Entities/VoiceSpeakingPayload.cs @@ -1,18 +1,18 @@ -using Newtonsoft.Json; - -namespace DSharpPlus.VoiceNext.Entities; - -internal sealed class VoiceSpeakingPayload -{ - [JsonProperty("speaking")] - public bool Speaking { get; set; } - - [JsonProperty("delay", NullValueHandling = NullValueHandling.Ignore)] - public int? Delay { get; set; } - - [JsonProperty("ssrc", NullValueHandling = NullValueHandling.Ignore)] - public uint? SSRC { get; set; } - - [JsonProperty("user_id", NullValueHandling = NullValueHandling.Ignore)] - public ulong? UserId { get; set; } -} +using Newtonsoft.Json; + +namespace DSharpPlus.VoiceNext.Entities; + +internal sealed class VoiceSpeakingPayload +{ + [JsonProperty("speaking")] + public bool Speaking { get; set; } + + [JsonProperty("delay", NullValueHandling = NullValueHandling.Ignore)] + public int? Delay { get; set; } + + [JsonProperty("ssrc", NullValueHandling = NullValueHandling.Ignore)] + public uint? SSRC { get; set; } + + [JsonProperty("user_id", NullValueHandling = NullValueHandling.Ignore)] + public ulong? UserId { get; set; } +} diff --git a/DSharpPlus.VoiceNext/Entities/VoiceStateUpdatePayload.cs b/DSharpPlus.VoiceNext/Entities/VoiceStateUpdatePayload.cs index 6532fce858..f4c665090b 100644 --- a/DSharpPlus.VoiceNext/Entities/VoiceStateUpdatePayload.cs +++ b/DSharpPlus.VoiceNext/Entities/VoiceStateUpdatePayload.cs @@ -1,24 +1,24 @@ -using Newtonsoft.Json; - -namespace DSharpPlus.VoiceNext.Entities; - -internal sealed class VoiceStateUpdatePayload -{ - [JsonProperty("guild_id")] - public ulong GuildId { get; set; } - - [JsonProperty("channel_id")] - public ulong? ChannelId { get; set; } - - [JsonProperty("user_id", NullValueHandling = NullValueHandling.Ignore)] - public ulong? UserId { get; set; } - - [JsonProperty("session_id", NullValueHandling = NullValueHandling.Ignore)] - public string SessionId { get; set; } - - [JsonProperty("self_deaf")] - public bool Deafened { get; set; } - - [JsonProperty("self_mute")] - public bool Muted { get; set; } -} +using Newtonsoft.Json; + +namespace DSharpPlus.VoiceNext.Entities; + +internal sealed class VoiceStateUpdatePayload +{ + [JsonProperty("guild_id")] + public ulong GuildId { get; set; } + + [JsonProperty("channel_id")] + public ulong? ChannelId { get; set; } + + [JsonProperty("user_id", NullValueHandling = NullValueHandling.Ignore)] + public ulong? UserId { get; set; } + + [JsonProperty("session_id", NullValueHandling = NullValueHandling.Ignore)] + public string SessionId { get; set; } + + [JsonProperty("self_deaf")] + public bool Deafened { get; set; } + + [JsonProperty("self_mute")] + public bool Muted { get; set; } +} diff --git a/DSharpPlus.VoiceNext/Entities/VoiceUserJoinPayload.cs b/DSharpPlus.VoiceNext/Entities/VoiceUserJoinPayload.cs index b17a156ed6..23e10b5c73 100644 --- a/DSharpPlus.VoiceNext/Entities/VoiceUserJoinPayload.cs +++ b/DSharpPlus.VoiceNext/Entities/VoiceUserJoinPayload.cs @@ -1,12 +1,12 @@ -using Newtonsoft.Json; - -namespace DSharpPlus.VoiceNext.Entities; - -internal sealed class VoiceUserJoinPayload -{ - [JsonProperty("user_id")] - public ulong UserId { get; private set; } - - [JsonProperty("audio_ssrc")] - public uint SSRC { get; private set; } -} +using Newtonsoft.Json; + +namespace DSharpPlus.VoiceNext.Entities; + +internal sealed class VoiceUserJoinPayload +{ + [JsonProperty("user_id")] + public ulong UserId { get; private set; } + + [JsonProperty("audio_ssrc")] + public uint SSRC { get; private set; } +} diff --git a/DSharpPlus.VoiceNext/Entities/VoiceUserLeavePayload.cs b/DSharpPlus.VoiceNext/Entities/VoiceUserLeavePayload.cs index de3b164d29..3229d6f386 100644 --- a/DSharpPlus.VoiceNext/Entities/VoiceUserLeavePayload.cs +++ b/DSharpPlus.VoiceNext/Entities/VoiceUserLeavePayload.cs @@ -1,9 +1,9 @@ -using Newtonsoft.Json; - -namespace DSharpPlus.VoiceNext.Entities; - -internal sealed class VoiceUserLeavePayload -{ - [JsonProperty("user_id")] - public ulong UserId { get; set; } -} +using Newtonsoft.Json; + +namespace DSharpPlus.VoiceNext.Entities; + +internal sealed class VoiceUserLeavePayload +{ + [JsonProperty("user_id")] + public ulong UserId { get; set; } +} diff --git a/DSharpPlus.VoiceNext/EventArgs/VoiceReceiveEventArgs.cs b/DSharpPlus.VoiceNext/EventArgs/VoiceReceiveEventArgs.cs index 11984f8cc4..5fb72babd3 100644 --- a/DSharpPlus.VoiceNext/EventArgs/VoiceReceiveEventArgs.cs +++ b/DSharpPlus.VoiceNext/EventArgs/VoiceReceiveEventArgs.cs @@ -1,50 +1,50 @@ -using System; -using DSharpPlus.Entities; -using DSharpPlus.EventArgs; - -namespace DSharpPlus.VoiceNext.EventArgs; - -/// -/// Represents arguments for VoiceReceived events. -/// -public class VoiceReceiveEventArgs : DiscordEventArgs -{ - /// - /// Gets the SSRC of the audio source. - /// - public uint SSRC { get; internal set; } - -#pragma warning disable CS8632 - - /// - /// Gets the user that sent the audio data. - /// - public DiscordUser? User { get; internal set; } - -#pragma warning restore - - /// - /// Gets the received voice data, decoded to PCM format. - /// - public ReadOnlyMemory PcmData { get; internal set; } - - /// - /// Gets the received voice data, in Opus format. Note that for packets that were lost and/or compensated for, this will be empty. - /// - public ReadOnlyMemory OpusData { get; internal set; } - - /// - /// Gets the format of the received PCM data. - /// - /// Important: This isn't always the format set in , and depends on the audio data received. - /// - /// - public AudioFormat AudioFormat { get; internal set; } - - /// - /// Gets the millisecond duration of the PCM audio sample. - /// - public int AudioDuration { get; internal set; } - - internal VoiceReceiveEventArgs() : base() { } -} +using System; +using DSharpPlus.Entities; +using DSharpPlus.EventArgs; + +namespace DSharpPlus.VoiceNext.EventArgs; + +/// +/// Represents arguments for VoiceReceived events. +/// +public class VoiceReceiveEventArgs : DiscordEventArgs +{ + /// + /// Gets the SSRC of the audio source. + /// + public uint SSRC { get; internal set; } + +#pragma warning disable CS8632 + + /// + /// Gets the user that sent the audio data. + /// + public DiscordUser? User { get; internal set; } + +#pragma warning restore + + /// + /// Gets the received voice data, decoded to PCM format. + /// + public ReadOnlyMemory PcmData { get; internal set; } + + /// + /// Gets the received voice data, in Opus format. Note that for packets that were lost and/or compensated for, this will be empty. + /// + public ReadOnlyMemory OpusData { get; internal set; } + + /// + /// Gets the format of the received PCM data. + /// + /// Important: This isn't always the format set in , and depends on the audio data received. + /// + /// + public AudioFormat AudioFormat { get; internal set; } + + /// + /// Gets the millisecond duration of the PCM audio sample. + /// + public int AudioDuration { get; internal set; } + + internal VoiceReceiveEventArgs() : base() { } +} diff --git a/DSharpPlus.VoiceNext/EventArgs/VoiceUserJoinEventArgs.cs b/DSharpPlus.VoiceNext/EventArgs/VoiceUserJoinEventArgs.cs index 25d05f49d5..84740d7efc 100644 --- a/DSharpPlus.VoiceNext/EventArgs/VoiceUserJoinEventArgs.cs +++ b/DSharpPlus.VoiceNext/EventArgs/VoiceUserJoinEventArgs.cs @@ -1,22 +1,22 @@ -using DSharpPlus.Entities; -using DSharpPlus.EventArgs; - -namespace DSharpPlus.VoiceNext.EventArgs; - -/// -/// Arguments for . -/// -public sealed class VoiceUserJoinEventArgs : DiscordEventArgs -{ - /// - /// Gets the user who left. - /// - public DiscordUser User { get; internal set; } - - /// - /// Gets the SSRC of the user who joined. - /// - public uint SSRC { get; internal set; } - - internal VoiceUserJoinEventArgs() : base() { } -} +using DSharpPlus.Entities; +using DSharpPlus.EventArgs; + +namespace DSharpPlus.VoiceNext.EventArgs; + +/// +/// Arguments for . +/// +public sealed class VoiceUserJoinEventArgs : DiscordEventArgs +{ + /// + /// Gets the user who left. + /// + public DiscordUser User { get; internal set; } + + /// + /// Gets the SSRC of the user who joined. + /// + public uint SSRC { get; internal set; } + + internal VoiceUserJoinEventArgs() : base() { } +} diff --git a/DSharpPlus.VoiceNext/EventArgs/VoiceUserLeaveEventArgs.cs b/DSharpPlus.VoiceNext/EventArgs/VoiceUserLeaveEventArgs.cs index 54a6b27955..0db81d2196 100644 --- a/DSharpPlus.VoiceNext/EventArgs/VoiceUserLeaveEventArgs.cs +++ b/DSharpPlus.VoiceNext/EventArgs/VoiceUserLeaveEventArgs.cs @@ -1,22 +1,22 @@ -using DSharpPlus.Entities; -using DSharpPlus.EventArgs; - -namespace DSharpPlus.VoiceNext.EventArgs; - -/// -/// Arguments for . -/// -public sealed class VoiceUserLeaveEventArgs : DiscordEventArgs -{ - /// - /// Gets the user who left. - /// - public DiscordUser User { get; internal set; } - - /// - /// Gets the SSRC of the user who left. - /// - public uint SSRC { get; internal set; } - - internal VoiceUserLeaveEventArgs() : base() { } -} +using DSharpPlus.Entities; +using DSharpPlus.EventArgs; + +namespace DSharpPlus.VoiceNext.EventArgs; + +/// +/// Arguments for . +/// +public sealed class VoiceUserLeaveEventArgs : DiscordEventArgs +{ + /// + /// Gets the user who left. + /// + public DiscordUser User { get; internal set; } + + /// + /// Gets the SSRC of the user who left. + /// + public uint SSRC { get; internal set; } + + internal VoiceUserLeaveEventArgs() : base() { } +} diff --git a/DSharpPlus.VoiceNext/IVoiceFilter.cs b/DSharpPlus.VoiceNext/IVoiceFilter.cs index 44915b9367..0f38238ab7 100644 --- a/DSharpPlus.VoiceNext/IVoiceFilter.cs +++ b/DSharpPlus.VoiceNext/IVoiceFilter.cs @@ -1,17 +1,17 @@ -using System; - -namespace DSharpPlus.VoiceNext; - -/// -/// Represents a filter for PCM data. PCM data submitted through a will be sent through all installed instances of first. -/// -public interface IVoiceFilter -{ - /// - /// Transforms the supplied PCM data using this filter. - /// - /// PCM data to transform. The transformation happens in-place. - /// Format of the supplied PCM data. - /// Millisecond duration of the supplied PCM data. - public void Transform(Span pcmData, AudioFormat pcmFormat, int duration); -} +using System; + +namespace DSharpPlus.VoiceNext; + +/// +/// Represents a filter for PCM data. PCM data submitted through a will be sent through all installed instances of first. +/// +public interface IVoiceFilter +{ + /// + /// Transforms the supplied PCM data using this filter. + /// + /// PCM data to transform. The transformation happens in-place. + /// Format of the supplied PCM data. + /// Millisecond duration of the supplied PCM data. + public void Transform(Span pcmData, AudioFormat pcmFormat, int duration); +} diff --git a/DSharpPlus.VoiceNext/RawVoicePacket.cs b/DSharpPlus.VoiceNext/RawVoicePacket.cs index b446f06bbd..558658aa01 100644 --- a/DSharpPlus.VoiceNext/RawVoicePacket.cs +++ b/DSharpPlus.VoiceNext/RawVoicePacket.cs @@ -1,23 +1,23 @@ -using System; - -namespace DSharpPlus.VoiceNext; - -internal readonly struct RawVoicePacket -{ - public RawVoicePacket(Memory bytes, int duration, bool silence) - { - this.Bytes = bytes; - this.Duration = duration; - this.Silence = silence; - this.RentedBuffer = null; - } - - public RawVoicePacket(Memory bytes, int duration, bool silence, byte[] rentedBuffer) - : this(bytes, duration, silence) => this.RentedBuffer = rentedBuffer; - - public readonly Memory Bytes; - public readonly int Duration; - public readonly bool Silence; - - public readonly byte[] RentedBuffer; -} +using System; + +namespace DSharpPlus.VoiceNext; + +internal readonly struct RawVoicePacket +{ + public RawVoicePacket(Memory bytes, int duration, bool silence) + { + this.Bytes = bytes; + this.Duration = duration; + this.Silence = silence; + this.RentedBuffer = null; + } + + public RawVoicePacket(Memory bytes, int duration, bool silence, byte[] rentedBuffer) + : this(bytes, duration, silence) => this.RentedBuffer = rentedBuffer; + + public readonly Memory Bytes; + public readonly int Duration; + public readonly bool Silence; + + public readonly byte[] RentedBuffer; +} diff --git a/DSharpPlus.VoiceNext/StreamExtensions.cs b/DSharpPlus.VoiceNext/StreamExtensions.cs index 3a51c0dd10..91b55ba1e8 100644 --- a/DSharpPlus.VoiceNext/StreamExtensions.cs +++ b/DSharpPlus.VoiceNext/StreamExtensions.cs @@ -1,47 +1,47 @@ -using System; -using System.Buffers; -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace DSharpPlus.VoiceNext; - -public static class StreamExtensions -{ - /// - /// Asynchronously reads the bytes from the current stream and writes them to the specified . - /// - /// The source - /// The target - /// The size, in bytes, of the buffer. This value must be greater than zero. If , defaults to the packet size specified by . - /// The token to monitor for cancellation requests. - /// - public static async Task CopyToAsync(this Stream source, VoiceTransmitSink destination, int? bufferSize = null, CancellationToken cancellationToken = default) - { - // adapted from CoreFX - // https://source.dot.net/#System.Private.CoreLib/Stream.cs,8048a9680abdd13b - - ArgumentNullException.ThrowIfNull(source); - ArgumentNullException.ThrowIfNull(destination); - - if (bufferSize is not null and <= 0) - { - throw new ArgumentOutOfRangeException(nameof(bufferSize), bufferSize, "bufferSize cannot be less than or equal to zero"); - } - - int bufferLength = bufferSize ?? destination.SampleLength; - byte[] buffer = ArrayPool.Shared.Rent(bufferLength); - try - { - int bytesRead; - while ((bytesRead = await source.ReadAsync(buffer.AsMemory(0, bufferLength), cancellationToken)) != 0) - { - await destination.WriteAsync(new ReadOnlyMemory(buffer, 0, bytesRead), cancellationToken); - } - } - finally - { - ArrayPool.Shared.Return(buffer); - } - } -} +using System; +using System.Buffers; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace DSharpPlus.VoiceNext; + +public static class StreamExtensions +{ + /// + /// Asynchronously reads the bytes from the current stream and writes them to the specified . + /// + /// The source + /// The target + /// The size, in bytes, of the buffer. This value must be greater than zero. If , defaults to the packet size specified by . + /// The token to monitor for cancellation requests. + /// + public static async Task CopyToAsync(this Stream source, VoiceTransmitSink destination, int? bufferSize = null, CancellationToken cancellationToken = default) + { + // adapted from CoreFX + // https://source.dot.net/#System.Private.CoreLib/Stream.cs,8048a9680abdd13b + + ArgumentNullException.ThrowIfNull(source); + ArgumentNullException.ThrowIfNull(destination); + + if (bufferSize is not null and <= 0) + { + throw new ArgumentOutOfRangeException(nameof(bufferSize), bufferSize, "bufferSize cannot be less than or equal to zero"); + } + + int bufferLength = bufferSize ?? destination.SampleLength; + byte[] buffer = ArrayPool.Shared.Rent(bufferLength); + try + { + int bytesRead; + while ((bytesRead = await source.ReadAsync(buffer.AsMemory(0, bufferLength), cancellationToken)) != 0) + { + await destination.WriteAsync(new ReadOnlyMemory(buffer, 0, bytesRead), cancellationToken); + } + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } +} diff --git a/DSharpPlus.VoiceNext/VoiceApplication.cs b/DSharpPlus.VoiceNext/VoiceApplication.cs index 4f07f8fc6e..400c91a6da 100644 --- a/DSharpPlus.VoiceNext/VoiceApplication.cs +++ b/DSharpPlus.VoiceNext/VoiceApplication.cs @@ -1,23 +1,23 @@ -namespace DSharpPlus.VoiceNext; - - -/// -/// Represents encoder settings preset for Opus. -/// -public enum VoiceApplication : int -{ - /// - /// Defines that the encoder must optimize settings for voice data. - /// - Voice = 2048, - - /// - /// Defines that the encoder must optimize settings for music data. - /// - Music = 2049, - - /// - /// Defines that the encoder must optimize settings for low latency applications. - /// - LowLatency = 2051 -} +namespace DSharpPlus.VoiceNext; + + +/// +/// Represents encoder settings preset for Opus. +/// +public enum VoiceApplication : int +{ + /// + /// Defines that the encoder must optimize settings for voice data. + /// + Voice = 2048, + + /// + /// Defines that the encoder must optimize settings for music data. + /// + Music = 2049, + + /// + /// Defines that the encoder must optimize settings for low latency applications. + /// + LowLatency = 2051 +} diff --git a/DSharpPlus.VoiceNext/VoiceNextConfiguration.cs b/DSharpPlus.VoiceNext/VoiceNextConfiguration.cs index adb006511e..a563039319 100644 --- a/DSharpPlus.VoiceNext/VoiceNextConfiguration.cs +++ b/DSharpPlus.VoiceNext/VoiceNextConfiguration.cs @@ -1,42 +1,42 @@ -namespace DSharpPlus.VoiceNext; - - -/// -/// VoiceNext client configuration. -/// -public sealed class VoiceNextConfiguration -{ - /// - /// Sets the audio format for Opus. This will determine the quality of the audio output. - /// Defaults to . - /// - public AudioFormat AudioFormat { internal get; set; } = AudioFormat.Default; - - /// - /// Sets whether incoming voice receiver should be enabled. - /// Defaults to false. - /// - public bool EnableIncoming { internal get; set; } = false; - - /// - /// Sets the size of the packet queue. - /// Defaults to 25 or ~500ms. - /// - public int PacketQueueSize { internal get; set; } = 25; - - /// - /// Creates a new instance of . - /// - public VoiceNextConfiguration() { } - - /// - /// Creates a new instance of , copying the properties of another configuration. - /// - /// Configuration the properties of which are to be copied. - public VoiceNextConfiguration(VoiceNextConfiguration other) - { - this.AudioFormat = new AudioFormat(other.AudioFormat.SampleRate, other.AudioFormat.ChannelCount, other.AudioFormat.VoiceApplication); - this.EnableIncoming = other.EnableIncoming; - this.PacketQueueSize = other.PacketQueueSize; - } -} +namespace DSharpPlus.VoiceNext; + + +/// +/// VoiceNext client configuration. +/// +public sealed class VoiceNextConfiguration +{ + /// + /// Sets the audio format for Opus. This will determine the quality of the audio output. + /// Defaults to . + /// + public AudioFormat AudioFormat { internal get; set; } = AudioFormat.Default; + + /// + /// Sets whether incoming voice receiver should be enabled. + /// Defaults to false. + /// + public bool EnableIncoming { internal get; set; } = false; + + /// + /// Sets the size of the packet queue. + /// Defaults to 25 or ~500ms. + /// + public int PacketQueueSize { internal get; set; } = 25; + + /// + /// Creates a new instance of . + /// + public VoiceNextConfiguration() { } + + /// + /// Creates a new instance of , copying the properties of another configuration. + /// + /// Configuration the properties of which are to be copied. + public VoiceNextConfiguration(VoiceNextConfiguration other) + { + this.AudioFormat = new AudioFormat(other.AudioFormat.SampleRate, other.AudioFormat.ChannelCount, other.AudioFormat.VoiceApplication); + this.EnableIncoming = other.EnableIncoming; + this.PacketQueueSize = other.PacketQueueSize; + } +} diff --git a/DSharpPlus.VoiceNext/VoiceNextConnection.cs b/DSharpPlus.VoiceNext/VoiceNextConnection.cs index 57df1e0667..872b20f33c 100644 --- a/DSharpPlus.VoiceNext/VoiceNextConnection.cs +++ b/DSharpPlus.VoiceNext/VoiceNextConnection.cs @@ -1,1112 +1,1112 @@ -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.Threading; -using System.Threading.Channels; -using System.Threading.Tasks; -using DSharpPlus.AsyncEvents; -using DSharpPlus.Entities; -using DSharpPlus.EventArgs; -using DSharpPlus.Net; -using DSharpPlus.Net.Serialization; -using DSharpPlus.Net.Udp; -using DSharpPlus.Net.WebSocket; -using DSharpPlus.VoiceNext.Codec; -using DSharpPlus.VoiceNext.Entities; -using DSharpPlus.VoiceNext.EventArgs; -using Microsoft.Extensions.Logging; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace DSharpPlus.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; - - private static DateTimeOffset UnixEpoch { get; } = new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero); - - private DiscordClient Discord { get; } - private DiscordGuild Guild { get; } - private ConcurrentDictionary TransmittingSSRCs { get; } - - private BaseUdpClient UdpClient { get; } - private IWebSocketClient VoiceWs { get; set; } - private Task HeartbeatTask { get; set; } - private int HeartbeatInterval { get; set; } - private DateTimeOffset LastHeartbeat { get; set; } - - private CancellationTokenSource TokenSource { get; set; } - private CancellationToken Token - => this.TokenSource.Token; - - internal VoiceServerUpdatePayload ServerData { get; set; } - internal VoiceStateUpdatePayload StateData { get; set; } - internal bool Resume { get; set; } - - private VoiceNextConfiguration Configuration { get; } - private Opus Opus { get; set; } - private Sodium Sodium { get; set; } - private Rtp Rtp { get; set; } - private EncryptionMode SelectedEncryptionMode { get; set; } - private uint Nonce { get; set; } = 0; - - private ushort Sequence { get; set; } - private uint Timestamp { get; set; } - private uint SSRC { get; set; } - private byte[] Key { get; set; } - private IpEndpoint DiscoveredEndpoint { get; set; } - internal ConnectionEndpoint WebSocketEndpoint { get; set; } - internal ConnectionEndpoint UdpEndpoint { get; set; } - - private TaskCompletionSource ReadyWait { get; set; } - private bool IsInitialized { get; set; } - private bool IsDisposed { get; set; } - - private TaskCompletionSource PlayingWait { get; set; } - - private AsyncManualResetEvent PauseEvent { get; } - private VoiceTransmitSink TransmitStream { get; set; } - private Channel TransmitChannel { get; } - private ConcurrentDictionary KeepaliveTimestamps { get; } - private ulong lastKeepalive = 0; - - private Task SenderTask { get; set; } - private CancellationTokenSource SenderTokenSource { get; set; } - private CancellationToken SenderToken - => this.SenderTokenSource.Token; - - private Task ReceiverTask { get; set; } - private CancellationTokenSource ReceiverTokenSource { get; set; } - private CancellationToken ReceiverToken - => this.ReceiverTokenSource.Token; - - private Task KeepaliveTask { get; set; } - private CancellationTokenSource KeepaliveTokenSource { get; set; } - private CancellationToken KeepaliveToken - => this.KeepaliveTokenSource.Token; - - private volatile bool isSpeaking = false; - - /// - /// Gets the audio format used by the Opus encoder. - /// - public AudioFormat AudioFormat => this.Configuration.AudioFormat; - - /// - /// Gets whether this connection is still playing audio. - /// - public bool IsPlaying - => this.PlayingWait != null && !this.PlayingWait.Task.IsCompleted; - - /// - /// Gets the websocket round-trip time in ms. - /// - public int WebSocketPing - => Volatile.Read(ref this.wsPing); - private int wsPing = 0; - - /// - /// 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; } - - internal VoiceNextConnection(DiscordClient client, DiscordGuild guild, DiscordChannel channel, VoiceNextConfiguration config, VoiceServerUpdatePayload server, VoiceStateUpdatePayload state) - { - this.Discord = client; - this.Guild = guild; - this.TargetChannel = channel; - this.TransmittingSSRCs = new ConcurrentDictionary(); - - DefaultClientErrorHandler errorHandler = new(client.Logger); - - this.userSpeaking = new AsyncEvent(errorHandler); - this.userJoined = new AsyncEvent(errorHandler); - this.userLeft = new AsyncEvent(errorHandler); - this.voiceReceived = new AsyncEvent(errorHandler); - this.voiceSocketError = new AsyncEvent(errorHandler); - this.TokenSource = new CancellationTokenSource(); - - this.Configuration = config; - this.Opus = new Opus(this.AudioFormat); - //this.Sodium = new Sodium(); - this.Rtp = new Rtp(); - - this.ServerData = server; - this.StateData = state; - - string eps = this.ServerData.Endpoint; - int epi = eps.LastIndexOf(':'); - string eph = string.Empty; - int 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.IsInitialized = false; - this.IsDisposed = false; - - 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 = new WebSocketClient(errorHandler); - this.VoiceWs.Disconnected += VoiceWS_SocketClosedAsync; - this.VoiceWs.MessageReceived += VoiceWS_SocketMessage; - this.VoiceWs.Connected += VoiceWS_SocketOpened; - this.VoiceWs.ExceptionThrown += VoiceWs_SocketException; - } - - /// - /// Connects to the specified voice channel. - /// - /// A task representing the connection operation. - internal Task ConnectAsync() - { - UriBuilder gwuri = new() - { - Scheme = "wss", - Host = this.WebSocketEndpoint.Hostname, - Query = "encoding=json&v=4" - }; - - return this.VoiceWs.ConnectAsync(gwuri.Uri); - } - - internal Task ReconnectAsync() - => this.VoiceWs.DisconnectAsync(); - - internal async Task StartAsync() - { - // Let's announce our intentions to the server - VoiceDispatch vdp = new(); - - 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 - }; - } - string vdj = JsonConvert.SerializeObject(vdp, Formatting.None); - await WsSendAsync(vdj); - } - - internal Task WaitForReadyAsync() - => this.ReadyWait.Task; - - internal async Task EnqueuePacketAsync(RawVoicePacket packet, CancellationToken token = default) - { - await this.TransmitChannel.Writer.WriteAsync(packet, token); - this.queueCount++; - } - - internal bool PreparePacket(ReadOnlySpan pcm, out byte[] target, out int length) - { - target = null; - length = 0; - - if (this.IsDisposed) - { - return false; - } - - AudioFormat audioFormat = this.AudioFormat; - - byte[] packetArray = ArrayPool.Shared.Rent(Rtp.CalculatePacketSize(audioFormat.SampleCountToSampleSize(audioFormat.CalculateMaximumFrameSize()), this.SelectedEncryptionMode)); - Span packet = packetArray.AsSpan(); - - Rtp.EncodeHeader(this.Sequence, this.Timestamp, this.SSRC, packet); - Span opus = packet.Slice(Rtp.HeaderSize, pcm.Length); - this.Opus.Encode(pcm, ref opus); - - this.Sequence++; - this.Timestamp += (uint)audioFormat.CalculateFrameSize(audioFormat.CalculateSampleDuration(pcm.Length)); - - Span nonce = stackalloc byte[Sodium.NonceSize]; - switch (this.SelectedEncryptionMode) - { - case EncryptionMode.AeadAes256GcmRtpSize: - Sodium.GenerateNonce(this.Nonce++, nonce); - break; - - default: - ArrayPool.Shared.Return(packetArray); - throw new Exception("Unsupported encryption mode."); - } - - Span encrypted = stackalloc byte[Sodium.CalculateTargetSize(opus)]; - this.Sodium.Encrypt(opus, encrypted, nonce); - encrypted.CopyTo(packet[Rtp.HeaderSize..]); - packet = packet[..Rtp.CalculatePacketSize(encrypted.Length, this.SelectedEncryptionMode)]; - Sodium.AppendNonce(nonce, packet, this.SelectedEncryptionMode); - - target = packetArray; - length = packet.Length; - return true; - } - - private async Task VoiceSenderTaskAsync() - { - CancellationToken token = this.SenderToken; - BaseUdpClient client = this.UdpClient; - ChannelReader reader = this.TransmitChannel.Reader; - - byte[] data = null; - int length = 0; - - double synchronizerTicks = Stopwatch.GetTimestamp(); - double synchronizerResolution = Stopwatch.Frequency * 0.005; - double tickResolution = 10_000_000.0 / Stopwatch.Frequency; - this.Discord.Logger.LogDebug(VoiceNextEvents.Misc, "Timer accuracy: {Frequency}/{Resolution} (high resolution? {IsHighRes})", Stopwatch.Frequency, synchronizerResolution, Stopwatch.IsHighResolution); - - while (!token.IsCancellationRequested) - { - await this.PauseEvent.WaitAsync(); - - bool hasPacket = reader.TryRead(out RawVoicePacket rawPacket); - if (hasPacket) - { - this.queueCount--; - - if (this.PlayingWait == null || this.PlayingWait.Task.IsCompleted) - { - this.PlayingWait = new TaskCompletionSource(); - } - } - - // Provided by Laura#0090 (214796473689178133); this is Python, but adaptable: - // - // delay = max(0, self.delay + ((start_time + self.delay * loops) + - time.time())) - // - // self.delay - // sample size - // start_time - // time since streaming started - // loops - // number of samples sent - // time.time() - // DateTime.Now - - if (hasPacket) - { - hasPacket = PreparePacket(rawPacket.Bytes.Span, out data, out length); - if (rawPacket.RentedBuffer != null) - { - ArrayPool.Shared.Return(rawPacket.RentedBuffer); - } - } - - int durationModifier = hasPacket ? rawPacket.Duration / 5 : 4; - double cts = Math.Max(Stopwatch.GetTimestamp() - synchronizerTicks, 0); - if (cts < synchronizerResolution * durationModifier) - { - await Task.Delay(TimeSpan.FromTicks((long)(((synchronizerResolution * durationModifier) - cts) * tickResolution))); - } - - synchronizerTicks += synchronizerResolution * durationModifier; - - if (!hasPacket) - { - continue; - } - - await SendSpeakingAsync(true); - await client.SendAsync(data, length); - ArrayPool.Shared.Return(data); - - if (!rawPacket.Silence && this.queueCount == 0) - { - byte[] nullpcm = new byte[this.AudioFormat.CalculateSampleSize(20)]; - for (int i = 0; i < 3; i++) - { - byte[] nullpacket = new byte[nullpcm.Length]; - Memory nullpacketmem = nullpacket.AsMemory(); - await EnqueuePacketAsync(new RawVoicePacket(nullpacketmem, 20, true)); - } - } - else if (this.queueCount == 0) - { - await SendSpeakingAsync(false); - this.PlayingWait?.SetResult(true); - } - } - } - - private bool ProcessPacket(ReadOnlySpan data, ref Memory opus, ref Memory pcm, List> pcmPackets, out AudioSender voiceSender, out AudioFormat outputFormat) - { - voiceSender = null; - outputFormat = default; - - if (!Rtp.IsRtpHeader(data)) - { - return false; - } - - Rtp.DecodeHeader(data, out ushort shortSequence, out uint _, out uint ssrc, out bool hasExtension); - - if (!this.TransmittingSSRCs.TryGetValue(ssrc, out AudioSender? vtx)) - { - OpusDecoder 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; - ulong sequence = vtx.GetTrueSequenceAfterWrapping(shortSequence); - ushort gap = 0; - if (vtx.LastTrueSequence is ulong lastTrueSequence) - { - if (sequence <= lastTrueSequence) // out-of-order packet; discard - { - return false; - } - - gap = (ushort)(sequence - 1 - lastTrueSequence); - if (gap >= 5) - { - this.Discord.Logger.LogWarning(VoiceNextEvents.VoiceReceiveFailure, "5 or more voice packets were dropped when receiving"); - } - } - - Span nonce = stackalloc byte[Sodium.NonceSize]; - Sodium.GetNonce(data, nonce, this.SelectedEncryptionMode); - Rtp.GetDataFromPacket(data, out ReadOnlySpan encryptedOpus, this.SelectedEncryptionMode); - - int opusSize = Sodium.CalculateSourceSize(encryptedOpus); - opus = opus[..opusSize]; - Span opusSpan = opus.Span; - try - { - this.Sodium.Decrypt(encryptedOpus, opusSpan, nonce); - - // Strip extensions, if any - if (hasExtension) - { - // RFC 5285, 4.2 One-Byte header - // http://www.rfcreader.com/#rfc5285_line186 - if (opusSpan[0] == 0xBE && opusSpan[1] == 0xDE) - { - int headerLen = (opusSpan[2] << 8) | opusSpan[3]; - int i = 4; - for (; i < headerLen + 4; i++) - { - byte @byte = opusSpan[i]; - - // ID is currently unused since we skip it anyway - //var id = (byte)(@byte >> 4); - int 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) - { - int lastSampleCount = Opus.GetLastPacketSampleCount(vtx.Decoder); - byte[] fecpcm = new byte[this.AudioFormat.SampleCountToSampleSize(lastSampleCount)]; - Span fecpcmMem = fecpcm.AsSpan(); - this.Opus.Decode(vtx.Decoder, opusSpan, ref fecpcmMem, true, out _); - pcmPackets.Add(fecpcm.AsMemory(0, fecpcmMem.Length)); - } - else if (gap > 1) - { - int lastSampleCount = Opus.GetLastPacketSampleCount(vtx.Decoder); - for (int i = 0; i < gap; i++) - { - byte[] fecpcm = new byte[this.AudioFormat.SampleCountToSampleSize(lastSampleCount)]; - Span fecpcmMem = fecpcm.AsSpan(); - Opus.ProcessPacketLoss(vtx.Decoder, lastSampleCount, ref fecpcmMem); - pcmPackets.Add(fecpcm.AsMemory(0, fecpcmMem.Length)); - } - } - - Span pcmSpan = pcm.Span; - this.Opus.Decode(vtx.Decoder, opusSpan, ref pcmSpan, false, out outputFormat); - pcm = pcm[..pcmSpan.Length]; - } - finally - { - vtx.LastTrueSequence = sequence; - } - - return true; - } - - private async Task ProcessVoicePacketAsync(byte[] data) - { - if (data.Length < 13) // minimum packet length - { - return; - } - - try - { - byte[] pcm = new byte[this.AudioFormat.CalculateMaximumFrameSize()]; - Memory pcmMem = pcm.AsMemory(); - byte[] opus = new byte[pcm.Length]; - Memory opusMem = opus.AsMemory(); - List> pcmFillers = []; - if (!ProcessPacket(data, ref opusMem, ref pcmMem, pcmFillers, out AudioSender? vtx, out AudioFormat audioFormat)) - { - return; - } - - foreach (ReadOnlyMemory pcmFiller in pcmFillers) - { - await this.voiceReceived.InvokeAsync(this, new VoiceReceiveEventArgs - { - SSRC = vtx.SSRC, - User = vtx.User, - PcmData = pcmFiller, - OpusData = Array.Empty(), - AudioFormat = audioFormat, - AudioDuration = audioFormat.CalculateSampleDuration(pcmFiller.Length) - }); - } - - await this.voiceReceived.InvokeAsync(this, new VoiceReceiveEventArgs - { - SSRC = vtx.SSRC, - User = vtx.User, - PcmData = pcmMem, - OpusData = opusMem, - AudioFormat = audioFormat, - AudioDuration = audioFormat.CalculateSampleDuration(pcmMem.Length) - }); - } - catch (Exception ex) - { - this.Discord.Logger.LogError(VoiceNextEvents.VoiceReceiveFailure, ex, "Exception occurred when decoding incoming audio data"); - } - } - - private void ProcessKeepalive(byte[] data) - { - try - { - ulong keepalive = BinaryPrimitives.ReadUInt64LittleEndian(data); - - if (!this.KeepaliveTimestamps.TryRemove(keepalive, out long timestamp)) - { - return; - } - - int tdelta = (int)((Stopwatch.GetTimestamp() - timestamp) / (double)Stopwatch.Frequency * 1000); - this.Discord.Logger.LogDebug(VoiceNextEvents.VoiceKeepalive, "Received UDP keepalive {KeepAlive} (ping {Ping}ms)", keepalive, tdelta); - Volatile.Write(ref this.udpPing, tdelta); - } - catch (Exception ex) - { - this.Discord.Logger.LogError(VoiceNextEvents.VoiceKeepalive, ex, "Exception occurred when handling keepalive"); - } - } - - private async Task UdpReceiverTaskAsync() - { - CancellationToken token = this.ReceiverToken; - BaseUdpClient client = this.UdpClient; - - while (!token.IsCancellationRequested) - { - byte[] data = await client.ReceiveAsync(); - if (data.Length == 8) - { - ProcessKeepalive(data); - } - else if (this.Configuration.EnableIncoming) - { - await ProcessVoicePacketAsync(data); - } - } - } - - /// - /// Sends a speaking status to the connected voice channel. - /// - /// Whether the current user is speaking or not. - /// A task representing the sending operation. - public async Task SendSpeakingAsync(bool speaking = true) - { - if (!this.IsInitialized) - { - throw new InvalidOperationException("The connection is not initialized"); - } - - if (this.isSpeaking != speaking) - { - this.isSpeaking = speaking; - VoiceDispatch pld = new() - { - OpCode = 5, - Payload = new VoiceSpeakingPayload - { - Speaking = speaking, - Delay = 0 - } - }; - - string plj = JsonConvert.SerializeObject(pld, Formatting.None); - await WsSendAsync(plj); - } - } - - /// - /// 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."); - } - - this.TransmitStream ??= new VoiceTransmitSink(this, sampleDuration); - return this.TransmitStream; - } - - /// - /// Asynchronously waits for playback to be finished. Playback is finished when speaking = false is signalled. - /// - /// A task representing the waiting operation. - public async Task WaitForPlaybackFinishAsync() - { - if (this.PlayingWait != null) - { - await this.PlayingWait.Task; - } - } - - /// - /// Pauses playback. - /// - public void Pause() - => this.PauseEvent.Reset(); - - /// - /// Asynchronously resumes playback. - /// - /// - public async Task ResumeAsync() - => await this.PauseEvent.SetAsync(); - - /// - /// Disconnects and disposes this voice connection. - /// - public void Disconnect() - => Dispose(); - - /// - /// Disconnects and disposes this voice connection. - /// - public void Dispose() - { - if (this.IsDisposed) - { - return; - } - - this.IsDisposed = true; - this.IsInitialized = false; - this.TokenSource?.Cancel(); - this.SenderTokenSource?.Cancel(); - this.ReceiverTokenSource?.Cancel(); - this.KeepaliveTokenSource?.Cancel(); - - this.TokenSource?.Dispose(); - this.SenderTokenSource?.Dispose(); - this.ReceiverTokenSource?.Dispose(); - this.KeepaliveTokenSource?.Dispose(); - - try - { - this.VoiceWs.DisconnectAsync().GetAwaiter().GetResult(); - this.UdpClient.Close(); - } - catch { } - - this.Opus?.Dispose(); - this.Opus = null!; - this.Sodium?.Dispose(); - this.Sodium = null!; - this.Rtp?.Dispose(); - this.Rtp = null!; - - this.VoiceDisconnected?.Invoke(this.Guild); - } - - private async Task HeartbeatAsync() - { - await Task.Yield(); - - CancellationToken token = this.Token; - while (true) - { - try - { - token.ThrowIfCancellationRequested(); - - DateTime dt = DateTime.Now; - this.Discord.Logger.LogTrace(VoiceNextEvents.VoiceHeartbeat, "Sent heartbeat"); - - VoiceDispatch hbd = new() - { - OpCode = 3, - Payload = UnixTimestamp(dt) - }; - string hbj = JsonConvert.SerializeObject(hbd); - await WsSendAsync(hbj); - - this.LastHeartbeat = dt; - await Task.Delay(this.HeartbeatInterval); - } - catch (OperationCanceledException) - { - return; - } - } - } - - private async Task KeepaliveAsync() - { - await Task.Yield(); - - CancellationToken token = this.KeepaliveToken; - BaseUdpClient client = this.UdpClient; - - while (!token.IsCancellationRequested) - { - long timestamp = Stopwatch.GetTimestamp(); - ulong keepalive = Volatile.Read(ref this.lastKeepalive); - Volatile.Write(ref this.lastKeepalive, keepalive + 1); - this.KeepaliveTimestamps.TryAdd(keepalive, timestamp); - - byte[] packet = new byte[8]; - BinaryPrimitives.WriteUInt64LittleEndian(packet, keepalive); - - await client.SendAsync(packet, packet.Length); - - await Task.Delay(5000, token); - } - } - - private async Task Stage1Async(VoiceReadyPayload voiceReady) - { - // IP Discovery - this.UdpClient.Setup(this.UdpEndpoint); - - byte[] pck = new byte[74]; - PreparePacket(pck); - - await this.UdpClient.SendAsync(pck, pck.Length); - - byte[] ipd = await this.UdpClient.ReceiveAsync(); - ReadPacket(ipd, out System.Net.IPAddress? ip, out ushort port); - this.DiscoveredEndpoint = new IpEndpoint - { - Address = ip, - Port = port - }; - this.Discord.Logger.LogTrace(VoiceNextEvents.VoiceHandshake, "Endpoint discovery finished - discovered endpoint is {Ip}:{Port}", ip, port); - - void PreparePacket(byte[] packet) - { - uint ssrc = this.SSRC; - ushort type = 0x1; // type: request (isn't this one way anyway?) - ushort length = 70; // length of everything after this. should for this step always be 70. - - Span packetSpan = packet.AsSpan(); - Helpers.ZeroFill(packetSpan); // fill with zeroes - - byte[] typeByte = BitConverter.GetBytes(type); - byte[] lengthByte = BitConverter.GetBytes(length); - byte[] ssrcByte = BitConverter.GetBytes(ssrc); - - if (BitConverter.IsLittleEndian) - { - Array.Reverse(typeByte); - Array.Reverse(lengthByte); - Array.Reverse(ssrcByte); - } - - typeByte.CopyTo(packet, 0); - lengthByte.CopyTo(packet, 2); - ssrcByte.CopyTo(packet, 4); - // https://discord.com/developers/docs/topics/voice-connections#ip-discovery - } - - void ReadPacket(byte[] packet, out System.Net.IPAddress decodedIp, out ushort decodedPort) - { - Span packetSpan = packet.AsSpan(); - - // the packet we received in this step should be the IP discovery response. - - // it has the same format as PreparePacket. All we really need is IP + port so we strip it from - // the response here, which are the last 6 bytes (4 for ip, 2 for port (ushort)) - - string ipString = Utilities.UTF8.GetString(packet, 8, 64 /* 74 - 6 */).TrimEnd('\0'); - decodedIp = System.Net.IPAddress.Parse(ipString); - decodedPort = BinaryPrimitives.ReadUInt16LittleEndian(packetSpan[72 /* 74 - 2 */..]); - } - - // Select voice encryption mode - KeyValuePair selectedEncryptionMode = Sodium.SelectMode(voiceReady.Modes); - this.SelectedEncryptionMode = selectedEncryptionMode.Value; - - // Ready - this.Discord.Logger.LogTrace(VoiceNextEvents.VoiceHandshake, "Selected encryption mode is {EncryptionMode}", selectedEncryptionMode.Key); - VoiceDispatch vsp = new() - { - OpCode = 1, - Payload = new VoiceSelectProtocolPayload - { - Protocol = "udp", - Data = new VoiceSelectProtocolPayloadData - { - Address = this.DiscoveredEndpoint.Address.ToString(), - Port = (ushort)this.DiscoveredEndpoint.Port, - Mode = selectedEncryptionMode.Key - } - } - }; - string vsj = JsonConvert.SerializeObject(vsp, Formatting.None); - await WsSendAsync(vsj); - - this.SenderTokenSource = new CancellationTokenSource(); - this.SenderTask = Task.Run(VoiceSenderTaskAsync, this.SenderToken); - - this.ReceiverTokenSource = new CancellationTokenSource(); - this.ReceiverTask = Task.Run(UdpReceiverTaskAsync, this.ReceiverToken); - } - - private async Task Stage2Async(VoiceSessionDescriptionPayload voiceSessionDescription) - { - this.SelectedEncryptionMode = Sodium.SupportedModes[voiceSessionDescription.Mode.ToLowerInvariant()]; - this.Discord.Logger.LogTrace(VoiceNextEvents.VoiceHandshake, "Discord updated encryption mode - new mode is {EncryptionMode}", this.SelectedEncryptionMode); - - // start keepalive - this.KeepaliveTokenSource = new CancellationTokenSource(); - this.KeepaliveTask = KeepaliveAsync(); - - // send 3 packets of silence to get things going - byte[] nullpcm = new byte[this.AudioFormat.CalculateSampleSize(20)]; - for (int i = 0; i < 3; i++) - { - byte[] nullPcm = new byte[nullpcm.Length]; - Memory nullpacketmem = nullPcm.AsMemory(); - await EnqueuePacketAsync(new RawVoicePacket(nullpacketmem, 20, true)); - } - - this.IsInitialized = true; - this.ReadyWait.SetResult(true); - } - - private async Task HandleDispatchAsync(JObject jo) - { - int opc = (int)jo["op"]; - JObject? opp = jo["d"] as JObject; - - switch (opc) - { - case 2: // READY - this.Discord.Logger.LogTrace(VoiceNextEvents.VoiceDispatch, "Received READY (OP2)"); - VoiceReadyPayload vrp = opp.ToDiscordObject(); - 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(HeartbeatAsync); - await Stage1Async(vrp); - break; - - case 4: // SESSION_DESCRIPTION - this.Discord.Logger.LogTrace(VoiceNextEvents.VoiceDispatch, "Received SESSION_DESCRIPTION (OP4)"); - VoiceSessionDescriptionPayload vsd = opp.ToDiscordObject(); - this.Key = vsd.SecretKey; - this.Sodium = new Sodium(this.Key.AsMemory()); - await Stage2Async(vsd); - 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)"); - VoiceSpeakingPayload spd = opp.ToDiscordObject(); - bool foundUserInCache = this.Discord.TryGetCachedUserInternal(spd.UserId.Value, out DiscordUser? resolvedUser); - UserSpeakingEventArgs spk = new() - { - Speaking = spd.Speaking, - SSRC = spd.SSRC.Value, - User = resolvedUser, - }; - - if (foundUserInCache && this.TransmittingSSRCs.TryGetValue(spk.SSRC, out AudioSender? txssrc5) && txssrc5.Id == 0) - { - txssrc5.User = spk.User; - } - else - { - OpusDecoder opus = this.Opus.CreateDecoder(); - AudioSender vtx = new(spk.SSRC, opus) - { - User = await this.Discord.GetUserAsync(spd.UserId.Value) - }; - - if (!this.TransmittingSSRCs.TryAdd(spk.SSRC, vtx)) - { - this.Opus.DestroyDecoder(opus); - } - } - - await this.userSpeaking.InvokeAsync(this, spk); - break; - - case 6: // HEARTBEAT ACK - DateTime dt = DateTime.Now; - int ping = (int)(dt - this.LastHeartbeat).TotalMilliseconds; - Volatile.Write(ref this.wsPing, ping); - this.Discord.Logger.LogTrace(VoiceNextEvents.VoiceDispatch, "Received HEARTBEAT_ACK (OP6, {Heartbeat}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"].ToDiscordObject(); - break; - - case 9: // RESUMED - this.Discord.Logger.LogTrace(VoiceNextEvents.VoiceDispatch, "Received RESUMED (OP9)"); - this.HeartbeatTask = Task.Run(HeartbeatAsync); - break; - - case 12: // CLIENT_CONNECTED - this.Discord.Logger.LogTrace(VoiceNextEvents.VoiceDispatch, "Received CLIENT_CONNECTED (OP12)"); - VoiceUserJoinPayload ujpd = opp.ToDiscordObject(); - DiscordUser usrj = await this.Discord.GetUserAsync(ujpd.UserId); - { - OpusDecoder opus = this.Opus.CreateDecoder(); - AudioSender vtx = new(ujpd.SSRC, opus) - { - User = usrj - }; - - if (!this.TransmittingSSRCs.TryAdd(vtx.SSRC, vtx)) - { - this.Opus.DestroyDecoder(opus); - } - } - - await this.userJoined.InvokeAsync(this, new VoiceUserJoinEventArgs { User = usrj, SSRC = ujpd.SSRC }); - break; - - case 13: // CLIENT_DISCONNECTED - this.Discord.Logger.LogTrace(VoiceNextEvents.VoiceDispatch, "Received CLIENT_DISCONNECTED (OP13)"); - VoiceUserLeavePayload ulpd = opp.ToDiscordObject(); - KeyValuePair txssrc = this.TransmittingSSRCs.FirstOrDefault(x => x.Value.Id == ulpd.UserId); - if (this.TransmittingSSRCs.ContainsKey(txssrc.Key)) - { - this.TransmittingSSRCs.TryRemove(txssrc.Key, out AudioSender? txssrc13); - this.Opus.DestroyDecoder(txssrc13.Decoder); - } - - DiscordUser usrl = await this.Discord.GetUserAsync(ulpd.UserId); - await this.userLeft.InvokeAsync(this, new VoiceUserLeaveEventArgs - { - User = usrl, - SSRC = txssrc.Key - }); - break; - - default: - this.Discord.Logger.LogTrace(VoiceNextEvents.VoiceDispatch, "Received unknown voice opcode (OP{Op})", opc); - break; - } - } - - private async Task VoiceWS_SocketClosedAsync(IWebSocketClient client, SocketClosedEventArgs e) - { - this.Discord.Logger.LogDebug(VoiceNextEvents.VoiceConnectionClose, "Voice WebSocket closed ({CloseCode}, '{CloseMessage}')", 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 is 4006 or 4009) - { - this.Resume = false; - } - - if (!this.IsDisposed) - { - this.TokenSource.Cancel(); - this.TokenSource = new CancellationTokenSource(); - this.VoiceWs = new WebSocketClient(new DefaultClientErrorHandler(this.Discord.Logger)); - this.VoiceWs.Disconnected += VoiceWS_SocketClosedAsync; - this.VoiceWs.MessageReceived += VoiceWS_SocketMessage; - this.VoiceWs.Connected += VoiceWS_SocketOpened; - - if (this.Resume) // emzi you dipshit - { - await ConnectAsync(); - } - } - } - - private Task VoiceWS_SocketMessage(IWebSocketClient client, SocketMessageEventArgs e) - { - if (e is not SocketTextMessageEventArgs et) - { - this.Discord.Logger.LogCritical(VoiceNextEvents.VoiceGatewayError, "Discord Voice Gateway sent binary data - unable to process"); - return Task.CompletedTask; - } - - this.Discord.Logger.LogTrace(VoiceNextEvents.VoiceWsRx, "{WebsocketMessage}", et.Message); - return HandleDispatchAsync(JObject.Parse(et.Message)); - } - - private Task VoiceWS_SocketOpened(IWebSocketClient client, SocketEventArgs e) - => StartAsync(); - - private Task VoiceWs_SocketException(IWebSocketClient client, SocketErrorEventArgs e) - => this.voiceSocketError.InvokeAsync(this, new SocketErrorEventArgs { Exception = e.Exception }); - - private async Task WsSendAsync(string payload) - { - this.Discord.Logger.LogTrace(VoiceNextEvents.VoiceWsTx, "{WebsocketPayload}", payload); - await this.VoiceWs.SendMessageAsync(payload); - } - - private static uint UnixTimestamp(DateTime dt) - { - TimeSpan ts = dt - UnixEpoch; - double sd = ts.TotalSeconds; - uint si = (uint)sd; - return si; - } -} - -// Naam you still owe me those noodles :^) -// I remember -// Alexa, how much is shipping to emzi -// NL -> PL is 18.50€ for packages <=2kg it seems (https://www.postnl.nl/en/mail-and-parcels/parcels/international-parcel/) +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.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; +using DSharpPlus.AsyncEvents; +using DSharpPlus.Entities; +using DSharpPlus.EventArgs; +using DSharpPlus.Net; +using DSharpPlus.Net.Serialization; +using DSharpPlus.Net.Udp; +using DSharpPlus.Net.WebSocket; +using DSharpPlus.VoiceNext.Codec; +using DSharpPlus.VoiceNext.Entities; +using DSharpPlus.VoiceNext.EventArgs; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace DSharpPlus.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; + + private static DateTimeOffset UnixEpoch { get; } = new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero); + + private DiscordClient Discord { get; } + private DiscordGuild Guild { get; } + private ConcurrentDictionary TransmittingSSRCs { get; } + + private BaseUdpClient UdpClient { get; } + private IWebSocketClient VoiceWs { get; set; } + private Task HeartbeatTask { get; set; } + private int HeartbeatInterval { get; set; } + private DateTimeOffset LastHeartbeat { get; set; } + + private CancellationTokenSource TokenSource { get; set; } + private CancellationToken Token + => this.TokenSource.Token; + + internal VoiceServerUpdatePayload ServerData { get; set; } + internal VoiceStateUpdatePayload StateData { get; set; } + internal bool Resume { get; set; } + + private VoiceNextConfiguration Configuration { get; } + private Opus Opus { get; set; } + private Sodium Sodium { get; set; } + private Rtp Rtp { get; set; } + private EncryptionMode SelectedEncryptionMode { get; set; } + private uint Nonce { get; set; } = 0; + + private ushort Sequence { get; set; } + private uint Timestamp { get; set; } + private uint SSRC { get; set; } + private byte[] Key { get; set; } + private IpEndpoint DiscoveredEndpoint { get; set; } + internal ConnectionEndpoint WebSocketEndpoint { get; set; } + internal ConnectionEndpoint UdpEndpoint { get; set; } + + private TaskCompletionSource ReadyWait { get; set; } + private bool IsInitialized { get; set; } + private bool IsDisposed { get; set; } + + private TaskCompletionSource PlayingWait { get; set; } + + private AsyncManualResetEvent PauseEvent { get; } + private VoiceTransmitSink TransmitStream { get; set; } + private Channel TransmitChannel { get; } + private ConcurrentDictionary KeepaliveTimestamps { get; } + private ulong lastKeepalive = 0; + + private Task SenderTask { get; set; } + private CancellationTokenSource SenderTokenSource { get; set; } + private CancellationToken SenderToken + => this.SenderTokenSource.Token; + + private Task ReceiverTask { get; set; } + private CancellationTokenSource ReceiverTokenSource { get; set; } + private CancellationToken ReceiverToken + => this.ReceiverTokenSource.Token; + + private Task KeepaliveTask { get; set; } + private CancellationTokenSource KeepaliveTokenSource { get; set; } + private CancellationToken KeepaliveToken + => this.KeepaliveTokenSource.Token; + + private volatile bool isSpeaking = false; + + /// + /// Gets the audio format used by the Opus encoder. + /// + public AudioFormat AudioFormat => this.Configuration.AudioFormat; + + /// + /// Gets whether this connection is still playing audio. + /// + public bool IsPlaying + => this.PlayingWait != null && !this.PlayingWait.Task.IsCompleted; + + /// + /// Gets the websocket round-trip time in ms. + /// + public int WebSocketPing + => Volatile.Read(ref this.wsPing); + private int wsPing = 0; + + /// + /// 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; } + + internal VoiceNextConnection(DiscordClient client, DiscordGuild guild, DiscordChannel channel, VoiceNextConfiguration config, VoiceServerUpdatePayload server, VoiceStateUpdatePayload state) + { + this.Discord = client; + this.Guild = guild; + this.TargetChannel = channel; + this.TransmittingSSRCs = new ConcurrentDictionary(); + + DefaultClientErrorHandler errorHandler = new(client.Logger); + + this.userSpeaking = new AsyncEvent(errorHandler); + this.userJoined = new AsyncEvent(errorHandler); + this.userLeft = new AsyncEvent(errorHandler); + this.voiceReceived = new AsyncEvent(errorHandler); + this.voiceSocketError = new AsyncEvent(errorHandler); + this.TokenSource = new CancellationTokenSource(); + + this.Configuration = config; + this.Opus = new Opus(this.AudioFormat); + //this.Sodium = new Sodium(); + this.Rtp = new Rtp(); + + this.ServerData = server; + this.StateData = state; + + string eps = this.ServerData.Endpoint; + int epi = eps.LastIndexOf(':'); + string eph = string.Empty; + int 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.IsInitialized = false; + this.IsDisposed = false; + + 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 = new WebSocketClient(errorHandler); + this.VoiceWs.Disconnected += VoiceWS_SocketClosedAsync; + this.VoiceWs.MessageReceived += VoiceWS_SocketMessage; + this.VoiceWs.Connected += VoiceWS_SocketOpened; + this.VoiceWs.ExceptionThrown += VoiceWs_SocketException; + } + + /// + /// Connects to the specified voice channel. + /// + /// A task representing the connection operation. + internal Task ConnectAsync() + { + UriBuilder gwuri = new() + { + Scheme = "wss", + Host = this.WebSocketEndpoint.Hostname, + Query = "encoding=json&v=4" + }; + + return this.VoiceWs.ConnectAsync(gwuri.Uri); + } + + internal Task ReconnectAsync() + => this.VoiceWs.DisconnectAsync(); + + internal async Task StartAsync() + { + // Let's announce our intentions to the server + VoiceDispatch vdp = new(); + + 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 + }; + } + string vdj = JsonConvert.SerializeObject(vdp, Formatting.None); + await WsSendAsync(vdj); + } + + internal Task WaitForReadyAsync() + => this.ReadyWait.Task; + + internal async Task EnqueuePacketAsync(RawVoicePacket packet, CancellationToken token = default) + { + await this.TransmitChannel.Writer.WriteAsync(packet, token); + this.queueCount++; + } + + internal bool PreparePacket(ReadOnlySpan pcm, out byte[] target, out int length) + { + target = null; + length = 0; + + if (this.IsDisposed) + { + return false; + } + + AudioFormat audioFormat = this.AudioFormat; + + byte[] packetArray = ArrayPool.Shared.Rent(Rtp.CalculatePacketSize(audioFormat.SampleCountToSampleSize(audioFormat.CalculateMaximumFrameSize()), this.SelectedEncryptionMode)); + Span packet = packetArray.AsSpan(); + + Rtp.EncodeHeader(this.Sequence, this.Timestamp, this.SSRC, packet); + Span opus = packet.Slice(Rtp.HeaderSize, pcm.Length); + this.Opus.Encode(pcm, ref opus); + + this.Sequence++; + this.Timestamp += (uint)audioFormat.CalculateFrameSize(audioFormat.CalculateSampleDuration(pcm.Length)); + + Span nonce = stackalloc byte[Sodium.NonceSize]; + switch (this.SelectedEncryptionMode) + { + case EncryptionMode.AeadAes256GcmRtpSize: + Sodium.GenerateNonce(this.Nonce++, nonce); + break; + + default: + ArrayPool.Shared.Return(packetArray); + throw new Exception("Unsupported encryption mode."); + } + + Span encrypted = stackalloc byte[Sodium.CalculateTargetSize(opus)]; + this.Sodium.Encrypt(opus, encrypted, nonce); + encrypted.CopyTo(packet[Rtp.HeaderSize..]); + packet = packet[..Rtp.CalculatePacketSize(encrypted.Length, this.SelectedEncryptionMode)]; + Sodium.AppendNonce(nonce, packet, this.SelectedEncryptionMode); + + target = packetArray; + length = packet.Length; + return true; + } + + private async Task VoiceSenderTaskAsync() + { + CancellationToken token = this.SenderToken; + BaseUdpClient client = this.UdpClient; + ChannelReader reader = this.TransmitChannel.Reader; + + byte[] data = null; + int length = 0; + + double synchronizerTicks = Stopwatch.GetTimestamp(); + double synchronizerResolution = Stopwatch.Frequency * 0.005; + double tickResolution = 10_000_000.0 / Stopwatch.Frequency; + this.Discord.Logger.LogDebug(VoiceNextEvents.Misc, "Timer accuracy: {Frequency}/{Resolution} (high resolution? {IsHighRes})", Stopwatch.Frequency, synchronizerResolution, Stopwatch.IsHighResolution); + + while (!token.IsCancellationRequested) + { + await this.PauseEvent.WaitAsync(); + + bool hasPacket = reader.TryRead(out RawVoicePacket rawPacket); + if (hasPacket) + { + this.queueCount--; + + if (this.PlayingWait == null || this.PlayingWait.Task.IsCompleted) + { + this.PlayingWait = new TaskCompletionSource(); + } + } + + // Provided by Laura#0090 (214796473689178133); this is Python, but adaptable: + // + // delay = max(0, self.delay + ((start_time + self.delay * loops) + - time.time())) + // + // self.delay + // sample size + // start_time + // time since streaming started + // loops + // number of samples sent + // time.time() + // DateTime.Now + + if (hasPacket) + { + hasPacket = PreparePacket(rawPacket.Bytes.Span, out data, out length); + if (rawPacket.RentedBuffer != null) + { + ArrayPool.Shared.Return(rawPacket.RentedBuffer); + } + } + + int durationModifier = hasPacket ? rawPacket.Duration / 5 : 4; + double cts = Math.Max(Stopwatch.GetTimestamp() - synchronizerTicks, 0); + if (cts < synchronizerResolution * durationModifier) + { + await Task.Delay(TimeSpan.FromTicks((long)(((synchronizerResolution * durationModifier) - cts) * tickResolution))); + } + + synchronizerTicks += synchronizerResolution * durationModifier; + + if (!hasPacket) + { + continue; + } + + await SendSpeakingAsync(true); + await client.SendAsync(data, length); + ArrayPool.Shared.Return(data); + + if (!rawPacket.Silence && this.queueCount == 0) + { + byte[] nullpcm = new byte[this.AudioFormat.CalculateSampleSize(20)]; + for (int i = 0; i < 3; i++) + { + byte[] nullpacket = new byte[nullpcm.Length]; + Memory nullpacketmem = nullpacket.AsMemory(); + await EnqueuePacketAsync(new RawVoicePacket(nullpacketmem, 20, true)); + } + } + else if (this.queueCount == 0) + { + await SendSpeakingAsync(false); + this.PlayingWait?.SetResult(true); + } + } + } + + private bool ProcessPacket(ReadOnlySpan data, ref Memory opus, ref Memory pcm, List> pcmPackets, out AudioSender voiceSender, out AudioFormat outputFormat) + { + voiceSender = null; + outputFormat = default; + + if (!Rtp.IsRtpHeader(data)) + { + return false; + } + + Rtp.DecodeHeader(data, out ushort shortSequence, out uint _, out uint ssrc, out bool hasExtension); + + if (!this.TransmittingSSRCs.TryGetValue(ssrc, out AudioSender? vtx)) + { + OpusDecoder 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; + ulong sequence = vtx.GetTrueSequenceAfterWrapping(shortSequence); + ushort gap = 0; + if (vtx.LastTrueSequence is ulong lastTrueSequence) + { + if (sequence <= lastTrueSequence) // out-of-order packet; discard + { + return false; + } + + gap = (ushort)(sequence - 1 - lastTrueSequence); + if (gap >= 5) + { + this.Discord.Logger.LogWarning(VoiceNextEvents.VoiceReceiveFailure, "5 or more voice packets were dropped when receiving"); + } + } + + Span nonce = stackalloc byte[Sodium.NonceSize]; + Sodium.GetNonce(data, nonce, this.SelectedEncryptionMode); + Rtp.GetDataFromPacket(data, out ReadOnlySpan encryptedOpus, this.SelectedEncryptionMode); + + int opusSize = Sodium.CalculateSourceSize(encryptedOpus); + opus = opus[..opusSize]; + Span opusSpan = opus.Span; + try + { + this.Sodium.Decrypt(encryptedOpus, opusSpan, nonce); + + // Strip extensions, if any + if (hasExtension) + { + // RFC 5285, 4.2 One-Byte header + // http://www.rfcreader.com/#rfc5285_line186 + if (opusSpan[0] == 0xBE && opusSpan[1] == 0xDE) + { + int headerLen = (opusSpan[2] << 8) | opusSpan[3]; + int i = 4; + for (; i < headerLen + 4; i++) + { + byte @byte = opusSpan[i]; + + // ID is currently unused since we skip it anyway + //var id = (byte)(@byte >> 4); + int 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) + { + int lastSampleCount = Opus.GetLastPacketSampleCount(vtx.Decoder); + byte[] fecpcm = new byte[this.AudioFormat.SampleCountToSampleSize(lastSampleCount)]; + Span fecpcmMem = fecpcm.AsSpan(); + this.Opus.Decode(vtx.Decoder, opusSpan, ref fecpcmMem, true, out _); + pcmPackets.Add(fecpcm.AsMemory(0, fecpcmMem.Length)); + } + else if (gap > 1) + { + int lastSampleCount = Opus.GetLastPacketSampleCount(vtx.Decoder); + for (int i = 0; i < gap; i++) + { + byte[] fecpcm = new byte[this.AudioFormat.SampleCountToSampleSize(lastSampleCount)]; + Span fecpcmMem = fecpcm.AsSpan(); + Opus.ProcessPacketLoss(vtx.Decoder, lastSampleCount, ref fecpcmMem); + pcmPackets.Add(fecpcm.AsMemory(0, fecpcmMem.Length)); + } + } + + Span pcmSpan = pcm.Span; + this.Opus.Decode(vtx.Decoder, opusSpan, ref pcmSpan, false, out outputFormat); + pcm = pcm[..pcmSpan.Length]; + } + finally + { + vtx.LastTrueSequence = sequence; + } + + return true; + } + + private async Task ProcessVoicePacketAsync(byte[] data) + { + if (data.Length < 13) // minimum packet length + { + return; + } + + try + { + byte[] pcm = new byte[this.AudioFormat.CalculateMaximumFrameSize()]; + Memory pcmMem = pcm.AsMemory(); + byte[] opus = new byte[pcm.Length]; + Memory opusMem = opus.AsMemory(); + List> pcmFillers = []; + if (!ProcessPacket(data, ref opusMem, ref pcmMem, pcmFillers, out AudioSender? vtx, out AudioFormat audioFormat)) + { + return; + } + + foreach (ReadOnlyMemory pcmFiller in pcmFillers) + { + await this.voiceReceived.InvokeAsync(this, new VoiceReceiveEventArgs + { + SSRC = vtx.SSRC, + User = vtx.User, + PcmData = pcmFiller, + OpusData = Array.Empty(), + AudioFormat = audioFormat, + AudioDuration = audioFormat.CalculateSampleDuration(pcmFiller.Length) + }); + } + + await this.voiceReceived.InvokeAsync(this, new VoiceReceiveEventArgs + { + SSRC = vtx.SSRC, + User = vtx.User, + PcmData = pcmMem, + OpusData = opusMem, + AudioFormat = audioFormat, + AudioDuration = audioFormat.CalculateSampleDuration(pcmMem.Length) + }); + } + catch (Exception ex) + { + this.Discord.Logger.LogError(VoiceNextEvents.VoiceReceiveFailure, ex, "Exception occurred when decoding incoming audio data"); + } + } + + private void ProcessKeepalive(byte[] data) + { + try + { + ulong keepalive = BinaryPrimitives.ReadUInt64LittleEndian(data); + + if (!this.KeepaliveTimestamps.TryRemove(keepalive, out long timestamp)) + { + return; + } + + int tdelta = (int)((Stopwatch.GetTimestamp() - timestamp) / (double)Stopwatch.Frequency * 1000); + this.Discord.Logger.LogDebug(VoiceNextEvents.VoiceKeepalive, "Received UDP keepalive {KeepAlive} (ping {Ping}ms)", keepalive, tdelta); + Volatile.Write(ref this.udpPing, tdelta); + } + catch (Exception ex) + { + this.Discord.Logger.LogError(VoiceNextEvents.VoiceKeepalive, ex, "Exception occurred when handling keepalive"); + } + } + + private async Task UdpReceiverTaskAsync() + { + CancellationToken token = this.ReceiverToken; + BaseUdpClient client = this.UdpClient; + + while (!token.IsCancellationRequested) + { + byte[] data = await client.ReceiveAsync(); + if (data.Length == 8) + { + ProcessKeepalive(data); + } + else if (this.Configuration.EnableIncoming) + { + await ProcessVoicePacketAsync(data); + } + } + } + + /// + /// Sends a speaking status to the connected voice channel. + /// + /// Whether the current user is speaking or not. + /// A task representing the sending operation. + public async Task SendSpeakingAsync(bool speaking = true) + { + if (!this.IsInitialized) + { + throw new InvalidOperationException("The connection is not initialized"); + } + + if (this.isSpeaking != speaking) + { + this.isSpeaking = speaking; + VoiceDispatch pld = new() + { + OpCode = 5, + Payload = new VoiceSpeakingPayload + { + Speaking = speaking, + Delay = 0 + } + }; + + string plj = JsonConvert.SerializeObject(pld, Formatting.None); + await WsSendAsync(plj); + } + } + + /// + /// 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."); + } + + this.TransmitStream ??= new VoiceTransmitSink(this, sampleDuration); + return this.TransmitStream; + } + + /// + /// Asynchronously waits for playback to be finished. Playback is finished when speaking = false is signalled. + /// + /// A task representing the waiting operation. + public async Task WaitForPlaybackFinishAsync() + { + if (this.PlayingWait != null) + { + await this.PlayingWait.Task; + } + } + + /// + /// Pauses playback. + /// + public void Pause() + => this.PauseEvent.Reset(); + + /// + /// Asynchronously resumes playback. + /// + /// + public async Task ResumeAsync() + => await this.PauseEvent.SetAsync(); + + /// + /// Disconnects and disposes this voice connection. + /// + public void Disconnect() + => Dispose(); + + /// + /// Disconnects and disposes this voice connection. + /// + public void Dispose() + { + if (this.IsDisposed) + { + return; + } + + this.IsDisposed = true; + this.IsInitialized = false; + this.TokenSource?.Cancel(); + this.SenderTokenSource?.Cancel(); + this.ReceiverTokenSource?.Cancel(); + this.KeepaliveTokenSource?.Cancel(); + + this.TokenSource?.Dispose(); + this.SenderTokenSource?.Dispose(); + this.ReceiverTokenSource?.Dispose(); + this.KeepaliveTokenSource?.Dispose(); + + try + { + this.VoiceWs.DisconnectAsync().GetAwaiter().GetResult(); + this.UdpClient.Close(); + } + catch { } + + this.Opus?.Dispose(); + this.Opus = null!; + this.Sodium?.Dispose(); + this.Sodium = null!; + this.Rtp?.Dispose(); + this.Rtp = null!; + + this.VoiceDisconnected?.Invoke(this.Guild); + } + + private async Task HeartbeatAsync() + { + await Task.Yield(); + + CancellationToken token = this.Token; + while (true) + { + try + { + token.ThrowIfCancellationRequested(); + + DateTime dt = DateTime.Now; + this.Discord.Logger.LogTrace(VoiceNextEvents.VoiceHeartbeat, "Sent heartbeat"); + + VoiceDispatch hbd = new() + { + OpCode = 3, + Payload = UnixTimestamp(dt) + }; + string hbj = JsonConvert.SerializeObject(hbd); + await WsSendAsync(hbj); + + this.LastHeartbeat = dt; + await Task.Delay(this.HeartbeatInterval); + } + catch (OperationCanceledException) + { + return; + } + } + } + + private async Task KeepaliveAsync() + { + await Task.Yield(); + + CancellationToken token = this.KeepaliveToken; + BaseUdpClient client = this.UdpClient; + + while (!token.IsCancellationRequested) + { + long timestamp = Stopwatch.GetTimestamp(); + ulong keepalive = Volatile.Read(ref this.lastKeepalive); + Volatile.Write(ref this.lastKeepalive, keepalive + 1); + this.KeepaliveTimestamps.TryAdd(keepalive, timestamp); + + byte[] packet = new byte[8]; + BinaryPrimitives.WriteUInt64LittleEndian(packet, keepalive); + + await client.SendAsync(packet, packet.Length); + + await Task.Delay(5000, token); + } + } + + private async Task Stage1Async(VoiceReadyPayload voiceReady) + { + // IP Discovery + this.UdpClient.Setup(this.UdpEndpoint); + + byte[] pck = new byte[74]; + PreparePacket(pck); + + await this.UdpClient.SendAsync(pck, pck.Length); + + byte[] ipd = await this.UdpClient.ReceiveAsync(); + ReadPacket(ipd, out System.Net.IPAddress? ip, out ushort port); + this.DiscoveredEndpoint = new IpEndpoint + { + Address = ip, + Port = port + }; + this.Discord.Logger.LogTrace(VoiceNextEvents.VoiceHandshake, "Endpoint discovery finished - discovered endpoint is {Ip}:{Port}", ip, port); + + void PreparePacket(byte[] packet) + { + uint ssrc = this.SSRC; + ushort type = 0x1; // type: request (isn't this one way anyway?) + ushort length = 70; // length of everything after this. should for this step always be 70. + + Span packetSpan = packet.AsSpan(); + Helpers.ZeroFill(packetSpan); // fill with zeroes + + byte[] typeByte = BitConverter.GetBytes(type); + byte[] lengthByte = BitConverter.GetBytes(length); + byte[] ssrcByte = BitConverter.GetBytes(ssrc); + + if (BitConverter.IsLittleEndian) + { + Array.Reverse(typeByte); + Array.Reverse(lengthByte); + Array.Reverse(ssrcByte); + } + + typeByte.CopyTo(packet, 0); + lengthByte.CopyTo(packet, 2); + ssrcByte.CopyTo(packet, 4); + // https://discord.com/developers/docs/topics/voice-connections#ip-discovery + } + + void ReadPacket(byte[] packet, out System.Net.IPAddress decodedIp, out ushort decodedPort) + { + Span packetSpan = packet.AsSpan(); + + // the packet we received in this step should be the IP discovery response. + + // it has the same format as PreparePacket. All we really need is IP + port so we strip it from + // the response here, which are the last 6 bytes (4 for ip, 2 for port (ushort)) + + string ipString = Utilities.UTF8.GetString(packet, 8, 64 /* 74 - 6 */).TrimEnd('\0'); + decodedIp = System.Net.IPAddress.Parse(ipString); + decodedPort = BinaryPrimitives.ReadUInt16LittleEndian(packetSpan[72 /* 74 - 2 */..]); + } + + // Select voice encryption mode + KeyValuePair selectedEncryptionMode = Sodium.SelectMode(voiceReady.Modes); + this.SelectedEncryptionMode = selectedEncryptionMode.Value; + + // Ready + this.Discord.Logger.LogTrace(VoiceNextEvents.VoiceHandshake, "Selected encryption mode is {EncryptionMode}", selectedEncryptionMode.Key); + VoiceDispatch vsp = new() + { + OpCode = 1, + Payload = new VoiceSelectProtocolPayload + { + Protocol = "udp", + Data = new VoiceSelectProtocolPayloadData + { + Address = this.DiscoveredEndpoint.Address.ToString(), + Port = (ushort)this.DiscoveredEndpoint.Port, + Mode = selectedEncryptionMode.Key + } + } + }; + string vsj = JsonConvert.SerializeObject(vsp, Formatting.None); + await WsSendAsync(vsj); + + this.SenderTokenSource = new CancellationTokenSource(); + this.SenderTask = Task.Run(VoiceSenderTaskAsync, this.SenderToken); + + this.ReceiverTokenSource = new CancellationTokenSource(); + this.ReceiverTask = Task.Run(UdpReceiverTaskAsync, this.ReceiverToken); + } + + private async Task Stage2Async(VoiceSessionDescriptionPayload voiceSessionDescription) + { + this.SelectedEncryptionMode = Sodium.SupportedModes[voiceSessionDescription.Mode.ToLowerInvariant()]; + this.Discord.Logger.LogTrace(VoiceNextEvents.VoiceHandshake, "Discord updated encryption mode - new mode is {EncryptionMode}", this.SelectedEncryptionMode); + + // start keepalive + this.KeepaliveTokenSource = new CancellationTokenSource(); + this.KeepaliveTask = KeepaliveAsync(); + + // send 3 packets of silence to get things going + byte[] nullpcm = new byte[this.AudioFormat.CalculateSampleSize(20)]; + for (int i = 0; i < 3; i++) + { + byte[] nullPcm = new byte[nullpcm.Length]; + Memory nullpacketmem = nullPcm.AsMemory(); + await EnqueuePacketAsync(new RawVoicePacket(nullpacketmem, 20, true)); + } + + this.IsInitialized = true; + this.ReadyWait.SetResult(true); + } + + private async Task HandleDispatchAsync(JObject jo) + { + int opc = (int)jo["op"]; + JObject? opp = jo["d"] as JObject; + + switch (opc) + { + case 2: // READY + this.Discord.Logger.LogTrace(VoiceNextEvents.VoiceDispatch, "Received READY (OP2)"); + VoiceReadyPayload vrp = opp.ToDiscordObject(); + 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(HeartbeatAsync); + await Stage1Async(vrp); + break; + + case 4: // SESSION_DESCRIPTION + this.Discord.Logger.LogTrace(VoiceNextEvents.VoiceDispatch, "Received SESSION_DESCRIPTION (OP4)"); + VoiceSessionDescriptionPayload vsd = opp.ToDiscordObject(); + this.Key = vsd.SecretKey; + this.Sodium = new Sodium(this.Key.AsMemory()); + await Stage2Async(vsd); + 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)"); + VoiceSpeakingPayload spd = opp.ToDiscordObject(); + bool foundUserInCache = this.Discord.TryGetCachedUserInternal(spd.UserId.Value, out DiscordUser? resolvedUser); + UserSpeakingEventArgs spk = new() + { + Speaking = spd.Speaking, + SSRC = spd.SSRC.Value, + User = resolvedUser, + }; + + if (foundUserInCache && this.TransmittingSSRCs.TryGetValue(spk.SSRC, out AudioSender? txssrc5) && txssrc5.Id == 0) + { + txssrc5.User = spk.User; + } + else + { + OpusDecoder opus = this.Opus.CreateDecoder(); + AudioSender vtx = new(spk.SSRC, opus) + { + User = await this.Discord.GetUserAsync(spd.UserId.Value) + }; + + if (!this.TransmittingSSRCs.TryAdd(spk.SSRC, vtx)) + { + this.Opus.DestroyDecoder(opus); + } + } + + await this.userSpeaking.InvokeAsync(this, spk); + break; + + case 6: // HEARTBEAT ACK + DateTime dt = DateTime.Now; + int ping = (int)(dt - this.LastHeartbeat).TotalMilliseconds; + Volatile.Write(ref this.wsPing, ping); + this.Discord.Logger.LogTrace(VoiceNextEvents.VoiceDispatch, "Received HEARTBEAT_ACK (OP6, {Heartbeat}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"].ToDiscordObject(); + break; + + case 9: // RESUMED + this.Discord.Logger.LogTrace(VoiceNextEvents.VoiceDispatch, "Received RESUMED (OP9)"); + this.HeartbeatTask = Task.Run(HeartbeatAsync); + break; + + case 12: // CLIENT_CONNECTED + this.Discord.Logger.LogTrace(VoiceNextEvents.VoiceDispatch, "Received CLIENT_CONNECTED (OP12)"); + VoiceUserJoinPayload ujpd = opp.ToDiscordObject(); + DiscordUser usrj = await this.Discord.GetUserAsync(ujpd.UserId); + { + OpusDecoder opus = this.Opus.CreateDecoder(); + AudioSender vtx = new(ujpd.SSRC, opus) + { + User = usrj + }; + + if (!this.TransmittingSSRCs.TryAdd(vtx.SSRC, vtx)) + { + this.Opus.DestroyDecoder(opus); + } + } + + await this.userJoined.InvokeAsync(this, new VoiceUserJoinEventArgs { User = usrj, SSRC = ujpd.SSRC }); + break; + + case 13: // CLIENT_DISCONNECTED + this.Discord.Logger.LogTrace(VoiceNextEvents.VoiceDispatch, "Received CLIENT_DISCONNECTED (OP13)"); + VoiceUserLeavePayload ulpd = opp.ToDiscordObject(); + KeyValuePair txssrc = this.TransmittingSSRCs.FirstOrDefault(x => x.Value.Id == ulpd.UserId); + if (this.TransmittingSSRCs.ContainsKey(txssrc.Key)) + { + this.TransmittingSSRCs.TryRemove(txssrc.Key, out AudioSender? txssrc13); + this.Opus.DestroyDecoder(txssrc13.Decoder); + } + + DiscordUser usrl = await this.Discord.GetUserAsync(ulpd.UserId); + await this.userLeft.InvokeAsync(this, new VoiceUserLeaveEventArgs + { + User = usrl, + SSRC = txssrc.Key + }); + break; + + default: + this.Discord.Logger.LogTrace(VoiceNextEvents.VoiceDispatch, "Received unknown voice opcode (OP{Op})", opc); + break; + } + } + + private async Task VoiceWS_SocketClosedAsync(IWebSocketClient client, SocketClosedEventArgs e) + { + this.Discord.Logger.LogDebug(VoiceNextEvents.VoiceConnectionClose, "Voice WebSocket closed ({CloseCode}, '{CloseMessage}')", 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 is 4006 or 4009) + { + this.Resume = false; + } + + if (!this.IsDisposed) + { + this.TokenSource.Cancel(); + this.TokenSource = new CancellationTokenSource(); + this.VoiceWs = new WebSocketClient(new DefaultClientErrorHandler(this.Discord.Logger)); + this.VoiceWs.Disconnected += VoiceWS_SocketClosedAsync; + this.VoiceWs.MessageReceived += VoiceWS_SocketMessage; + this.VoiceWs.Connected += VoiceWS_SocketOpened; + + if (this.Resume) // emzi you dipshit + { + await ConnectAsync(); + } + } + } + + private Task VoiceWS_SocketMessage(IWebSocketClient client, SocketMessageEventArgs e) + { + if (e is not SocketTextMessageEventArgs et) + { + this.Discord.Logger.LogCritical(VoiceNextEvents.VoiceGatewayError, "Discord Voice Gateway sent binary data - unable to process"); + return Task.CompletedTask; + } + + this.Discord.Logger.LogTrace(VoiceNextEvents.VoiceWsRx, "{WebsocketMessage}", et.Message); + return HandleDispatchAsync(JObject.Parse(et.Message)); + } + + private Task VoiceWS_SocketOpened(IWebSocketClient client, SocketEventArgs e) + => StartAsync(); + + private Task VoiceWs_SocketException(IWebSocketClient client, SocketErrorEventArgs e) + => this.voiceSocketError.InvokeAsync(this, new SocketErrorEventArgs { Exception = e.Exception }); + + private async Task WsSendAsync(string payload) + { + this.Discord.Logger.LogTrace(VoiceNextEvents.VoiceWsTx, "{WebsocketPayload}", payload); + await this.VoiceWs.SendMessageAsync(payload); + } + + private static uint UnixTimestamp(DateTime dt) + { + TimeSpan ts = dt - UnixEpoch; + double sd = ts.TotalSeconds; + uint si = (uint)sd; + return si; + } +} + +// Naam you still owe me those noodles :^) +// I remember +// Alexa, how much is shipping to emzi +// NL -> PL is 18.50€ for packages <=2kg it seems (https://www.postnl.nl/en/mail-and-parcels/parcels/international-parcel/) diff --git a/DSharpPlus.VoiceNext/VoiceNextEvents.cs b/DSharpPlus.VoiceNext/VoiceNextEvents.cs index a6e97f7e47..5d9a685d0d 100644 --- a/DSharpPlus.VoiceNext/VoiceNextEvents.cs +++ b/DSharpPlus.VoiceNext/VoiceNextEvents.cs @@ -1,59 +1,59 @@ -using Microsoft.Extensions.Logging; - -namespace DSharpPlus.VoiceNext; - -/// -/// Contains well-defined event IDs used by the VoiceNext extension. -/// -public static class VoiceNextEvents -{ - /// - /// Miscellaneous events, that do not fit in any other category. - /// - public static EventId Misc { get; } = new EventId(300, "VoiceNext"); - - /// - /// Events pertaining to Voice Gateway connection lifespan, specifically, heartbeats. - /// - public static EventId VoiceHeartbeat { get; } = new EventId(301, nameof(VoiceHeartbeat)); - - /// - /// Events pertaining to Voice Gateway connection early lifespan, specifically, the establishing thereof as well as negotiating various modes. - /// - public static EventId VoiceHandshake { get; } = new EventId(302, nameof(VoiceHandshake)); - - /// - /// Events emitted when incoming voice data is corrupted, or packets are being dropped. - /// - public static EventId VoiceReceiveFailure { get; } = new EventId(303, nameof(VoiceReceiveFailure)); - - /// - /// Events pertaining to UDP connection lifespan, specifically the keepalive (or heartbeats). - /// - public static EventId VoiceKeepalive { get; } = new EventId(304, nameof(VoiceKeepalive)); - - /// - /// Events emitted for high-level dispatch receive events. - /// - public static EventId VoiceDispatch { get; } = new EventId(305, nameof(VoiceDispatch)); - - /// - /// Events emitted for Voice Gateway connection closes, clean or otherwise. - /// - public static EventId VoiceConnectionClose { get; } = new EventId(306, nameof(VoiceConnectionClose)); - - /// - /// Events emitted when decoding data received via Voice Gateway fails for any reason. - /// - public static EventId VoiceGatewayError { get; } = new EventId(307, nameof(VoiceGatewayError)); - - /// - /// Events containing raw (but decompressed) payloads, received from Discord Voice Gateway. - /// - public static EventId VoiceWsRx { get; } = new EventId(308, "Voice ↓"); - - /// - /// Events containing raw payloads, as they're being sent to Discord Voice Gateway. - /// - public static EventId VoiceWsTx { get; } = new EventId(309, "Voice ↑"); -} +using Microsoft.Extensions.Logging; + +namespace DSharpPlus.VoiceNext; + +/// +/// Contains well-defined event IDs used by the VoiceNext extension. +/// +public static class VoiceNextEvents +{ + /// + /// Miscellaneous events, that do not fit in any other category. + /// + public static EventId Misc { get; } = new EventId(300, "VoiceNext"); + + /// + /// Events pertaining to Voice Gateway connection lifespan, specifically, heartbeats. + /// + public static EventId VoiceHeartbeat { get; } = new EventId(301, nameof(VoiceHeartbeat)); + + /// + /// Events pertaining to Voice Gateway connection early lifespan, specifically, the establishing thereof as well as negotiating various modes. + /// + public static EventId VoiceHandshake { get; } = new EventId(302, nameof(VoiceHandshake)); + + /// + /// Events emitted when incoming voice data is corrupted, or packets are being dropped. + /// + public static EventId VoiceReceiveFailure { get; } = new EventId(303, nameof(VoiceReceiveFailure)); + + /// + /// Events pertaining to UDP connection lifespan, specifically the keepalive (or heartbeats). + /// + public static EventId VoiceKeepalive { get; } = new EventId(304, nameof(VoiceKeepalive)); + + /// + /// Events emitted for high-level dispatch receive events. + /// + public static EventId VoiceDispatch { get; } = new EventId(305, nameof(VoiceDispatch)); + + /// + /// Events emitted for Voice Gateway connection closes, clean or otherwise. + /// + public static EventId VoiceConnectionClose { get; } = new EventId(306, nameof(VoiceConnectionClose)); + + /// + /// Events emitted when decoding data received via Voice Gateway fails for any reason. + /// + public static EventId VoiceGatewayError { get; } = new EventId(307, nameof(VoiceGatewayError)); + + /// + /// Events containing raw (but decompressed) payloads, received from Discord Voice Gateway. + /// + public static EventId VoiceWsRx { get; } = new EventId(308, "Voice ↓"); + + /// + /// Events containing raw payloads, as they're being sent to Discord Voice Gateway. + /// + public static EventId VoiceWsTx { get; } = new EventId(309, "Voice ↑"); +} diff --git a/DSharpPlus.VoiceNext/VoiceNextExtension.cs b/DSharpPlus.VoiceNext/VoiceNextExtension.cs index 904c429867..64a2417915 100644 --- a/DSharpPlus.VoiceNext/VoiceNextExtension.cs +++ b/DSharpPlus.VoiceNext/VoiceNextExtension.cs @@ -1,236 +1,236 @@ -using System; -using System.Collections.Concurrent; -using System.Threading.Tasks; - -using DSharpPlus.Entities; -using DSharpPlus.EventArgs; -using DSharpPlus.Net; -using DSharpPlus.Net.Abstractions; -using DSharpPlus.VoiceNext.Entities; - -namespace DSharpPlus.VoiceNext; - -/// -/// Represents VoiceNext extension, which acts as Discord voice client. -/// -public sealed class VoiceNextExtension : IDisposable -{ - private VoiceNextConfiguration Configuration { get; set; } - - private ConcurrentDictionary ActiveConnections { get; set; } - private ConcurrentDictionary> VoiceStateUpdates { get; set; } - private ConcurrentDictionary> VoiceServerUpdates { get; set; } - - /// - /// Gets whether this connection has incoming voice enabled. - /// - public bool IsIncomingEnabled { get; } - public DiscordClient Client { get; private set; } - - internal VoiceNextExtension(VoiceNextConfiguration config) - { - this.Configuration = new VoiceNextConfiguration(config); - this.IsIncomingEnabled = config.EnableIncoming; - - this.ActiveConnections = new ConcurrentDictionary(); - this.VoiceStateUpdates = new ConcurrentDictionary>(); - this.VoiceServerUpdates = new ConcurrentDictionary>(); - } - - /// - /// DO NOT USE THIS MANUALLY. - /// - /// DO NOT USE THIS MANUALLY. - /// - public void Setup(DiscordClient client) - { - if (this.Client != null) - { - throw new InvalidOperationException("What did I tell you?"); - } - - this.Client = client; - } - - /// - /// 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 is not DiscordChannelType.Voice and not DiscordChannelType.Stage) - { - throw new ArgumentException("Invalid channel specified; needs to be voice or stage channel", nameof(channel)); - } - - if (channel.Guild is null) - { - throw new ArgumentException("Invalid channel specified; needs to be guild channel", nameof(channel)); - } - - if (!channel.PermissionsFor(channel.Guild.CurrentMember).HasAllPermissions(DiscordPermission.ViewChannel, DiscordPermission.Connect)) - { - throw new InvalidOperationException("You need AccessChannels and UseVoice permission to connect to this voice channel"); - } - - DiscordGuild gld = channel.Guild; - if (this.ActiveConnections.ContainsKey(gld.Id)) - { - throw new InvalidOperationException("This guild already has a voice connection"); - } - - TaskCompletionSource vstut = new(); - TaskCompletionSource vsrut = new(); - this.VoiceStateUpdates[gld.Id] = vstut; - this.VoiceServerUpdates[gld.Id] = vsrut; - - VoiceStateUpdatePayload payload = new() - { - GuildId = gld.Id, - ChannelId = channel.Id, - Deafened = false, - Muted = false - }; - -#pragma warning disable DSP0004 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. - await (channel.Discord as DiscordClient).SendPayloadAsync(GatewayOpCode.VoiceStateUpdate, payload, gld.Id); -#pragma warning restore DSP0004 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. - - VoiceStateUpdatedEventArgs vstu = await vstut.Task; - VoiceStateUpdatePayload vstup = new() - { - SessionId = vstu.SessionId, - UserId = vstu.After.UserId - }; - VoiceServerUpdatedEventArgs vsru = await vsrut.Task; - VoiceServerUpdatePayload vsrup = new() - { - Endpoint = vsru.Endpoint, - GuildId = vsru.Guild.Id, - Token = vsru.VoiceToken - }; - - VoiceNextConnection vnc = new(this.Client, gld, channel, this.Configuration, vsrup, vstup); - vnc.VoiceDisconnected += Vnc_VoiceDisconnectedAsync; - await vnc.ConnectAsync(); - await vnc.WaitForReadyAsync(); - 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.TryGetValue(guild.Id, out VoiceNextConnection value) ? value : null; - - private async Task Vnc_VoiceDisconnectedAsync(DiscordGuild guild) - { - if (this.ActiveConnections.ContainsKey(guild.Id)) - { - this.ActiveConnections.TryRemove(guild.Id, out _); - } - - VoiceStateUpdatePayload payload = new() - { - GuildId = guild.Id, - ChannelId = null - }; - -#pragma warning disable DSP0004 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. - await (guild.Discord as DiscordClient).SendPayloadAsync(GatewayOpCode.VoiceStateUpdate, payload, guild.Id); -#pragma warning restore DSP0004 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. - } - - internal async Task Client_VoiceStateUpdate(DiscordClient client, VoiceStateUpdatedEventArgs e) - { - DiscordGuild? gld = await e.After.GetGuildAsync(); - if (gld is null) - { - return; - } - - if (e.After.UserId == this.Client.CurrentUser.Id) - { - if (e.After.ChannelId == null && this.ActiveConnections.TryRemove(gld.Id, out VoiceNextConnection? ac)) - { - ac.Disconnect(); - } - - DiscordChannel? channel = await e.After.GetChannelAsync(); - - if (e.After.GuildId is not null && - e.After.ChannelId is not null && - this.ActiveConnections.TryGetValue(e.After.GuildId.Value, out VoiceNextConnection? vnc)) - { - vnc.TargetChannel = channel!; - } - - if (!string.IsNullOrWhiteSpace(e.SessionId) && - channel is not null && - this.VoiceStateUpdates.TryRemove(gld.Id, out TaskCompletionSource? xe)) - { - xe.SetResult(e); - } - } - } - - internal async Task Client_VoiceServerUpdateAsync(DiscordClient client, VoiceServerUpdatedEventArgs e) - { - DiscordGuild gld = e.Guild; - if (gld == null) - { - return; - } - - if (this.ActiveConnections.TryGetValue(e.Guild.Id, out VoiceNextConnection? vnc)) - { - vnc.ServerData = new VoiceServerUpdatePayload - { - Endpoint = e.Endpoint, - GuildId = e.Guild.Id, - Token = e.VoiceToken - }; - - string eps = e.Endpoint; - int epi = eps.LastIndexOf(':'); - string eph; - int 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(); - } - - if (this.VoiceServerUpdates.ContainsKey(gld.Id)) - { - this.VoiceServerUpdates.TryRemove(gld.Id, out TaskCompletionSource? xe); - xe.SetResult(e); - } - } - - public void Dispose() - { - foreach (System.Collections.Generic.KeyValuePair conn in this.ActiveConnections) - { - conn.Value?.Dispose(); - } - - // Lo and behold, the audacious man who dared lay his hand upon VoiceNext hath once more trespassed upon its profane ground! - - // Satisfy rule CA1816. Can be removed if this class is sealed. - GC.SuppressFinalize(this); - } -} +using System; +using System.Collections.Concurrent; +using System.Threading.Tasks; + +using DSharpPlus.Entities; +using DSharpPlus.EventArgs; +using DSharpPlus.Net; +using DSharpPlus.Net.Abstractions; +using DSharpPlus.VoiceNext.Entities; + +namespace DSharpPlus.VoiceNext; + +/// +/// Represents VoiceNext extension, which acts as Discord voice client. +/// +public sealed class VoiceNextExtension : IDisposable +{ + private VoiceNextConfiguration Configuration { get; set; } + + private ConcurrentDictionary ActiveConnections { get; set; } + private ConcurrentDictionary> VoiceStateUpdates { get; set; } + private ConcurrentDictionary> VoiceServerUpdates { get; set; } + + /// + /// Gets whether this connection has incoming voice enabled. + /// + public bool IsIncomingEnabled { get; } + public DiscordClient Client { get; private set; } + + internal VoiceNextExtension(VoiceNextConfiguration config) + { + this.Configuration = new VoiceNextConfiguration(config); + this.IsIncomingEnabled = config.EnableIncoming; + + this.ActiveConnections = new ConcurrentDictionary(); + this.VoiceStateUpdates = new ConcurrentDictionary>(); + this.VoiceServerUpdates = new ConcurrentDictionary>(); + } + + /// + /// DO NOT USE THIS MANUALLY. + /// + /// DO NOT USE THIS MANUALLY. + /// + public void Setup(DiscordClient client) + { + if (this.Client != null) + { + throw new InvalidOperationException("What did I tell you?"); + } + + this.Client = client; + } + + /// + /// 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 is not DiscordChannelType.Voice and not DiscordChannelType.Stage) + { + throw new ArgumentException("Invalid channel specified; needs to be voice or stage channel", nameof(channel)); + } + + if (channel.Guild is null) + { + throw new ArgumentException("Invalid channel specified; needs to be guild channel", nameof(channel)); + } + + if (!channel.PermissionsFor(channel.Guild.CurrentMember).HasAllPermissions(DiscordPermission.ViewChannel, DiscordPermission.Connect)) + { + throw new InvalidOperationException("You need AccessChannels and UseVoice permission to connect to this voice channel"); + } + + DiscordGuild gld = channel.Guild; + if (this.ActiveConnections.ContainsKey(gld.Id)) + { + throw new InvalidOperationException("This guild already has a voice connection"); + } + + TaskCompletionSource vstut = new(); + TaskCompletionSource vsrut = new(); + this.VoiceStateUpdates[gld.Id] = vstut; + this.VoiceServerUpdates[gld.Id] = vsrut; + + VoiceStateUpdatePayload payload = new() + { + GuildId = gld.Id, + ChannelId = channel.Id, + Deafened = false, + Muted = false + }; + +#pragma warning disable DSP0004 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + await (channel.Discord as DiscordClient).SendPayloadAsync(GatewayOpCode.VoiceStateUpdate, payload, gld.Id); +#pragma warning restore DSP0004 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + + VoiceStateUpdatedEventArgs vstu = await vstut.Task; + VoiceStateUpdatePayload vstup = new() + { + SessionId = vstu.SessionId, + UserId = vstu.After.UserId + }; + VoiceServerUpdatedEventArgs vsru = await vsrut.Task; + VoiceServerUpdatePayload vsrup = new() + { + Endpoint = vsru.Endpoint, + GuildId = vsru.Guild.Id, + Token = vsru.VoiceToken + }; + + VoiceNextConnection vnc = new(this.Client, gld, channel, this.Configuration, vsrup, vstup); + vnc.VoiceDisconnected += Vnc_VoiceDisconnectedAsync; + await vnc.ConnectAsync(); + await vnc.WaitForReadyAsync(); + 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.TryGetValue(guild.Id, out VoiceNextConnection value) ? value : null; + + private async Task Vnc_VoiceDisconnectedAsync(DiscordGuild guild) + { + if (this.ActiveConnections.ContainsKey(guild.Id)) + { + this.ActiveConnections.TryRemove(guild.Id, out _); + } + + VoiceStateUpdatePayload payload = new() + { + GuildId = guild.Id, + ChannelId = null + }; + +#pragma warning disable DSP0004 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + await (guild.Discord as DiscordClient).SendPayloadAsync(GatewayOpCode.VoiceStateUpdate, payload, guild.Id); +#pragma warning restore DSP0004 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + } + + internal async Task Client_VoiceStateUpdate(DiscordClient client, VoiceStateUpdatedEventArgs e) + { + DiscordGuild? gld = await e.After.GetGuildAsync(); + if (gld is null) + { + return; + } + + if (e.After.UserId == this.Client.CurrentUser.Id) + { + if (e.After.ChannelId == null && this.ActiveConnections.TryRemove(gld.Id, out VoiceNextConnection? ac)) + { + ac.Disconnect(); + } + + DiscordChannel? channel = await e.After.GetChannelAsync(); + + if (e.After.GuildId is not null && + e.After.ChannelId is not null && + this.ActiveConnections.TryGetValue(e.After.GuildId.Value, out VoiceNextConnection? vnc)) + { + vnc.TargetChannel = channel!; + } + + if (!string.IsNullOrWhiteSpace(e.SessionId) && + channel is not null && + this.VoiceStateUpdates.TryRemove(gld.Id, out TaskCompletionSource? xe)) + { + xe.SetResult(e); + } + } + } + + internal async Task Client_VoiceServerUpdateAsync(DiscordClient client, VoiceServerUpdatedEventArgs e) + { + DiscordGuild gld = e.Guild; + if (gld == null) + { + return; + } + + if (this.ActiveConnections.TryGetValue(e.Guild.Id, out VoiceNextConnection? vnc)) + { + vnc.ServerData = new VoiceServerUpdatePayload + { + Endpoint = e.Endpoint, + GuildId = e.Guild.Id, + Token = e.VoiceToken + }; + + string eps = e.Endpoint; + int epi = eps.LastIndexOf(':'); + string eph; + int 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(); + } + + if (this.VoiceServerUpdates.ContainsKey(gld.Id)) + { + this.VoiceServerUpdates.TryRemove(gld.Id, out TaskCompletionSource? xe); + xe.SetResult(e); + } + } + + public void Dispose() + { + foreach (System.Collections.Generic.KeyValuePair conn in this.ActiveConnections) + { + conn.Value?.Dispose(); + } + + // Lo and behold, the audacious man who dared lay his hand upon VoiceNext hath once more trespassed upon its profane ground! + + // Satisfy rule CA1816. Can be removed if this class is sealed. + GC.SuppressFinalize(this); + } +} diff --git a/DSharpPlus.VoiceNext/VoiceTransmitSink.cs b/DSharpPlus.VoiceNext/VoiceTransmitSink.cs index 64f4d0cd20..975fab79eb 100644 --- a/DSharpPlus.VoiceNext/VoiceTransmitSink.cs +++ b/DSharpPlus.VoiceNext/VoiceTransmitSink.cs @@ -1,235 +1,235 @@ -using System; -using System.Buffers; -using System.Collections.Generic; -using System.Runtime.InteropServices; -using System.Threading; -using System.Threading.Tasks; -using DSharpPlus.VoiceNext.Codec; - -namespace DSharpPlus.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; - - /// - /// 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; - - /// - /// 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 is < 0 or > 2.5) - { - throw new ArgumentOutOfRangeException(nameof(value), "Volume needs to be between 0% and 250%."); - } - - this.volume = value; - } - } - private double volume = 1.0; - - private VoiceNextConnection Connection { get; } - private int PcmBufferDuration { get; } - private byte[] PcmBuffer { get; } - private Memory PcmMemory { get; } - private int PcmBufferLength { get; set; } - private SemaphoreSlim WriteSemaphore { get; } - private List Filters { get; } - - 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 = []; - } - - /// - /// 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 WriteAsync(new ReadOnlyMemory(buffer, offset, count), cancellationToken); - - /// - /// 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); - - try - { - int remaining = buffer.Length; - ReadOnlyMemory buffSpan = buffer; - Memory pcmSpan = this.PcmMemory; - - while (remaining > 0) - { - int len = Math.Min(pcmSpan.Length - this.PcmBufferLength, remaining); - - Memory tgt = pcmSpan[this.PcmBufferLength..]; - ReadOnlyMemory src = buffSpan[..len]; - - src.CopyTo(tgt); - this.PcmBufferLength += len; - remaining -= len; - buffSpan = buffSpan[len..]; - - if (this.PcmBufferLength == this.PcmBuffer.Length) - { - ApplyFiltersSync(pcmSpan); - - this.PcmBufferLength = 0; - - byte[] packet = ArrayPool.Shared.Rent(this.PcmMemory.Length); - Memory packetMemory = packet.AsMemory(0, this.PcmMemory.Length); - this.PcmMemory.CopyTo(packetMemory); - - await this.Connection.EnqueuePacketAsync(new RawVoicePacket(packetMemory, this.PcmBufferDuration, false, packet), cancellationToken); - } - } - } - finally - { - 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) - { - Memory pcm = this.PcmMemory; - Helpers.ZeroFill(pcm[this.PcmBufferLength..].Span); - - ApplyFiltersSync(pcm); - - byte[] packet = ArrayPool.Shared.Rent(pcm.Length); - Memory packetMemory = packet.AsMemory(0, pcm.Length); - pcm.CopyTo(packetMemory); - - await this.Connection.EnqueuePacketAsync(new RawVoicePacket(packetMemory, this.PcmBufferDuration, false, packet), cancellationToken); - } - - /// - /// Pauses playback. - /// - public void Pause() - => this.Connection.Pause(); - - /// - /// Resumes playback. - /// - /// - public async Task ResumeAsync() - => await this.Connection.ResumeAsync(); - - /// - /// Gets the collection of installed PCM filters, in order of their execution. - /// - /// Installed PCM filters, in order of execution. - public IEnumerable GetInstalledFilters() - { - foreach (IVoiceFilter 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) - { - ArgumentNullException.ThrowIfNull(filter); - if (order < 0) - { - throw new ArgumentOutOfRangeException(nameof(order), "Filter order must be greater than or equal to 0."); - } - - lock (this.Filters) - { - List 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) - { - ArgumentNullException.ThrowIfNull(filter); - lock (this.Filters) - { - List filters = this.Filters; - return filters.Contains(filter) && filters.Remove(filter); - } - } - - private void ApplyFiltersSync(Memory pcmSpan) - { - Span pcm16 = MemoryMarshal.Cast(pcmSpan.Span); - - // pass through any filters, if applicable - lock (this.Filters) - { - if (this.Filters.Count != 0) - { - foreach (IVoiceFilter filter in this.Filters) - { - filter.Transform(pcm16, this.Connection.AudioFormat, this.SampleDuration); - } - } - } - - if (this.VolumeModifier != 1) - { - // alter volume - for (int i = 0; i < pcm16.Length; i++) - { - pcm16[i] = (short)(pcm16[i] * this.VolumeModifier); - } - } - } - - public void Dispose() - => this.WriteSemaphore?.Dispose(); -} +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using DSharpPlus.VoiceNext.Codec; + +namespace DSharpPlus.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; + + /// + /// 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; + + /// + /// 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 is < 0 or > 2.5) + { + throw new ArgumentOutOfRangeException(nameof(value), "Volume needs to be between 0% and 250%."); + } + + this.volume = value; + } + } + private double volume = 1.0; + + private VoiceNextConnection Connection { get; } + private int PcmBufferDuration { get; } + private byte[] PcmBuffer { get; } + private Memory PcmMemory { get; } + private int PcmBufferLength { get; set; } + private SemaphoreSlim WriteSemaphore { get; } + private List Filters { get; } + + 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 = []; + } + + /// + /// 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 WriteAsync(new ReadOnlyMemory(buffer, offset, count), cancellationToken); + + /// + /// 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); + + try + { + int remaining = buffer.Length; + ReadOnlyMemory buffSpan = buffer; + Memory pcmSpan = this.PcmMemory; + + while (remaining > 0) + { + int len = Math.Min(pcmSpan.Length - this.PcmBufferLength, remaining); + + Memory tgt = pcmSpan[this.PcmBufferLength..]; + ReadOnlyMemory src = buffSpan[..len]; + + src.CopyTo(tgt); + this.PcmBufferLength += len; + remaining -= len; + buffSpan = buffSpan[len..]; + + if (this.PcmBufferLength == this.PcmBuffer.Length) + { + ApplyFiltersSync(pcmSpan); + + this.PcmBufferLength = 0; + + byte[] packet = ArrayPool.Shared.Rent(this.PcmMemory.Length); + Memory packetMemory = packet.AsMemory(0, this.PcmMemory.Length); + this.PcmMemory.CopyTo(packetMemory); + + await this.Connection.EnqueuePacketAsync(new RawVoicePacket(packetMemory, this.PcmBufferDuration, false, packet), cancellationToken); + } + } + } + finally + { + 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) + { + Memory pcm = this.PcmMemory; + Helpers.ZeroFill(pcm[this.PcmBufferLength..].Span); + + ApplyFiltersSync(pcm); + + byte[] packet = ArrayPool.Shared.Rent(pcm.Length); + Memory packetMemory = packet.AsMemory(0, pcm.Length); + pcm.CopyTo(packetMemory); + + await this.Connection.EnqueuePacketAsync(new RawVoicePacket(packetMemory, this.PcmBufferDuration, false, packet), cancellationToken); + } + + /// + /// Pauses playback. + /// + public void Pause() + => this.Connection.Pause(); + + /// + /// Resumes playback. + /// + /// + public async Task ResumeAsync() + => await this.Connection.ResumeAsync(); + + /// + /// Gets the collection of installed PCM filters, in order of their execution. + /// + /// Installed PCM filters, in order of execution. + public IEnumerable GetInstalledFilters() + { + foreach (IVoiceFilter 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) + { + ArgumentNullException.ThrowIfNull(filter); + if (order < 0) + { + throw new ArgumentOutOfRangeException(nameof(order), "Filter order must be greater than or equal to 0."); + } + + lock (this.Filters) + { + List 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) + { + ArgumentNullException.ThrowIfNull(filter); + lock (this.Filters) + { + List filters = this.Filters; + return filters.Contains(filter) && filters.Remove(filter); + } + } + + private void ApplyFiltersSync(Memory pcmSpan) + { + Span pcm16 = MemoryMarshal.Cast(pcmSpan.Span); + + // pass through any filters, if applicable + lock (this.Filters) + { + if (this.Filters.Count != 0) + { + foreach (IVoiceFilter filter in this.Filters) + { + filter.Transform(pcm16, this.Connection.AudioFormat, this.SampleDuration); + } + } + } + + if (this.VolumeModifier != 1) + { + // alter volume + for (int i = 0; i < pcm16.Length; i++) + { + pcm16[i] = (short)(pcm16[i] * this.VolumeModifier); + } + } + } + + public void Dispose() + => this.WriteSemaphore?.Dispose(); +} diff --git a/DSharpPlus/AnsiColor.cs b/DSharpPlus/AnsiColor.cs index 0b04e019f3..f69b9ba238 100644 --- a/DSharpPlus/AnsiColor.cs +++ b/DSharpPlus/AnsiColor.cs @@ -1,36 +1,36 @@ -namespace DSharpPlus; - - -/// -/// A list of ansi colors supported by Discord. -/// -/// -/// Background support in the client is dodgy at best. -/// These colors are mapped as per the ansi standard, but may not appear correctly in the client. -/// -public enum AnsiColor -{ - Reset = 0, - Bold = 1, - Underline = 4, - - Black = 30, - Red = 31, - Green = 32, - Yellow = 33, - Blue = 34, - Magenta = 35, - Cyan = 36, - White = 37, - LightGray = 38, - - BlackBackground = 40, - RedBackground = 41, - GreenBackground = 42, - YellowBackground = 43, - BlueBackground = 44, - MagentaBackground = 45, - CyanBackground = 46, - WhiteBackground = 47, - -} +namespace DSharpPlus; + + +/// +/// A list of ansi colors supported by Discord. +/// +/// +/// Background support in the client is dodgy at best. +/// These colors are mapped as per the ansi standard, but may not appear correctly in the client. +/// +public enum AnsiColor +{ + Reset = 0, + Bold = 1, + Underline = 4, + + Black = 30, + Red = 31, + Green = 32, + Yellow = 33, + Blue = 34, + Magenta = 35, + Cyan = 36, + White = 37, + LightGray = 38, + + BlackBackground = 40, + RedBackground = 41, + GreenBackground = 42, + YellowBackground = 43, + BlueBackground = 44, + MagentaBackground = 45, + CyanBackground = 46, + WhiteBackground = 47, + +} diff --git a/DSharpPlus/AsyncEvents/AsyncEvent.cs b/DSharpPlus/AsyncEvents/AsyncEvent.cs index 0b88bbe4f1..a4a4c92995 100644 --- a/DSharpPlus/AsyncEvents/AsyncEvent.cs +++ b/DSharpPlus/AsyncEvents/AsyncEvent.cs @@ -1,19 +1,19 @@ -using System; - -namespace DSharpPlus.AsyncEvents; - -/// -/// Represents a non-generic base for async events. -/// -public abstract class AsyncEvent -{ - public string Name { get; } - - protected internal AsyncEvent(string name) => this.Name = name; - - internal abstract void Register(Delegate @delegate); - - internal AsyncEvent As() - where T : AsyncEventArgs - => (AsyncEvent)this; -} +using System; + +namespace DSharpPlus.AsyncEvents; + +/// +/// Represents a non-generic base for async events. +/// +public abstract class AsyncEvent +{ + public string Name { get; } + + protected internal AsyncEvent(string name) => this.Name = name; + + internal abstract void Register(Delegate @delegate); + + internal AsyncEvent As() + where T : AsyncEventArgs + => (AsyncEvent)this; +} diff --git a/DSharpPlus/AsyncEvents/AsyncEventArgs.cs b/DSharpPlus/AsyncEvents/AsyncEventArgs.cs index 16a6c462c6..94467ac797 100644 --- a/DSharpPlus/AsyncEvents/AsyncEventArgs.cs +++ b/DSharpPlus/AsyncEvents/AsyncEventArgs.cs @@ -1,6 +1,6 @@ -namespace DSharpPlus.AsyncEvents; - -/// -/// A base class for arguments passed to an event handler. -/// -public class AsyncEventArgs : System.EventArgs; +namespace DSharpPlus.AsyncEvents; + +/// +/// A base class for arguments passed to an event handler. +/// +public class AsyncEventArgs : System.EventArgs; diff --git a/DSharpPlus/AsyncEvents/AsyncEventHandler.cs b/DSharpPlus/AsyncEvents/AsyncEventHandler.cs index d2a598c145..53b3ccfd23 100644 --- a/DSharpPlus/AsyncEvents/AsyncEventHandler.cs +++ b/DSharpPlus/AsyncEvents/AsyncEventHandler.cs @@ -1,39 +1,39 @@ -using System; -using System.Threading.Tasks; - -namespace DSharpPlus.AsyncEvents; - -/// -/// Provides a registration surface for asynchronous events using C# language event syntax. -/// -/// The type of the event dispatcher. -/// The type of the argument object for this event. -/// The instance that dispatched this event. -/// The arguments passed to this event. -public delegate Task AsyncEventHandler -( - TSender sender, - TArgs args -) - where TArgs : AsyncEventArgs; - -/// -/// Provides a registration surface for a handler for exceptions raised by an async event or its registered -/// event handlers. -/// -/// The type of the event dispatcher. -/// The type of the argument object for this event. -/// The async event that threw this exception. -/// The thrown exception. -/// The async event handler that threw this exception. -/// The instance that dispatched this event. -/// The arguments passed to this event. -public delegate void AsyncEventExceptionHandler -( - AsyncEvent @event, - Exception exception, - AsyncEventHandler handler, - TSender sender, - TArgs args -) - where TArgs : AsyncEventArgs; +using System; +using System.Threading.Tasks; + +namespace DSharpPlus.AsyncEvents; + +/// +/// Provides a registration surface for asynchronous events using C# language event syntax. +/// +/// The type of the event dispatcher. +/// The type of the argument object for this event. +/// The instance that dispatched this event. +/// The arguments passed to this event. +public delegate Task AsyncEventHandler +( + TSender sender, + TArgs args +) + where TArgs : AsyncEventArgs; + +/// +/// Provides a registration surface for a handler for exceptions raised by an async event or its registered +/// event handlers. +/// +/// The type of the event dispatcher. +/// The type of the argument object for this event. +/// The async event that threw this exception. +/// The thrown exception. +/// The async event handler that threw this exception. +/// The instance that dispatched this event. +/// The arguments passed to this event. +public delegate void AsyncEventExceptionHandler +( + AsyncEvent @event, + Exception exception, + AsyncEventHandler handler, + TSender sender, + TArgs args +) + where TArgs : AsyncEventArgs; diff --git a/DSharpPlus/AsyncEvents/AsyncEvent`2.cs b/DSharpPlus/AsyncEvents/AsyncEvent`2.cs index 489169b48d..c6a117a6ad 100644 --- a/DSharpPlus/AsyncEvents/AsyncEvent`2.cs +++ b/DSharpPlus/AsyncEvents/AsyncEvent`2.cs @@ -1,105 +1,105 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -namespace DSharpPlus.AsyncEvents; - -/// -/// Provides an implementation of an asynchronous event. Registered handlers are executed asynchronously, -/// in parallel, and potential exceptions are caught and sent to the specified exception handler. -/// -/// The type of the object to dispatch this event. -/// The type of the argument object for this event. -public sealed class AsyncEvent : AsyncEvent - where TArgs : AsyncEventArgs -{ - private readonly SemaphoreSlim @lock = new(1); - private readonly IClientErrorHandler errorHandler; - private List> handlers; - - public AsyncEvent(IClientErrorHandler errorHandler) - : base(typeof(TArgs).ToString()) - { - this.handlers = []; - this.errorHandler = errorHandler; - } - - /// - /// Registers a new handler for this event. - /// - /// Thrown if the specified handler was null. - public void Register(AsyncEventHandler handler) - { - ArgumentNullException.ThrowIfNull(handler); - this.@lock.Wait(); - try - { - this.handlers.Add(handler); - } - finally - { - this.@lock.Release(); - } - } - - // this serves as a stopgap solution until we address the shortcomings of event dispatch in DiscordClient - internal override void Register(Delegate @delegate) - => Register((AsyncEventHandler)@delegate); - - /// - /// Unregisters a specific handler from this event. - /// - /// Thrown if the specified handler was null. - public void Unregister(AsyncEventHandler handler) - { - ArgumentNullException.ThrowIfNull(handler); - this.@lock.Wait(); - try - { - this.handlers.Remove(handler); - } - finally - { - this.@lock.Release(); - } - } - - /// - /// Unregisters all handlers from this event. - /// - public void UnregisterAll() - => this.handlers = []; - - /// - /// Raises this event, invoking all registered handlers in parallel. - /// - /// The instance that dispatched this event. - /// The arguments passed to this event. - public async Task InvokeAsync(TSender sender, TArgs args) - { - if (this.handlers.Count == 0) - { - return; - } - - await this.@lock.WaitAsync(); - List> copiedHandlers = new(this.handlers); - this.@lock.Release(); - - _ = Task.WhenAll(copiedHandlers.Select(async (handler) => - { - try - { - await handler(sender, args); - } - catch (Exception ex) - { - await this.errorHandler.HandleEventHandlerError(this.Name, ex, handler, sender, args); - } - })); - - return; - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace DSharpPlus.AsyncEvents; + +/// +/// Provides an implementation of an asynchronous event. Registered handlers are executed asynchronously, +/// in parallel, and potential exceptions are caught and sent to the specified exception handler. +/// +/// The type of the object to dispatch this event. +/// The type of the argument object for this event. +public sealed class AsyncEvent : AsyncEvent + where TArgs : AsyncEventArgs +{ + private readonly SemaphoreSlim @lock = new(1); + private readonly IClientErrorHandler errorHandler; + private List> handlers; + + public AsyncEvent(IClientErrorHandler errorHandler) + : base(typeof(TArgs).ToString()) + { + this.handlers = []; + this.errorHandler = errorHandler; + } + + /// + /// Registers a new handler for this event. + /// + /// Thrown if the specified handler was null. + public void Register(AsyncEventHandler handler) + { + ArgumentNullException.ThrowIfNull(handler); + this.@lock.Wait(); + try + { + this.handlers.Add(handler); + } + finally + { + this.@lock.Release(); + } + } + + // this serves as a stopgap solution until we address the shortcomings of event dispatch in DiscordClient + internal override void Register(Delegate @delegate) + => Register((AsyncEventHandler)@delegate); + + /// + /// Unregisters a specific handler from this event. + /// + /// Thrown if the specified handler was null. + public void Unregister(AsyncEventHandler handler) + { + ArgumentNullException.ThrowIfNull(handler); + this.@lock.Wait(); + try + { + this.handlers.Remove(handler); + } + finally + { + this.@lock.Release(); + } + } + + /// + /// Unregisters all handlers from this event. + /// + public void UnregisterAll() + => this.handlers = []; + + /// + /// Raises this event, invoking all registered handlers in parallel. + /// + /// The instance that dispatched this event. + /// The arguments passed to this event. + public async Task InvokeAsync(TSender sender, TArgs args) + { + if (this.handlers.Count == 0) + { + return; + } + + await this.@lock.WaitAsync(); + List> copiedHandlers = new(this.handlers); + this.@lock.Release(); + + _ = Task.WhenAll(copiedHandlers.Select(async (handler) => + { + try + { + await handler(sender, args); + } + catch (Exception ex) + { + await this.errorHandler.HandleEventHandlerError(this.Name, ex, handler, sender, args); + } + })); + + return; + } +} diff --git a/DSharpPlus/AsyncManualResetEvent.cs b/DSharpPlus/AsyncManualResetEvent.cs index b51ebecd48..98d356475c 100644 --- a/DSharpPlus/AsyncManualResetEvent.cs +++ b/DSharpPlus/AsyncManualResetEvent.cs @@ -1,47 +1,47 @@ -using System.Threading; -using System.Threading.Tasks; - -namespace DSharpPlus; - -// source: https://blogs.msdn.microsoft.com/pfxteam/2012/02/11/building-async-coordination-primitives-part-1-asyncmanualresetevent/ -/// -/// Implements an async version of a -/// This class does currently not support Timeouts or the use of CancellationTokens -/// -internal class AsyncManualResetEvent -{ - public bool IsSet => this.tsc != null && this.tsc.Task.IsCompleted; - - private TaskCompletionSource tsc; - - public AsyncManualResetEvent() - : this(false) - { } - - public AsyncManualResetEvent(bool initialState) - { - this.tsc = new TaskCompletionSource(); - - if (initialState) - { - this.tsc.TrySetResult(true); - } - } - - public Task WaitAsync() => this.tsc.Task; - - public Task SetAsync() => Task.Run(() => this.tsc.TrySetResult(true)); - - public void Reset() - { - while (true) - { - TaskCompletionSource tsc = this.tsc; - - if (!tsc.Task.IsCompleted || Interlocked.CompareExchange(ref this.tsc, new TaskCompletionSource(), tsc) == tsc) - { - return; - } - } - } -} +using System.Threading; +using System.Threading.Tasks; + +namespace DSharpPlus; + +// source: https://blogs.msdn.microsoft.com/pfxteam/2012/02/11/building-async-coordination-primitives-part-1-asyncmanualresetevent/ +/// +/// Implements an async version of a +/// This class does currently not support Timeouts or the use of CancellationTokens +/// +internal class AsyncManualResetEvent +{ + public bool IsSet => this.tsc != null && this.tsc.Task.IsCompleted; + + private TaskCompletionSource tsc; + + public AsyncManualResetEvent() + : this(false) + { } + + public AsyncManualResetEvent(bool initialState) + { + this.tsc = new TaskCompletionSource(); + + if (initialState) + { + this.tsc.TrySetResult(true); + } + } + + public Task WaitAsync() => this.tsc.Task; + + public Task SetAsync() => Task.Run(() => this.tsc.TrySetResult(true)); + + public void Reset() + { + while (true) + { + TaskCompletionSource tsc = this.tsc; + + if (!tsc.Task.IsCompleted || Interlocked.CompareExchange(ref this.tsc, new TaskCompletionSource(), tsc) == tsc) + { + return; + } + } + } +} diff --git a/DSharpPlus/Clients/BaseDiscordClient.cs b/DSharpPlus/Clients/BaseDiscordClient.cs index 3445876e16..073a59e966 100644 --- a/DSharpPlus/Clients/BaseDiscordClient.cs +++ b/DSharpPlus/Clients/BaseDiscordClient.cs @@ -1,182 +1,182 @@ -#pragma warning disable CS0618 -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Reflection; -using System.Threading.Tasks; - -using DSharpPlus.Entities; -using DSharpPlus.Metrics; -using DSharpPlus.Net; - -using Microsoft.Extensions.Logging; - -namespace DSharpPlus; - -/// -/// Represents a common base for various Discord client implementations. -/// -public abstract class BaseDiscordClient : IDisposable -{ - protected internal DiscordApiClient ApiClient { get; internal init; } - protected internal DiscordConfiguration Configuration { get; internal init; } - - /// - /// Gets the intents this client has. - /// - public DiscordIntents Intents { get; internal set; } = DiscordIntents.None; - - /// - /// Gets the instance of the logger for this client. - /// - public ILogger Logger { get; internal init; } - - /// - /// Gets the string representing the version of D#+. - /// - public string VersionString { get; } - - /// - /// Gets the current user. - /// - public DiscordUser CurrentUser { get; internal set; } - - /// - /// Gets the current application. - /// - public DiscordApplication CurrentApplication { get; internal set; } - - /// - /// Gets the cached guilds for this client. - /// - public abstract IReadOnlyDictionary Guilds { get; } - - /// - /// Gets the cached users for this client. - /// - protected internal ConcurrentDictionary UserCache { get; } - - /// - /// Gets the list of available voice regions. Note that this property will not contain VIP voice regions. - /// - public IReadOnlyDictionary VoiceRegions - => this.InternalVoiceRegions; - - /// - /// Gets the list of available voice regions. This property is meant as a way to modify . - /// - protected internal ConcurrentDictionary InternalVoiceRegions { get; set; } - - /// - /// Initializes this Discord API client. - /// - internal BaseDiscordClient() - { - this.UserCache = new ConcurrentDictionary(); - this.InternalVoiceRegions = new ConcurrentDictionary(); - - Assembly a = typeof(DiscordClient).GetTypeInfo().Assembly; - - AssemblyInformationalVersionAttribute? iv = a.GetCustomAttribute(); - if (iv != null) - { - this.VersionString = iv.InformationalVersion; - } - else - { - Version? v = a.GetName().Version; - string vs = v.ToString(3); - - if (v.Revision > 0) - { - this.VersionString = $"{vs}, CI build {v.Revision}"; - } - } - } - - /// - public RequestMetricsCollection GetRequestMetrics(bool sinceLastCall = false) - => this.ApiClient.GetRequestMetrics(sinceLastCall); - - /// - /// Gets the current API application. - /// - /// Current API application. - public async Task GetCurrentApplicationAsync() - { - Net.Abstractions.TransportApplication tapp = await this.ApiClient.GetCurrentApplicationInfoAsync(); - return new DiscordApplication(tapp, this); - } - - /// - /// Gets a list of regions - /// - /// - /// Thrown when Discord is unable to process the request. - public async Task> ListVoiceRegionsAsync() - => await this.ApiClient.ListVoiceRegionsAsync(); - - /// - /// Initializes this client. This method fetches information about current user, application, and voice regions. - /// - /// - public virtual async Task InitializeAsync() - { - if (this.CurrentUser is null) - { - this.CurrentUser = await this.ApiClient.GetCurrentUserAsync(); - UpdateUserCache(this.CurrentUser); - } - - if (this is DiscordClient && this.CurrentApplication is null) - { - this.CurrentApplication = await GetCurrentApplicationAsync(); - } - - if (this is DiscordClient && this.InternalVoiceRegions.IsEmpty) - { - IReadOnlyList vrs = await ListVoiceRegionsAsync(); - foreach (DiscordVoiceRegion xvr in vrs) - { - this.InternalVoiceRegions.TryAdd(xvr.Id, xvr); - } - } - } - - /// - /// Gets the current gateway info. - /// - /// A gateway info object. - public async Task GetGatewayInfoAsync() - => await this.ApiClient.GetGatewayInfoAsync(); - - internal DiscordUser GetCachedOrEmptyUserInternal(ulong user_id) - { - TryGetCachedUserInternal(user_id, out DiscordUser? user); - return user; - } - - internal bool TryGetCachedUserInternal(ulong user_id, out DiscordUser user) - { - if (this.UserCache.TryGetValue(user_id, out user)) - { - return true; - } - - user = new DiscordUser { Id = user_id, Discord = this }; - return false; - } - - // This previously set properties on the old user and re-injected into the cache. - // That's terrible. Instead, insert the new reference and let the old one get GC'd. - // End-users are more likely to be holding a reference to the new object via an event or w/e - // anyways. - // Furthermore, setting properties requires keeping track of where we update cache and updating repeat code. - internal DiscordUser UpdateUserCache(DiscordUser newUser) - => this.UserCache.AddOrUpdate(newUser.Id, newUser, (_, _) => newUser); - - /// - /// Disposes this client. - /// - public abstract void Dispose(); -} +#pragma warning disable CS0618 +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Reflection; +using System.Threading.Tasks; + +using DSharpPlus.Entities; +using DSharpPlus.Metrics; +using DSharpPlus.Net; + +using Microsoft.Extensions.Logging; + +namespace DSharpPlus; + +/// +/// Represents a common base for various Discord client implementations. +/// +public abstract class BaseDiscordClient : IDisposable +{ + protected internal DiscordApiClient ApiClient { get; internal init; } + protected internal DiscordConfiguration Configuration { get; internal init; } + + /// + /// Gets the intents this client has. + /// + public DiscordIntents Intents { get; internal set; } = DiscordIntents.None; + + /// + /// Gets the instance of the logger for this client. + /// + public ILogger Logger { get; internal init; } + + /// + /// Gets the string representing the version of D#+. + /// + public string VersionString { get; } + + /// + /// Gets the current user. + /// + public DiscordUser CurrentUser { get; internal set; } + + /// + /// Gets the current application. + /// + public DiscordApplication CurrentApplication { get; internal set; } + + /// + /// Gets the cached guilds for this client. + /// + public abstract IReadOnlyDictionary Guilds { get; } + + /// + /// Gets the cached users for this client. + /// + protected internal ConcurrentDictionary UserCache { get; } + + /// + /// Gets the list of available voice regions. Note that this property will not contain VIP voice regions. + /// + public IReadOnlyDictionary VoiceRegions + => this.InternalVoiceRegions; + + /// + /// Gets the list of available voice regions. This property is meant as a way to modify . + /// + protected internal ConcurrentDictionary InternalVoiceRegions { get; set; } + + /// + /// Initializes this Discord API client. + /// + internal BaseDiscordClient() + { + this.UserCache = new ConcurrentDictionary(); + this.InternalVoiceRegions = new ConcurrentDictionary(); + + Assembly a = typeof(DiscordClient).GetTypeInfo().Assembly; + + AssemblyInformationalVersionAttribute? iv = a.GetCustomAttribute(); + if (iv != null) + { + this.VersionString = iv.InformationalVersion; + } + else + { + Version? v = a.GetName().Version; + string vs = v.ToString(3); + + if (v.Revision > 0) + { + this.VersionString = $"{vs}, CI build {v.Revision}"; + } + } + } + + /// + public RequestMetricsCollection GetRequestMetrics(bool sinceLastCall = false) + => this.ApiClient.GetRequestMetrics(sinceLastCall); + + /// + /// Gets the current API application. + /// + /// Current API application. + public async Task GetCurrentApplicationAsync() + { + Net.Abstractions.TransportApplication tapp = await this.ApiClient.GetCurrentApplicationInfoAsync(); + return new DiscordApplication(tapp, this); + } + + /// + /// Gets a list of regions + /// + /// + /// Thrown when Discord is unable to process the request. + public async Task> ListVoiceRegionsAsync() + => await this.ApiClient.ListVoiceRegionsAsync(); + + /// + /// Initializes this client. This method fetches information about current user, application, and voice regions. + /// + /// + public virtual async Task InitializeAsync() + { + if (this.CurrentUser is null) + { + this.CurrentUser = await this.ApiClient.GetCurrentUserAsync(); + UpdateUserCache(this.CurrentUser); + } + + if (this is DiscordClient && this.CurrentApplication is null) + { + this.CurrentApplication = await GetCurrentApplicationAsync(); + } + + if (this is DiscordClient && this.InternalVoiceRegions.IsEmpty) + { + IReadOnlyList vrs = await ListVoiceRegionsAsync(); + foreach (DiscordVoiceRegion xvr in vrs) + { + this.InternalVoiceRegions.TryAdd(xvr.Id, xvr); + } + } + } + + /// + /// Gets the current gateway info. + /// + /// A gateway info object. + public async Task GetGatewayInfoAsync() + => await this.ApiClient.GetGatewayInfoAsync(); + + internal DiscordUser GetCachedOrEmptyUserInternal(ulong user_id) + { + TryGetCachedUserInternal(user_id, out DiscordUser? user); + return user; + } + + internal bool TryGetCachedUserInternal(ulong user_id, out DiscordUser user) + { + if (this.UserCache.TryGetValue(user_id, out user)) + { + return true; + } + + user = new DiscordUser { Id = user_id, Discord = this }; + return false; + } + + // This previously set properties on the old user and re-injected into the cache. + // That's terrible. Instead, insert the new reference and let the old one get GC'd. + // End-users are more likely to be holding a reference to the new object via an event or w/e + // anyways. + // Furthermore, setting properties requires keeping track of where we update cache and updating repeat code. + internal DiscordUser UpdateUserCache(DiscordUser newUser) + => this.UserCache.AddOrUpdate(newUser.Id, newUser, (_, _) => newUser); + + /// + /// Disposes this client. + /// + public abstract void Dispose(); +} diff --git a/DSharpPlus/Clients/DiscordClient.Dispatch.cs b/DSharpPlus/Clients/DiscordClient.Dispatch.cs index e7dd517c58..e68c58fc9a 100644 --- a/DSharpPlus/Clients/DiscordClient.Dispatch.cs +++ b/DSharpPlus/Clients/DiscordClient.Dispatch.cs @@ -1,3041 +1,3041 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Globalization; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -using DSharpPlus.Entities; -using DSharpPlus.Entities.AuditLogs; -using DSharpPlus.EventArgs; -using DSharpPlus.Net.Abstractions; -using DSharpPlus.Net.InboundWebhooks; -using DSharpPlus.Net.InboundWebhooks.Payloads; -using DSharpPlus.Net.Serialization; - -using Microsoft.Extensions.Logging; -using Newtonsoft.Json.Linq; - -namespace DSharpPlus; - -public sealed partial class DiscordClient -{ - #region Private Fields - - private string sessionId; - private string? gatewayResumeUrl; - private bool guildDownloadCompleted = false; - - #endregion - - #region Dispatch Handler - - private async Task ReceiveGatewayEventsAsync() - { - while (!this.eventReader.Completion.IsCompleted) - { - GatewayPayload payload = await this.eventReader.ReadAsync(); - - try - { - await HandleDispatchAsync(payload); - } - catch (Exception ex) - { - this.Logger.LogError(ex, "Dispatch threw an exception: "); - } - } - } - - internal async Task HandleDispatchAsync(GatewayPayload payload) - { - if (payload.Data is not JObject dat) - { - this.Logger.LogWarning(LoggerEvents.WebSocketReceive, "Invalid payload body (this message is probably safe to ignore); opcode: {Op} event: {Event}; payload: {Payload}", payload.OpCode, payload.EventName, payload.Data); - return; - } - - if (payload.OpCode is not GatewayOpCode.Dispatch) - { - return; - } - - DiscordChannel chn; - DiscordThreadChannel thread; - ulong gid; - ulong cid; - TransportUser usr; - TransportMember mbr = default; - TransportUser refUsr = default; - TransportMember refMbr = default; - JToken rawMbr; - JToken? rawRefMsg = dat["referenced_message"]; - JArray rawMembers; - JArray rawPresences; - - switch (payload.EventName.ToLowerInvariant()) - { - #region Gateway Status - - case "ready": - JArray? glds = (JArray?)dat["guilds"]; - JArray? dmcs = (JArray?)dat["private_channels"]; - - int readyShardId = payload is ShardIdContainingGatewayPayload { ShardId: { } id } ? id : 0; - - await OnReadyEventAsync(dat.ToDiscordObject(), glds, dmcs, readyShardId); - break; - - case "resumed": - int resumedShardId = payload is ShardIdContainingGatewayPayload { ShardId: { } otherId } ? otherId : 0; - - await OnResumedAsync(resumedShardId); - break; - - #endregion - - #region Channel - - case "channel_create": - chn = dat.ToDiscordObject(); - await OnChannelCreateEventAsync(chn); - break; - - case "channel_update": - await OnChannelUpdateEventAsync(dat.ToDiscordObject()); - break; - - case "channel_delete": - bool isPrivate = dat["is_private"]?.ToObject() ?? false; - - chn = isPrivate ? dat.ToDiscordObject() : dat.ToDiscordObject(); - await OnChannelDeleteEventAsync(chn); - break; - - case "channel_pins_update": - cid = (ulong)dat["channel_id"]; - string? ts = (string)dat["last_pin_timestamp"]; - await OnChannelPinsUpdateAsync((ulong?)dat["guild_id"], cid, ts != null ? DateTimeOffset.Parse(ts, CultureInfo.InvariantCulture) : default(DateTimeOffset?)); - break; - - #endregion - - #region Scheduled Guild Events - - case "guild_scheduled_event_create": - DiscordScheduledGuildEvent cevt = dat.ToDiscordObject(); - await OnScheduledGuildEventCreateEventAsync(cevt); - break; - case "guild_scheduled_event_delete": - DiscordScheduledGuildEvent devt = dat.ToDiscordObject(); - await OnScheduledGuildEventDeleteEventAsync(devt); - break; - case "guild_scheduled_event_update": - DiscordScheduledGuildEvent uevt = dat.ToDiscordObject(); - await OnScheduledGuildEventUpdateEventAsync(uevt); - break; - case "guild_scheduled_event_user_add": - gid = (ulong)dat["guild_id"]; - ulong uid = (ulong)dat["user_id"]; - ulong eid = (ulong)dat["guild_scheduled_event_id"]; - await OnScheduledGuildEventUserAddEventAsync(gid, eid, uid); - break; - case "guild_scheduled_event_user_remove": - gid = (ulong)dat["guild_id"]; - uid = (ulong)dat["user_id"]; - eid = (ulong)dat["guild_scheduled_event_id"]; - await OnScheduledGuildEventUserRemoveEventAsync(gid, eid, uid); - break; - #endregion - - #region Guild - - case "guild_create": - - rawMembers = (JArray)dat["members"]; - rawPresences = (JArray)dat["presences"]; - dat.Remove("members"); - dat.Remove("presences"); - - await OnGuildCreateEventAsync(dat.ToDiscordObject(), rawMembers, rawPresences.ToDiscordObject>()); - break; - - case "guild_update": - - rawMembers = (JArray)dat["members"]; - dat.Remove("members"); - - await OnGuildUpdateEventAsync(dat.ToDiscordObject(), rawMembers); - break; - - case "guild_delete": - dat.Remove("members"); - - await OnGuildDeleteEventAsync(dat.ToDiscordObject()); - break; - - case "guild_emojis_update": - gid = (ulong)dat["guild_id"]; - IEnumerable ems = dat["emojis"].ToDiscordObject>(); - await OnGuildEmojisUpdateEventAsync(this.guilds[gid], ems); - break; - - case "guild_integrations_update": - gid = (ulong)dat["guild_id"]; - - // discord fires this event inconsistently if the current user leaves a guild. - if (!this.guilds.TryGetValue(gid, out DiscordGuild value)) - { - return; - } - - await OnGuildIntegrationsUpdateEventAsync(value); - break; - - case "guild_audit_log_entry_create": - gid = (ulong)dat["guild_id"]; - DiscordGuild guild = this.guilds[gid]; - AuditLogAction auditLogAction = dat.ToDiscordObject(); - DiscordAuditLogEntry entry = await AuditLogParser.ParseAuditLogEntryAsync(guild, auditLogAction); - await OnGuildAuditLogEntryCreateEventAsync(guild, entry); - break; - - #endregion - - #region Guild Ban - - case "guild_ban_add": - usr = dat["user"].ToDiscordObject(); - gid = (ulong)dat["guild_id"]; - await OnGuildBanAddEventAsync(usr, this.guilds[gid]); - break; - - case "guild_ban_remove": - usr = dat["user"].ToDiscordObject(); - gid = (ulong)dat["guild_id"]; - await OnGuildBanRemoveEventAsync(usr, this.guilds[gid]); - break; - - #endregion - - #region Guild Member - - case "guild_member_add": - gid = (ulong)dat["guild_id"]; - await OnGuildMemberAddEventAsync(dat.ToDiscordObject(), this.guilds[gid]); - break; - - case "guild_member_remove": - gid = (ulong)dat["guild_id"]; - usr = dat["user"].ToDiscordObject(); - - if (!this.guilds.TryGetValue(gid, out value)) - { - // discord fires this event inconsistently if the current user leaves a guild. - if (usr.Id != this.CurrentUser.Id) - { - this.Logger.LogError(LoggerEvents.WebSocketReceive, "Could not find {Guild} in guild cache", gid); - } - - return; - } - - await OnGuildMemberRemoveEventAsync(usr, value); - break; - - case "guild_member_update": - gid = (ulong)dat["guild_id"]; - await OnGuildMemberUpdateEventAsync(dat.ToDiscordObject(), this.guilds[gid]); - break; - - case "guild_members_chunk": - await OnGuildMembersChunkEventAsync(dat); - break; - - #endregion - - #region Guild Role - - case "guild_role_create": - gid = (ulong)dat["guild_id"]; - await OnGuildRoleCreateEventAsync(dat["role"].ToDiscordObject(), this.guilds[gid]); - break; - - case "guild_role_update": - gid = (ulong)dat["guild_id"]; - await OnGuildRoleUpdateEventAsync(dat["role"].ToDiscordObject(), this.guilds[gid]); - break; - - case "guild_role_delete": - gid = (ulong)dat["guild_id"]; - await OnGuildRoleDeleteEventAsync((ulong)dat["role_id"], this.guilds[gid]); - break; - - #endregion - - #region Invite - - case "invite_create": - gid = (ulong)dat["guild_id"]; - cid = (ulong)dat["channel_id"]; - await OnInviteCreateEventAsync(cid, gid, dat.ToDiscordObject()); - break; - - case "invite_delete": - gid = (ulong)dat["guild_id"]; - cid = (ulong)dat["channel_id"]; - await OnInviteDeleteEventAsync(cid, gid, dat); - break; - - #endregion - - #region Message - - case "message_create": - rawMbr = dat["member"]; - - if (rawMbr != null) - { - mbr = rawMbr.ToDiscordObject(); - } - - if (rawRefMsg != null && rawRefMsg.HasValues) - { - if (rawRefMsg.SelectToken("author") != null) - { - refUsr = rawRefMsg.SelectToken("author").ToDiscordObject(); - } - - if (rawRefMsg.SelectToken("member") != null) - { - refMbr = rawRefMsg.SelectToken("member").ToDiscordObject(); - } - } - - TransportUser author = dat["author"].ToDiscordObject(); - dat.Remove("author"); - dat.Remove("member"); - - await OnMessageCreateEventAsync(dat.ToDiscordObject(), author, mbr, refUsr, refMbr); - break; - - case "message_update": - rawMbr = dat["member"]; - - if (rawMbr != null) - { - mbr = rawMbr.ToDiscordObject(); - } - - if (rawRefMsg != null && rawRefMsg.HasValues) - { - if (rawRefMsg.SelectToken("author") != null) - { - refUsr = rawRefMsg.SelectToken("author").ToDiscordObject(); - } - - if (rawRefMsg.SelectToken("member") != null) - { - refMbr = rawRefMsg.SelectToken("member").ToDiscordObject(); - } - } - - await OnMessageUpdateEventAsync(dat.ToDiscordObject(), dat["author"]?.ToDiscordObject(), mbr, refUsr, refMbr); - break; - - // delete event does *not* include message object - case "message_delete": - await OnMessageDeleteEventAsync((ulong)dat["id"], (ulong)dat["channel_id"], (ulong?)dat["guild_id"]); - break; - - case "message_delete_bulk": - await OnMessageBulkDeleteEventAsync(dat["ids"].ToDiscordObject(), (ulong)dat["channel_id"], (ulong?)dat["guild_id"]); - break; - - case "message_poll_vote_add": - await OnMessagePollVoteEventAsync(dat.ToDiscordObject(), true); - break; - - case "message_poll_vote_remove": - await OnMessagePollVoteEventAsync(dat.ToDiscordObject(), false); - break; - - #endregion - - #region Message Reaction - - case "message_reaction_add": - rawMbr = dat["member"]; - - if (rawMbr != null) - { - mbr = rawMbr.ToDiscordObject(); - } - - await OnMessageReactionAddAsync((ulong)dat["user_id"], (ulong)dat["message_id"], (ulong)dat["channel_id"], (ulong?)dat["guild_id"], mbr, dat["emoji"].ToDiscordObject()); - break; - - case "message_reaction_remove": - await OnMessageReactionRemoveAsync((ulong)dat["user_id"], (ulong)dat["message_id"], (ulong)dat["channel_id"], (ulong?)dat["guild_id"], dat["emoji"].ToDiscordObject()); - break; - - case "message_reaction_remove_all": - await OnMessageReactionRemoveAllAsync((ulong)dat["message_id"], (ulong)dat["channel_id"], (ulong?)dat["guild_id"]); - break; - - case "message_reaction_remove_emoji": - await OnMessageReactionRemoveEmojiAsync((ulong)dat["message_id"], (ulong)dat["channel_id"], (ulong)dat["guild_id"], dat["emoji"]); - break; - - #endregion - - #region User/Presence Update - - case "presence_update": - // Presences are a mess. I'm not touching this. ~Velvet - await OnPresenceUpdateEventAsync(dat, (JObject)dat["user"]); - break; - - case "user_settings_update": - await OnUserSettingsUpdateEventAsync(dat.ToDiscordObject()); - break; - - case "user_update": - await OnUserUpdateEventAsync(dat.ToDiscordObject()); - break; - - #endregion - - #region Voice - - case "voice_state_update": - await OnVoiceStateUpdateEventAsync(dat); - break; - - case "voice_server_update": - gid = (ulong)dat["guild_id"]; - await OnVoiceServerUpdateEventAsync((string)dat["endpoint"], (string)dat["token"], this.guilds[gid]); - break; - - #endregion - - #region Thread - - case "thread_create": - thread = dat.ToDiscordObject(); - await OnThreadCreateEventAsync(thread); - break; - - case "thread_update": - thread = dat.ToDiscordObject(); - await OnThreadUpdateEventAsync(thread); - break; - - case "thread_delete": - thread = dat.ToDiscordObject(); - await OnThreadDeleteEventAsync(thread); - break; - - case "thread_list_sync": - gid = (ulong)dat["guild_id"]; //get guild - await OnThreadListSyncEventAsync(this.guilds[gid], dat["channel_ids"].ToDiscordObject>(), dat["threads"].ToDiscordObject>(), dat["members"].ToDiscordObject>()); - break; - - case "thread_member_update": - gid = (ulong)dat["guild_id"]; - await OnThreadMemberUpdateEventAsync(this.guilds[gid], dat.ToDiscordObject()); - break; - - case "thread_members_update": - gid = (ulong)dat["guild_id"]; - await OnThreadMembersUpdateEventAsync(this.guilds[gid], (ulong)dat["id"], dat["added_members"]?.ToDiscordObject>(), dat["removed_member_ids"]?.ToDiscordObject>(), (int)dat["member_count"]); - break; - - #endregion - - #region Interaction/Integration/Application - - case "interaction_create": - - rawMbr = dat["member"]; - - if (rawMbr != null) - { - mbr = dat["member"].ToDiscordObject(); - usr = mbr.User; - } - else - { - usr = dat["user"].ToDiscordObject(); - } - - JToken? rawChannel = dat["channel"]; - DiscordChannel? channel = null; - if (rawChannel is not null) - { - channel = rawChannel.ToDiscordObject(); - channel.Discord = this; - } - - // Re: Removing re-serialized data: This one is probably fine? - // The user on the object is marked with [JsonIgnore]. - - cid = (ulong)dat["channel_id"]; - await OnInteractionCreateAsync((ulong?)dat["guild_id"], cid, usr, mbr, channel, dat.ToDiscordObject()); - break; - - case "integration_create": - await OnIntegrationCreateAsync(dat.ToDiscordObject(), (ulong)dat["guild_id"]); - break; - - case "integration_update": - await OnIntegrationUpdateAsync(dat.ToDiscordObject(), (ulong)dat["guild_id"]); - break; - - case "integration_delete": - await OnIntegrationDeleteAsync((ulong)dat["id"], (ulong)dat["guild_id"], (ulong?)dat["application_id"]); - break; - - case "application_command_permissions_update": - await OnApplicationCommandPermissionsUpdateAsync(dat); - break; - #endregion - - #region Stage Instance - - case "stage_instance_create": - await OnStageInstanceCreateAsync(dat.ToDiscordObject()); - break; - - case "stage_instance_update": - await OnStageInstanceUpdateAsync(dat.ToDiscordObject()); - break; - - case "stage_instance_delete": - await OnStageInstanceDeleteAsync(dat.ToDiscordObject()); - break; - - #endregion - - #region Misc - - case "gift_code_update": //Not supposed to be dispatched to bots - break; - - case "embedded_activity_update": //Not supposed to be dispatched to bots - break; - - case "typing_start": - cid = (ulong)dat["channel_id"]; - rawMbr = dat["member"]; - - if (rawMbr != null) - { - mbr = rawMbr.ToDiscordObject(); - } - - ulong? guildId = (ulong?)dat["guild_id"]; - await OnTypingStartEventAsync((ulong)dat["user_id"], cid, InternalGetCachedChannel(cid, guildId)!, guildId, Utilities.GetDateTimeOffset((long)dat["timestamp"]), mbr); - break; - - case "webhooks_update": - gid = (ulong)dat["guild_id"]; - cid = (ulong)dat["channel_id"]; - await OnWebhooksUpdateAsync(this.guilds[gid].GetChannel(cid), this.guilds[gid]); - break; - - case "guild_stickers_update": - IEnumerable strs = dat["stickers"].ToDiscordObject>(); - await OnStickersUpdatedAsync(strs, dat); - break; - - default: - await OnUnknownEventAsync(payload); - if (this.Configuration.LogUnknownEvents) - { - this.Logger.LogWarning(LoggerEvents.WebSocketReceive, "Unknown event: {EventName}\npayload: {@Payload}", payload.EventName, payload.Data); - } - - break; - - #endregion - - #region AutoModeration - case "auto_moderation_rule_create": - await OnAutoModerationRuleCreateAsync(dat.ToDiscordObject()); - break; - - case "auto_moderation_rule_update": - await OnAutoModerationRuleUpdatedAsync(dat.ToDiscordObject()); - break; - - case "auto_moderation_rule_delete": - await OnAutoModerationRuleDeletedAsync(dat.ToDiscordObject()); - break; - - case "auto_moderation_action_execution": - await OnAutoModerationRuleExecutedAsync(dat.ToDiscordObject()); - break; - #endregion - - #region Entitlements - case "entitlement_create": - await OnEntitlementCreatedAsync(dat.ToDiscordObject()); - break; - - case "entitlement_update": - await OnEntitlementUpdatedAsync(dat.ToDiscordObject()); - break; - - case "entitlement_delete": - await OnEntitlementDeletedAsync(dat.ToDiscordObject()); - break; - #endregion - } - } - - #endregion - - #region Webhook Events - - private async Task ReceiveWebhookEventsAsync() - { - while (!this.webhookEventReader.Completion.IsCompleted) - { - DiscordWebhookEvent payload = await this.webhookEventReader.ReadAsync(); - - try - { - await HandleWebhookDispatchAsync(payload); - } - catch (Exception ex) - { - this.Logger.LogError(ex, "Dispatch threw an exception: "); - } - } - } - - private async Task ReceiveInteractionEventsAsync() - { - while (!this.interactionEventReader.Completion.IsCompleted) - { - DiscordHttpInteractionPayload payload = await this.interactionEventReader.ReadAsync(); - DiscordHttpInteraction interaction = payload.ProtoInteraction; - interaction.Discord = this; - - ulong? guildId = interaction.GuildId; - ulong channelId = interaction.ChannelId; - - JToken rawMember = payload.Data["member"]; - TransportMember? transportMember = null; - TransportUser transportUser; - if (rawMember != null) - { - transportMember = payload.Data["member"].ToDiscordObject(); - transportUser = transportMember.User; - } - else - { - transportUser = payload.Data["user"].ToDiscordObject(); - } - - DiscordChannel channel = interaction.Channel; - channel.Discord = this; - - await OnInteractionCreateAsync(guildId, channelId, transportUser, transportMember, channel, interaction); - } - } - - private Task HandleWebhookDispatchAsync(DiscordWebhookEvent @event) - { - if (@event.ApplicationID != this.CurrentApplication.Id) - { - this.Logger.LogCritical - ( - "The application event webhook received an event for application {OtherId}, which is different from the current application.", - @event.ApplicationID - ); - - return Task.CompletedTask; - } - - if (@event.Type == DiscordWebhookEventType.Ping) - { - return Task.CompletedTask; - } - - DiscordWebhookEventBody body = @event.Event; - - _ = body.Type switch - { - DiscordWebhookEventBodyType.ApplicationAuthorized => OnApplicationAuthorizedAsync(body), - DiscordWebhookEventBodyType.EntitlementCreate => OnWebhookEntitlementCreateAsync(body), - _ => OnUnknownWebhookEventAsync(body) - }; - - return Task.CompletedTask; - } - - #endregion - - #region Events - - #region Gateway - - internal async Task OnReadyEventAsync(ReadyPayload ready, JArray rawGuilds, JArray rawDmChannels, int shardId) - { - TransportUser rusr = ready.CurrentUser; - this.CurrentUser = new DiscordUser(rusr) - { - Discord = this - }; - - this.sessionId = ready.SessionId; - this.gatewayResumeUrl = ready.ResumeGatewayUrl; - Dictionary rawGuildIndex = rawGuilds.ToDictionary(xt => (ulong)xt["id"], xt => (JObject)xt); - - this.privateChannels.Clear(); - foreach (JToken rawChannel in rawDmChannels) - { - DiscordDmChannel channel = rawChannel.ToDiscordObject(); - - channel.Discord = this; - - //xdc.recipients = - // .Select(xtu => this.InternalGetCachedUser(xtu.Id) ?? new DiscordUser(xtu) { Discord = this }) - // .ToList(); - - IEnumerable recipsRaw = rawChannel["recipients"].ToDiscordObject>(); - List recipients = []; - foreach (TransportUser xr in recipsRaw) - { - DiscordUser xu = new(xr) { Discord = this }; - xu = UpdateUserCache(xu); - - recipients.Add(xu); - } - - channel.Recipients = recipients; - - this.privateChannels[channel.Id] = channel; - } - - List guilds = rawGuilds.ToDiscordObject>().ToList(); - foreach (DiscordGuild guild in guilds) - { - guild.Discord = this; - guild.channels ??= new ConcurrentDictionary(); - guild.threads ??= new ConcurrentDictionary(); - - foreach (DiscordChannel xc in guild.Channels.Values) - { - xc.GuildId = guild.Id; - xc.Discord = this; - foreach (DiscordOverwrite xo in xc.permissionOverwrites) - { - xo.Discord = this; - xo.channelId = xc.Id; - } - } - - foreach (DiscordThreadChannel xt in guild.Threads.Values) - { - xt.GuildId = guild.Id; - xt.Discord = this; - } - - guild.roles ??= new ConcurrentDictionary(); - - foreach (DiscordRole xr in guild.Roles.Values) - { - xr.Discord = this; - xr.guild_id = guild.Id; - } - - JObject rawGuild = rawGuildIndex[guild.Id]; - JArray? rawMembers = (JArray)rawGuild["members"]; - - guild.members?.Clear(); - guild.members ??= new ConcurrentDictionary(); - - if (rawMembers != null) - { - foreach (JToken xj in rawMembers) - { - TransportMember xtm = xj.ToDiscordObject(); - - DiscordUser xu = new(xtm.User) { Discord = this }; - xu = UpdateUserCache(xu); - - guild.members[xtm.User.Id] = new DiscordMember(xtm) { Discord = this, guild_id = guild.Id }; - } - } - - guild.emojis ??= new ConcurrentDictionary(); - - foreach (DiscordEmoji xe in guild.Emojis.Values) - { - xe.Discord = this; - } - - guild.voiceStates ??= new ConcurrentDictionary(); - - foreach (DiscordVoiceState xvs in guild.VoiceStates.Values) - { - xvs.Discord = this; - } - - this.guilds[guild.Id] = guild; - } - - await this.dispatcher.DispatchAsync - ( - this, - new() - { - ShardId = shardId, - GuildIds = [.. guilds.Select(guild => guild.Id)] - } - ); - - if (!guilds.Any() && this.orchestrator.AllShardsConnected) - { - this.guildDownloadCompleted = true; - GuildDownloadCompletedEventArgs ea = new(this.Guilds); - - await this.dispatcher.DispatchAsync(this, ea); - } - } - - internal async Task OnResumedAsync(int shardId) - { - await this.dispatcher.DispatchAsync - ( - this, - new() - { - ShardId = shardId - } - ); - } - - #endregion - - #region Channel - - internal async Task OnChannelCreateEventAsync(DiscordChannel channel) - { - channel.Discord = this; - foreach (DiscordOverwrite xo in channel.permissionOverwrites) - { - xo.Discord = this; - xo.channelId = channel.Id; - } - - this.guilds[channel.GuildId.Value].channels[channel.Id] = channel; - - await this.dispatcher.DispatchAsync(this, new ChannelCreatedEventArgs - { - Channel = channel, - Guild = channel.Guild - }); - } - - internal async Task OnChannelUpdateEventAsync(DiscordChannel channel) - { - if (channel == null) - { - return; - } - - channel.Discord = this; - - DiscordGuild? gld = channel.Guild; - - DiscordChannel? channel_new = InternalGetCachedChannel(channel.Id, channel.GuildId); - DiscordChannel channel_old = null!; - - if (channel_new != null) - { - channel_old = new DiscordChannel - { - Bitrate = channel_new.Bitrate, - Discord = this, - GuildId = channel_new.GuildId, - Id = channel_new.Id, - //IsPrivate = channel_new.IsPrivate, - LastMessageId = channel_new.LastMessageId, - Name = channel_new.Name, - permissionOverwrites = new List(channel_new.permissionOverwrites), - Position = channel_new.Position, - Topic = channel_new.Topic, - Type = channel_new.Type, - UserLimit = channel_new.UserLimit, - ParentId = channel_new.ParentId, - IsNSFW = channel_new.IsNSFW, - PerUserRateLimit = channel_new.PerUserRateLimit, - RtcRegionId = channel_new.RtcRegionId, - QualityMode = channel_new.QualityMode - }; - - channel_new.Bitrate = channel.Bitrate; - channel_new.Name = channel.Name; - channel_new.Position = channel.Position; - channel_new.Topic = channel.Topic; - channel_new.UserLimit = channel.UserLimit; - channel_new.ParentId = channel.ParentId; - channel_new.IsNSFW = channel.IsNSFW; - channel_new.PerUserRateLimit = channel.PerUserRateLimit; - channel_new.Type = channel.Type; - channel_new.RtcRegionId = channel.RtcRegionId; - channel_new.QualityMode = channel.QualityMode; - - channel_new.permissionOverwrites.Clear(); - - foreach (DiscordOverwrite po in channel.permissionOverwrites) - { - po.Discord = this; - po.channelId = channel.Id; - } - - channel_new.permissionOverwrites.AddRange(channel.permissionOverwrites); - } - else if (gld != null) - { - gld.channels[channel.Id] = channel; - } - - await this.dispatcher.DispatchAsync(this, new ChannelUpdatedEventArgs - { - ChannelAfter = channel_new, - Guild = gld, - ChannelBefore = channel_old - }); - } - - internal async Task OnChannelDeleteEventAsync(DiscordChannel channel) - { - if (channel == null) - { - return; - } - - channel.Discord = this; - - //if (channel.IsPrivate) - if (channel.Type is DiscordChannelType.Group or DiscordChannelType.Private) - { - DiscordDmChannel? dmChannel = channel as DiscordDmChannel; - - _ = this.privateChannels.TryRemove(dmChannel.Id, out _); - - await this.dispatcher.DispatchAsync(this, new DmChannelDeletedEventArgs - { - Channel = dmChannel - }); - } - else - { - DiscordGuild gld = channel.Guild; - - if (gld.channels.TryRemove(channel.Id, out DiscordChannel? cachedChannel)) - { - channel = cachedChannel; - } - - await this.dispatcher.DispatchAsync(this, new ChannelDeletedEventArgs - { - Channel = channel, - Guild = gld - }); - } - } - - internal async Task OnChannelPinsUpdateAsync(ulong? guildId, ulong channelId, DateTimeOffset? lastPinTimestamp) - { - DiscordGuild guild = InternalGetCachedGuild(guildId); - DiscordChannel? channel = InternalGetCachedChannel(channelId, guildId) ?? InternalGetCachedThread(channelId, guildId); - - if (channel == null) - { - channel = new DiscordDmChannel - { - Id = channelId, - Discord = this, - Type = DiscordChannelType.Private, - Recipients = Array.Empty() - }; - - DiscordDmChannel chn = (DiscordDmChannel)channel; - - this.privateChannels[channelId] = chn; - } - - ChannelPinsUpdatedEventArgs ea = new() - { - Guild = guild, - Channel = channel, - LastPinTimestamp = lastPinTimestamp - }; - - await this.dispatcher.DispatchAsync(this, ea); - } - - #endregion - - #region Scheduled Guild Events - - private async Task OnScheduledGuildEventCreateEventAsync(DiscordScheduledGuildEvent evt) - { - evt.Discord = this; - - if (evt.Creator != null) - { - evt.Creator.Discord = this; - UpdateUserCache(evt.Creator); - } - - evt.Guild.scheduledEvents[evt.Id] = evt; - - await this.dispatcher.DispatchAsync(this, new ScheduledGuildEventCreatedEventArgs - { - Event = evt - }); - } - - private async Task OnScheduledGuildEventDeleteEventAsync(DiscordScheduledGuildEvent evt) - { - DiscordGuild guild = InternalGetCachedGuild(evt.GuildId); - - if (guild == null) // ??? // - { - return; - } - - guild.scheduledEvents.TryRemove(evt.Id, out _); - - evt.Discord = this; - - if (evt.Creator != null) - { - evt.Creator.Discord = this; - UpdateUserCache(evt.Creator); - } - - await this.dispatcher.DispatchAsync(this, new ScheduledGuildEventDeletedEventArgs - { - Event = evt - }); - } - - private async Task OnScheduledGuildEventUpdateEventAsync(DiscordScheduledGuildEvent evt) - { - evt.Discord = this; - - if (evt.Creator != null) - { - evt.Creator.Discord = this; - UpdateUserCache(evt.Creator); - } - - DiscordGuild guild = InternalGetCachedGuild(evt.GuildId); - guild.scheduledEvents.TryGetValue(evt.GuildId, out DiscordScheduledGuildEvent? oldEvt); - - evt.Guild.scheduledEvents[evt.Id] = evt; - - if (evt.Status is DiscordScheduledGuildEventStatus.Completed) - { - await this.dispatcher.DispatchAsync(this, new ScheduledGuildEventCompletedEventArgs() - { - Event = evt - }); - } - else - { - await this.dispatcher.DispatchAsync(this, new ScheduledGuildEventUpdatedEventArgs() - { - EventBefore = oldEvt, - EventAfter = evt - }); - } - } - - private async Task OnScheduledGuildEventUserAddEventAsync(ulong guildId, ulong eventId, ulong userId) - { - DiscordGuild guild = InternalGetCachedGuild(guildId); - DiscordScheduledGuildEvent evt = guild.scheduledEvents.GetOrAdd(eventId, new DiscordScheduledGuildEvent() - { - Id = eventId, - GuildId = guildId, - Discord = this, - UserCount = 0 - }); - - evt.UserCount++; - - DiscordUser user = - guild.Members.TryGetValue(userId, out DiscordMember? mbr) ? mbr : - GetCachedOrEmptyUserInternal(userId) ?? new DiscordUser() { Id = userId, Discord = this }; - - await this.dispatcher.DispatchAsync(this, new ScheduledGuildEventUserAddedEventArgs() - { - Event = evt, - User = user - }); - } - - private async Task OnScheduledGuildEventUserRemoveEventAsync(ulong guildId, ulong eventId, ulong userId) - { - DiscordGuild guild = InternalGetCachedGuild(guildId); - DiscordScheduledGuildEvent evt = guild.scheduledEvents.GetOrAdd(eventId, new DiscordScheduledGuildEvent() - { - Id = eventId, - GuildId = guildId, - Discord = this, - UserCount = 0 - }); - - evt.UserCount = evt.UserCount is 0 ? 0 : evt.UserCount - 1; - - DiscordUser user = - guild.Members.TryGetValue(userId, out DiscordMember? mbr) ? mbr : - GetCachedOrEmptyUserInternal(userId) ?? new DiscordUser() { Id = userId, Discord = this }; - - await this.dispatcher.DispatchAsync(this, new ScheduledGuildEventUserRemovedEventArgs() - { - Event = evt, - User = user - }); - } - - #endregion - - #region Guild - - internal async Task OnGuildCreateEventAsync(DiscordGuild guild, JArray rawMembers, IEnumerable presences) - { - if (presences != null) - { - foreach (DiscordPresence xp in presences) - { - xp.Discord = this; - xp.GuildId = guild.Id; - xp.Activity = new DiscordActivity(xp.RawActivity); - - if (xp.RawActivities != null) - { - xp.internalActivities = new DiscordActivity[xp.RawActivities.Length]; - for (int i = 0; i < xp.RawActivities.Length; i++) - { - xp.internalActivities[i] = new DiscordActivity(xp.RawActivities[i]); - } - } - - this.presences[xp.User.Id] = xp; - } - } - - bool exists = this.guilds.TryGetValue(guild.Id, out DiscordGuild? foundGuild); - - guild.Discord = this; - guild.IsUnavailable = false; - DiscordGuild eventGuild = guild; - - if (exists) - { - guild = foundGuild; - } - - guild.channels ??= new ConcurrentDictionary(); - guild.threads ??= new ConcurrentDictionary(); - guild.roles ??= new ConcurrentDictionary(); - guild.emojis ??= new ConcurrentDictionary(); - guild.stickers ??= new ConcurrentDictionary(); - guild.voiceStates ??= new ConcurrentDictionary(); - guild.members ??= new ConcurrentDictionary(); - guild.stageInstances ??= new ConcurrentDictionary(); - guild.scheduledEvents ??= new ConcurrentDictionary(); - - UpdateCachedGuild(eventGuild, rawMembers); - - guild.JoinedAt = eventGuild.JoinedAt; - guild.IsLarge = eventGuild.IsLarge; - guild.MemberCount = Math.Max(eventGuild.MemberCount, guild.members.Count); - guild.IsUnavailable = eventGuild.IsUnavailable; - guild.PremiumSubscriptionCount = eventGuild.PremiumSubscriptionCount; - guild.PremiumTier = eventGuild.PremiumTier; - guild.Banner = eventGuild.Banner; - guild.VanityUrlCode = eventGuild.VanityUrlCode; - guild.Description = eventGuild.Description; - guild.IsNSFW = eventGuild.IsNSFW; - - foreach (KeyValuePair kvp in eventGuild.voiceStates ??= new()) - { - guild.voiceStates[kvp.Key] = kvp.Value; - } - - foreach (DiscordScheduledGuildEvent xe in guild.scheduledEvents.Values) - { - xe.Discord = this; - - if (xe.Creator != null) - { - xe.Creator.Discord = this; - } - } - - foreach (DiscordChannel xc in guild.channels.Values) - { - xc.GuildId = guild.Id; - xc.Discord = this; - foreach (DiscordOverwrite xo in xc.permissionOverwrites) - { - xo.Discord = this; - xo.channelId = xc.Id; - } - } - - foreach (DiscordThreadChannel xt in guild.threads.Values) - { - xt.GuildId = guild.Id; - xt.Discord = this; - } - - foreach (DiscordEmoji xe in guild.emojis.Values) - { - xe.Discord = this; - } - - foreach (DiscordMessageSticker xs in guild.stickers.Values) - { - xs.Discord = this; - } - - foreach (DiscordVoiceState xvs in guild.voiceStates.Values) - { - xvs.Discord = this; - } - - foreach (DiscordRole xr in guild.roles.Values) - { - xr.Discord = this; - xr.guild_id = guild.Id; - } - - foreach (DiscordStageInstance instance in guild.stageInstances.Values) - { - instance.Discord = this; - } - - bool old = Volatile.Read(ref this.guildDownloadCompleted); - bool dcompl = this.guilds.Values.All(xg => !xg.IsUnavailable) && !this.guildDownloadCompleted; - - if (exists) - { - await this.dispatcher.DispatchAsync(this, new GuildAvailableEventArgs - { - Guild = guild - }); - } - else - { - await this.dispatcher.DispatchAsync(this, new GuildCreatedEventArgs - { - Guild = guild - }); - } - - if (dcompl && !old && this.orchestrator.AllShardsConnected) - { - this.guildDownloadCompleted = true; - await this.dispatcher.DispatchAsync(this, new GuildDownloadCompletedEventArgs(this.Guilds)); - } - } - - internal async Task OnGuildUpdateEventAsync(DiscordGuild guild, JArray rawMembers) - { - DiscordGuild oldGuild; - - if (!this.guilds.TryGetValue(guild.Id, out DiscordGuild gld)) - { - this.guilds[guild.Id] = guild; - oldGuild = null; - } - else - { - oldGuild = new DiscordGuild - { - Discord = gld.Discord, - Name = gld.Name, - AfkChannelId = gld.AfkChannelId, - AfkTimeout = gld.AfkTimeout, - DefaultMessageNotifications = gld.DefaultMessageNotifications, - ExplicitContentFilter = gld.ExplicitContentFilter, - Features = gld.Features, - IconHash = gld.IconHash, - Id = gld.Id, - IsLarge = gld.IsLarge, - isSynced = gld.isSynced, - IsUnavailable = gld.IsUnavailable, - JoinedAt = gld.JoinedAt, - MemberCount = gld.MemberCount, - MaxMembers = gld.MaxMembers, - MaxPresences = gld.MaxPresences, - ApproximateMemberCount = gld.ApproximateMemberCount, - ApproximatePresenceCount = gld.ApproximatePresenceCount, - MaxVideoChannelUsers = gld.MaxVideoChannelUsers, - DiscoverySplashHash = gld.DiscoverySplashHash, - PreferredLocale = gld.PreferredLocale, - MfaLevel = gld.MfaLevel, - OwnerId = gld.OwnerId, - SplashHash = gld.SplashHash, - SystemChannelId = gld.SystemChannelId, - SystemChannelFlags = gld.SystemChannelFlags, - WidgetEnabled = gld.WidgetEnabled, - WidgetChannelId = gld.WidgetChannelId, - VerificationLevel = gld.VerificationLevel, - RulesChannelId = gld.RulesChannelId, - PublicUpdatesChannelId = gld.PublicUpdatesChannelId, - voiceRegionId = gld.voiceRegionId, - PremiumProgressBarEnabled = gld.PremiumProgressBarEnabled, - IsNSFW = gld.IsNSFW, - channels = new ConcurrentDictionary(), - threads = new ConcurrentDictionary(), - emojis = new ConcurrentDictionary(), - members = new ConcurrentDictionary(), - roles = new ConcurrentDictionary(), - voiceStates = new ConcurrentDictionary() - }; - - foreach (KeyValuePair kvp in gld.channels ??= new()) - { - oldGuild.channels[kvp.Key] = kvp.Value; - } - - foreach (KeyValuePair kvp in gld.threads ??= new()) - { - oldGuild.threads[kvp.Key] = kvp.Value; - } - - foreach (KeyValuePair kvp in gld.emojis ??= new()) - { - oldGuild.emojis[kvp.Key] = kvp.Value; - } - - foreach (KeyValuePair kvp in gld.roles ??= new()) - { - oldGuild.roles[kvp.Key] = kvp.Value; - } - //new ConcurrentDictionary() - foreach (KeyValuePair kvp in gld.voiceStates ??= new()) - { - oldGuild.voiceStates[kvp.Key] = kvp.Value; - } - - foreach (KeyValuePair kvp in gld.members ??= new()) - { - oldGuild.members[kvp.Key] = kvp.Value; - } - } - - guild.Discord = this; - guild.IsUnavailable = false; - DiscordGuild eventGuild = guild; - guild = this.guilds[eventGuild.Id]; - guild.channels ??= new ConcurrentDictionary(); - guild.threads ??= new ConcurrentDictionary(); - guild.roles ??= new ConcurrentDictionary(); - guild.emojis ??= new ConcurrentDictionary(); - guild.voiceStates ??= new ConcurrentDictionary(); - guild.members ??= new ConcurrentDictionary(); - UpdateCachedGuild(eventGuild, rawMembers); - - foreach (DiscordChannel xc in guild.channels.Values) - { - xc.GuildId = guild.Id; - xc.Discord = this; - foreach (DiscordOverwrite xo in xc.permissionOverwrites) - { - xo.Discord = this; - xo.channelId = xc.Id; - } - } - - foreach (DiscordThreadChannel xc in guild.threads.Values) - { - xc.GuildId = guild.Id; - xc.Discord = this; - } - - foreach (DiscordEmoji xe in guild.emojis.Values) - { - xe.Discord = this; - } - - foreach (DiscordVoiceState xvs in guild.voiceStates.Values) - { - xvs.Discord = this; - } - - foreach (DiscordRole xr in guild.roles.Values) - { - xr.Discord = this; - xr.guild_id = guild.Id; - } - - await this.dispatcher.DispatchAsync(this, new GuildUpdatedEventArgs - { - GuildBefore = oldGuild, - GuildAfter = guild - }); - } - - internal async Task OnGuildDeleteEventAsync(DiscordGuild guild) - { - if (guild.IsUnavailable) - { - if (!this.guilds.TryGetValue(guild.Id, out DiscordGuild? gld)) - { - return; - } - - gld.IsUnavailable = true; - - await this.dispatcher.DispatchAsync(this, new GuildUnavailableEventArgs - { - Guild = guild, - Unavailable = true - }); - } - else - { - if (!this.guilds.TryRemove(guild.Id, out DiscordGuild? gld)) - { - return; - } - - await this.dispatcher.DispatchAsync(this, new GuildDeletedEventArgs - { - Guild = gld - }); - } - } - - internal async Task OnGuildEmojisUpdateEventAsync(DiscordGuild guild, IEnumerable newEmojis) - { - ConcurrentDictionary oldEmojis = new(guild.emojis); - guild.emojis.Clear(); - - foreach (DiscordEmoji emoji in newEmojis) - { - emoji.Discord = this; - guild.emojis[emoji.Id] = emoji; - } - - GuildEmojisUpdatedEventArgs ea = new() - { - Guild = guild, - EmojisAfter = guild.Emojis, - EmojisBefore = oldEmojis - }; - - await this.dispatcher.DispatchAsync(this, ea); - } - - internal async Task OnGuildIntegrationsUpdateEventAsync(DiscordGuild guild) - { - GuildIntegrationsUpdatedEventArgs ea = new() - { - Guild = guild - }; - - await this.dispatcher.DispatchAsync(this, ea); - } - - private async Task OnGuildAuditLogEntryCreateEventAsync(DiscordGuild guild, DiscordAuditLogEntry auditLogEntry) - { - GuildAuditLogCreatedEventArgs ea = new() - { - Guild = guild, - AuditLogEntry = auditLogEntry - }; - - await this.dispatcher.DispatchAsync(this, ea); - } - - #endregion - - #region Guild Ban - - internal async Task OnGuildBanAddEventAsync(TransportUser user, DiscordGuild guild) - { - DiscordUser usr = new(user) { Discord = this }; - usr = UpdateUserCache(usr); - - if (!guild.Members.TryGetValue(user.Id, out DiscordMember? mbr)) - { - mbr = new DiscordMember(usr) { Discord = this, guild_id = guild.Id }; - } - - GuildBanAddedEventArgs ea = new() - { - Guild = guild, - Member = mbr - }; - - await this.dispatcher.DispatchAsync(this, ea); - } - - internal async Task OnGuildBanRemoveEventAsync(TransportUser user, DiscordGuild guild) - { - DiscordUser usr = new(user) { Discord = this }; - usr = UpdateUserCache(usr); - - if (!guild.Members.TryGetValue(user.Id, out DiscordMember? mbr)) - { - mbr = new DiscordMember(usr) { Discord = this, guild_id = guild.Id }; - } - - GuildBanRemovedEventArgs ea = new() - { - Guild = guild, - Member = mbr - }; - - await this.dispatcher.DispatchAsync(this, ea); - } - - #endregion - - #region Guild Member - - internal async Task OnGuildMemberAddEventAsync(TransportMember member, DiscordGuild guild) - { - DiscordUser usr = new(member.User) { Discord = this }; - UpdateUserCache(usr); - - DiscordMember mbr = new(member) - { - Discord = this, - guild_id = guild.Id - }; - - guild.members[mbr.Id] = mbr; - guild.MemberCount++; - - GuildMemberAddedEventArgs ea = new() - { - Guild = guild, - Member = mbr - }; - - await this.dispatcher.DispatchAsync(this, ea); - } - - internal async Task OnGuildMemberRemoveEventAsync(TransportUser user, DiscordGuild guild) - { - DiscordUser usr = new(user); - - if (!guild.members.TryRemove(user.Id, out DiscordMember? mbr)) - { - mbr = new DiscordMember(usr) { Discord = this, guild_id = guild.Id }; - } - - guild.MemberCount--; - - UpdateUserCache(usr); - - GuildMemberRemovedEventArgs ea = new() - { - Guild = guild, - Member = mbr - }; - await this.dispatcher.DispatchAsync(this, ea); - } - - internal async Task OnGuildMemberUpdateEventAsync(TransportMember member, DiscordGuild guild) - { - DiscordUser userAfter = new(member.User) { Discord = this }; - _ = UpdateUserCache(userAfter); - - DiscordMember memberAfter = new(member) { Discord = this, guild_id = guild.Id }; - - if (!guild.Members.TryGetValue(member.User.Id, out DiscordMember? memberBefore)) - { - memberBefore = new DiscordMember(member) { Discord = this, guild_id = guild.Id }; - } - - guild.members.AddOrUpdate(member.User.Id, memberAfter, (_, _) => memberAfter); - - GuildMemberUpdatedEventArgs ea = new() - { - Guild = guild, - MemberAfter = memberAfter, - MemberBefore = memberBefore, - }; - - await this.dispatcher.DispatchAsync(this, ea); - } - - internal async Task OnGuildMembersChunkEventAsync(JObject dat) - { - DiscordGuild guild = this.Guilds[(ulong)dat["guild_id"]]; - int chunkIndex = (int)dat["chunk_index"]; - int chunkCount = (int)dat["chunk_count"]; - string? nonce = (string)dat["nonce"]; - - HashSet mbrs = []; - HashSet pres = []; - - TransportMember[] members = dat["members"].ToDiscordObject(); - - int memCount = members.Length; - for (int i = 0; i < memCount; i++) - { - DiscordMember mbr = new(members[i]) { Discord = this, guild_id = guild.Id }; - - if (!this.UserCache.ContainsKey(mbr.Id)) - { - this.UserCache[mbr.Id] = new DiscordUser(members[i].User) { Discord = this }; - } - - guild.members[mbr.Id] = mbr; - - mbrs.Add(mbr); - } - - guild.MemberCount = guild.members.Count; - - GuildMembersChunkedEventArgs ea = new() - { - Guild = guild, - Members = new ReadOnlySet(mbrs), - ChunkIndex = chunkIndex, - ChunkCount = chunkCount, - Nonce = nonce, - }; - - if (dat["presences"] != null) - { - DiscordPresence[] presences = dat["presences"].ToDiscordObject(); - - int presCount = presences.Length; - for (int i = 0; i < presCount; i++) - { - DiscordPresence xp = presences[i]; - xp.Discord = this; - xp.Activity = new DiscordActivity(xp.RawActivity); - - if (xp.RawActivities != null) - { - xp.internalActivities = new DiscordActivity[xp.RawActivities.Length]; - for (int j = 0; j < xp.RawActivities.Length; j++) - { - xp.internalActivities[j] = new DiscordActivity(xp.RawActivities[j]); - } - } - - pres.Add(xp); - } - - ea.Presences = new ReadOnlySet(pres); - } - - if (dat["not_found"] != null) - { - ISet nf = dat["not_found"].ToDiscordObject>(); - ea.NotFound = new ReadOnlySet(nf); - } - - _ = DispatchGuildMembersChunkForIteratorsAsync(ea); - - await this.dispatcher.DispatchAsync(this, ea); - } - - #endregion - - #region Guild Role - - internal async Task OnGuildRoleCreateEventAsync(DiscordRole role, DiscordGuild guild) - { - role.Discord = this; - role.guild_id = guild.Id; - - guild.roles[role.Id] = role; - - GuildRoleCreatedEventArgs ea = new() - { - Guild = guild, - Role = role - }; - - await this.dispatcher.DispatchAsync(this, ea); - } - - internal async Task OnGuildRoleUpdateEventAsync(DiscordRole role, DiscordGuild guild) - { - DiscordRole newRole = await guild.GetRoleAsync(role.Id); - DiscordRole oldRole = new() - { - guild_id = guild.Id, - color = newRole.color, - Discord = this, - IsHoisted = newRole.IsHoisted, - Id = newRole.Id, - IsManaged = newRole.IsManaged, - IsMentionable = newRole.IsMentionable, - Name = newRole.Name, - Permissions = newRole.Permissions, - Position = newRole.Position, - IconHash = newRole.IconHash, - emoji = newRole.emoji - }; - - newRole.guild_id = guild.Id; - newRole.color = role.color; - newRole.IsHoisted = role.IsHoisted; - newRole.IsManaged = role.IsManaged; - newRole.IsMentionable = role.IsMentionable; - newRole.Name = role.Name; - newRole.Permissions = role.Permissions; - newRole.Position = role.Position; - newRole.emoji = role.emoji; - newRole.IconHash = role.IconHash; - - GuildRoleUpdatedEventArgs ea = new() - { - Guild = guild, - RoleAfter = newRole, - RoleBefore = oldRole - }; - - await this.dispatcher.DispatchAsync(this, ea); - } - - internal async Task OnGuildRoleDeleteEventAsync(ulong roleId, DiscordGuild guild) - { - if (!guild.roles.TryRemove(roleId, out DiscordRole? role)) - { - this.Logger.LogWarning("Attempted to delete a nonexistent role ({RoleId}) from guild ({Guild}).", roleId, guild); - } - - GuildRoleDeletedEventArgs ea = new() - { - Guild = guild, - Role = role - }; - - await this.dispatcher.DispatchAsync(this, ea); - } - - #endregion - - #region Invite - - internal async Task OnInviteCreateEventAsync(ulong channelId, ulong guildId, DiscordInvite invite) - { - DiscordGuild guild = InternalGetCachedGuild(guildId); - DiscordChannel channel = InternalGetCachedChannel(channelId, guildId); - - invite.Discord = this; - - guild.invites[invite.Code] = invite; - - InviteCreatedEventArgs ea = new() - { - Channel = channel, - Guild = guild, - Invite = invite - }; - - await this.dispatcher.DispatchAsync(this, ea); - } - - internal async Task OnInviteDeleteEventAsync(ulong channelId, ulong guildId, JToken dat) - { - DiscordGuild guild = InternalGetCachedGuild(guildId); - DiscordChannel channel = InternalGetCachedChannel(channelId, guildId); - - if (!guild.invites.TryRemove(dat["code"].ToString(), out DiscordInvite? invite)) - { - invite = dat.ToDiscordObject(); - invite.Discord = this; - } - - invite.IsRevoked = true; - - InviteDeletedEventArgs ea = new() - { - Channel = channel, - Guild = guild, - Invite = invite - }; - - await this.dispatcher.DispatchAsync(this, ea); - } - - #endregion - - #region Message - - internal async Task OnMessageCreateEventAsync(DiscordMessage message, TransportUser author, TransportMember member, TransportUser referenceAuthor, TransportMember referenceMember) - { - message.Discord = this; - PopulateMessageReactionsAndCache(message, author, member); - message.PopulateMentions(); - - if (message.ReferencedMessage != null) - { - message.ReferencedMessage.Discord = this; - PopulateMessageReactionsAndCache(message.ReferencedMessage, referenceAuthor, referenceMember); - message.ReferencedMessage.PopulateMentions(); - } - - if (message.MessageSnapshots != null) - { - foreach (DiscordMessageSnapshot snapshot in message.MessageSnapshots) - { - if (snapshot?.Message != null) - { - snapshot.Message.PopulateMentions(); - } - } - } - - foreach (DiscordMessageSticker sticker in message.Stickers) - { - sticker.Discord = this; - } - - MessageCreatedEventArgs ea = new() - { - Message = message, - - MentionedUsers = new ReadOnlyCollection(message.mentionedUsers), - MentionedRoles = message.mentionedRoles != null ? new ReadOnlyCollection(message.mentionedRoles) : null, - MentionedChannels = message.mentionedChannels != null ? new ReadOnlyCollection(message.mentionedChannels) : null - }; - - await this.dispatcher.DispatchAsync(this, ea); - } - - internal async Task OnMessageUpdateEventAsync(DiscordMessage message, TransportUser author, TransportMember member, TransportUser referenceAuthor, TransportMember referenceMember) - { - message.Discord = this; - DiscordMessage event_message = message; - - DiscordMessage oldmsg = null; - - if (!this.MessageCache.TryGet(event_message.Id, out message)) // previous message was not in cache - { - message = event_message; - PopulateMessageReactionsAndCache(message, author, member); - - if (message.ReferencedMessage != null) - { - message.ReferencedMessage.Discord = this; - PopulateMessageReactionsAndCache(message.ReferencedMessage, referenceAuthor, referenceMember); - message.ReferencedMessage.PopulateMentions(); - } - - if (message.MessageSnapshots != null) - { - foreach (DiscordMessageSnapshot snapshot in message.MessageSnapshots) - { - if (snapshot?.Message != null) - { - snapshot.Message.PopulateMentions(); - } - } - } - } - else // previous message was fetched in cache - { - oldmsg = new DiscordMessage(message); - - // cached message is updated with information from the event message - message.EditedTimestamp = event_message.EditedTimestamp; - if (event_message.Content != null) - { - message.Content = event_message.Content; - } - - message.embeds.Clear(); - message.embeds.AddRange(event_message.embeds); - message.attachments.Clear(); - message.attachments.AddRange(event_message.attachments); - message.Pinned = event_message.Pinned; - message.IsTTS = event_message.IsTTS; - message.Poll = event_message.Poll; - - // Mentions - message.mentionedUsers.Clear(); - message.mentionedUsers.AddRange(event_message.mentionedUsers ?? []); - message.mentionedRoles.Clear(); - message.mentionedRoles.AddRange(event_message.mentionedRoles ?? []); - message.mentionedChannels.Clear(); - message.mentionedChannels.AddRange(event_message.mentionedChannels ?? []); - message.MentionEveryone = event_message.MentionEveryone; - } - - message.PopulateMentions(); - - MessageUpdatedEventArgs ea = new() - { - Message = message, - MessageBefore = oldmsg, - MentionedUsers = new ReadOnlyCollection(message.mentionedUsers), - MentionedRoles = message.mentionedRoles != null ? new ReadOnlyCollection(message.mentionedRoles) : null, - MentionedChannels = message.mentionedChannels != null ? new ReadOnlyCollection(message.mentionedChannels) : null - }; - - await this.dispatcher.DispatchAsync(this, ea); - } - - internal async Task OnMessageDeleteEventAsync(ulong messageId, ulong channelId, ulong? guildId) - { - DiscordGuild guild = InternalGetCachedGuild(guildId); - DiscordChannel? channel = InternalGetCachedChannel(channelId, guildId) ?? InternalGetCachedThread(channelId, guildId); - - if (channel == null) - { - channel = new DiscordDmChannel - { - Id = channelId, - Discord = this, - Type = DiscordChannelType.Private, - Recipients = Array.Empty() - - }; - - this.privateChannels[channelId] = (DiscordDmChannel)channel; - } - - if (!this.MessageCache.TryGet(messageId, out DiscordMessage? msg)) - { - msg = new DiscordMessage - { - Id = messageId, - ChannelId = channelId, - Discord = this, - }; - } - - this.MessageCache?.Remove(msg.Id); - - MessageDeletedEventArgs ea = new() - { - Message = msg, - Channel = channel, - Guild = guild, - }; - - await this.dispatcher.DispatchAsync(this, ea); - } - - private async Task OnMessagePollVoteEventAsync(DiscordPollVoteUpdate voteUpdate, bool wasAdded) - { - voteUpdate.WasAdded = wasAdded; - voteUpdate.client = this; - - MessagePollVotedEventArgs ea = new() - { - PollVoteUpdate = voteUpdate - }; - - await this.dispatcher.DispatchAsync(this, ea); - } - - internal async Task OnMessageBulkDeleteEventAsync(ulong[] messageIds, ulong channelId, ulong? guildId) - { - DiscordChannel? channel = InternalGetCachedChannel(channelId, guildId) ?? InternalGetCachedThread(channelId, guildId); - - List msgs = new(messageIds.Length); - foreach (ulong messageId in messageIds) - { - if (!this.MessageCache.TryGet(messageId, out DiscordMessage? msg)) - { - msg = new DiscordMessage - { - Id = messageId, - ChannelId = channelId, - Discord = this, - }; - } - - this.MessageCache?.Remove(msg.Id); - - msgs.Add(msg); - } - - DiscordGuild guild = InternalGetCachedGuild(guildId); - - MessagesBulkDeletedEventArgs ea = new() - { - Channel = channel, - Messages = new ReadOnlyCollection(msgs), - Guild = guild - }; - - await this.dispatcher.DispatchAsync(this, ea); - } - - #endregion - - #region Message Reaction - - internal async Task OnMessageReactionAddAsync(ulong userId, ulong messageId, ulong channelId, ulong? guildId, TransportMember mbr, DiscordEmoji emoji) - { - DiscordChannel? channel = InternalGetCachedChannel(channelId, guildId) ?? InternalGetCachedThread(channelId, guildId); - DiscordGuild? guild = InternalGetCachedGuild(guildId); - - emoji.Discord = this; - - DiscordUser usr = null!; - usr = !TryGetCachedUserInternal(userId, out usr) - ? UpdateUser(new DiscordUser { Id = userId, Discord = this }, guildId, guild, mbr) - : UpdateUser(usr, guild?.Id, guild, mbr); - - if (channel == null) - { - channel = new DiscordDmChannel - { - Id = channelId, - Discord = this, - Type = DiscordChannelType.Private, - Recipients = new DiscordUser[] { usr } - }; - this.privateChannels[channelId] = (DiscordDmChannel)channel; - } - - if (!this.MessageCache.TryGet(messageId, out DiscordMessage? msg)) - { - msg = new DiscordMessage - { - Id = messageId, - ChannelId = channelId, - Discord = this, - reactions = [] - }; - } - - DiscordReaction? react = msg.reactions.FirstOrDefault(xr => xr.Emoji == emoji); - - if (react == null) - { - msg.reactions.Add(react = new DiscordReaction - { - Count = 1, - Emoji = emoji, - IsMe = this.CurrentUser.Id == userId - }); - } - else - { - react.Count++; - react.IsMe |= this.CurrentUser.Id == userId; - } - - MessageReactionAddedEventArgs ea = new() - { - Message = msg, - User = usr, - Guild = guild, - Emoji = emoji - }; - - await this.dispatcher.DispatchAsync(this, ea); - } - - internal async Task OnMessageReactionRemoveAsync(ulong userId, ulong messageId, ulong channelId, ulong? guildId, DiscordEmoji emoji) - { - DiscordChannel? channel = InternalGetCachedChannel(channelId, guildId) ?? InternalGetCachedThread(channelId, guildId); - - emoji.Discord = this; - - if (!this.UserCache.TryGetValue(userId, out DiscordUser? usr)) - { - usr = new DiscordUser { Id = userId, Discord = this }; - } - - if (channel == null) - { - channel = new DiscordDmChannel - { - Id = channelId, - Discord = this, - Type = DiscordChannelType.Private, - Recipients = new DiscordUser[] { usr } - }; - this.privateChannels[channelId] = (DiscordDmChannel)channel; - } - - if (channel?.Guild != null) - { - usr = channel.Guild.Members.TryGetValue(userId, out DiscordMember? member) - ? member - : new DiscordMember(usr) { Discord = this, guild_id = channel.GuildId.Value }; - } - - if (!this.MessageCache.TryGet(messageId, out DiscordMessage? msg)) - { - msg = new DiscordMessage - { - Id = messageId, - ChannelId = channelId, - Discord = this - }; - } - - DiscordReaction? react = msg.reactions?.FirstOrDefault(xr => xr.Emoji == emoji); - if (react != null) - { - react.Count--; - react.IsMe &= this.CurrentUser.Id != userId; - - if (msg.reactions != null && react.Count <= 0) // shit happens - { - for (int i = 0; i < msg.reactions.Count; i++) - { - if (msg.reactions[i].Emoji == emoji) - { - msg.reactions.RemoveAt(i); - break; - } - } - } - } - - DiscordGuild guild = InternalGetCachedGuild(guildId); - - MessageReactionRemovedEventArgs ea = new() - { - Message = msg, - User = usr, - Guild = guild, - Emoji = emoji - }; - await this.dispatcher.DispatchAsync(this, ea); - } - - internal async Task OnMessageReactionRemoveAllAsync(ulong messageId, ulong channelId, ulong? guildId) - { - _ = InternalGetCachedChannel(channelId, guildId) ?? InternalGetCachedThread(channelId, guildId); - - if (!this.MessageCache.TryGet(messageId, out DiscordMessage? msg)) - { - msg = new DiscordMessage - { - Id = messageId, - ChannelId = channelId, - Discord = this - }; - } - - msg.reactions?.Clear(); - - MessageReactionsClearedEventArgs ea = new() - { - Message = msg, - }; - - await this.dispatcher.DispatchAsync(this, ea); - } - - internal async Task OnMessageReactionRemoveEmojiAsync(ulong messageId, ulong channelId, ulong guildId, JToken dat) - { - DiscordGuild guild = InternalGetCachedGuild(guildId); - DiscordChannel? channel = InternalGetCachedChannel(channelId, guildId) ?? InternalGetCachedThread(channelId, guildId); - - if (channel == null) - { - channel = new DiscordDmChannel - { - Id = channelId, - Discord = this, - Type = DiscordChannelType.Private, - Recipients = Array.Empty() - }; - this.privateChannels[channelId] = (DiscordDmChannel)channel; - } - - if (!this.MessageCache.TryGet(messageId, out DiscordMessage? msg)) - { - msg = new DiscordMessage - { - Id = messageId, - ChannelId = channelId, - Discord = this - }; - } - - DiscordEmoji partialEmoji = dat.ToDiscordObject(); - - if (!guild.emojis.TryGetValue(partialEmoji.Id, out DiscordEmoji? emoji)) - { - emoji = partialEmoji; - emoji.Discord = this; - } - - msg.reactions?.RemoveAll(r => r.Emoji.Equals(emoji)); - - MessageReactionRemovedEmojiEventArgs ea = new() - { - Message = msg, - Channel = channel, - Guild = guild, - Emoji = emoji - }; - - await this.dispatcher.DispatchAsync(this, ea); - } - - #endregion - - #region User/Presence Update - - internal async Task OnPresenceUpdateEventAsync(JObject rawPresence, JObject rawUser) - { - ulong uid = (ulong)rawUser["id"]; - DiscordPresence old = null; - - if (this.presences.TryGetValue(uid, out DiscordPresence? presence)) - { - old = new DiscordPresence(presence); - DiscordJson.PopulateObject(rawPresence, presence); - } - else - { - presence = rawPresence.ToDiscordObject(); - presence.Discord = this; - presence.Activity = new DiscordActivity(presence.RawActivity); - this.presences[presence.InternalUser.Id] = presence; - } - - // reuse arrays / avoid linq (this is a hot zone) - if (presence.Activities == null || rawPresence["activities"] == null) - { - presence.internalActivities = []; - } - else - { - if (presence.internalActivities.Length != presence.RawActivities.Length) - { - presence.internalActivities = new DiscordActivity[presence.RawActivities.Length]; - } - - for (int i = 0; i < presence.internalActivities.Length; i++) - { - presence.internalActivities[i] = new DiscordActivity(presence.RawActivities[i]); - } - - if (presence.internalActivities.Length > 0) - { - presence.RawActivity = presence.RawActivities[0]; - - if (presence.Activity != null) - { - presence.Activity.UpdateWith(presence.RawActivity); - } - else - { - presence.Activity = new DiscordActivity(presence.RawActivity); - } - } - else - { - presence.RawActivity = null; - presence.Activity = null; - } - } - - // Caching partial objects is not a good idea, but considering these - // Objects will most likely be GC'd immediately after this event, - // This probably isn't great for GC pressure because this is a hot zone. - _ = this.UserCache.TryGetValue(uid, out DiscordUser? usr); - - DiscordUser usrafter = usr ?? new DiscordUser(presence.InternalUser); - PresenceUpdatedEventArgs ea = new() - { - Status = presence.Status, - Activity = presence.Activity, - User = usr, - PresenceBefore = old, - PresenceAfter = presence, - UserBefore = old != null ? new DiscordUser(old.InternalUser) { Discord = this } : usrafter, - UserAfter = usrafter - }; - - await this.dispatcher.DispatchAsync(this, ea); - } - - internal async Task OnUserSettingsUpdateEventAsync(TransportUser user) - { - DiscordUser usr = new(user) { Discord = this }; - - UserSettingsUpdatedEventArgs ea = new() - { - User = usr - }; - - await this.dispatcher.DispatchAsync(this, ea); - } - - internal async Task OnUserUpdateEventAsync(TransportUser user) - { - DiscordUser usr_old = new() - { - AvatarHash = this.CurrentUser.AvatarHash, - Discord = this, - Discriminator = this.CurrentUser.Discriminator, - Email = this.CurrentUser.Email, - Id = this.CurrentUser.Id, - IsBot = this.CurrentUser.IsBot, - MfaEnabled = this.CurrentUser.MfaEnabled, - Username = this.CurrentUser.Username, - Verified = this.CurrentUser.Verified - }; - - this.CurrentUser.AvatarHash = user.AvatarHash; - this.CurrentUser.Discriminator = user.Discriminator; - this.CurrentUser.Email = user.Email; - this.CurrentUser.Id = user.Id; - this.CurrentUser.IsBot = user.IsBot; - this.CurrentUser.MfaEnabled = user.MfaEnabled; - this.CurrentUser.Username = user.Username; - this.CurrentUser.Verified = user.Verified; - - UserUpdatedEventArgs ea = new() - { - UserAfter = this.CurrentUser, - UserBefore = usr_old - }; - - await this.dispatcher.DispatchAsync(this, ea); - } - - #endregion - - #region Voice - - internal async Task OnVoiceStateUpdateEventAsync(JObject raw) - { - ulong gid = (ulong)raw["guild_id"]; - ulong uid = (ulong)raw["user_id"]; - DiscordGuild gld = this.guilds[gid]; - - DiscordVoiceState vstateNew = raw.ToDiscordObject(); - vstateNew.Discord = this; - - gld.voiceStates.TryRemove(uid, out DiscordVoiceState? vstateOld); - - if (vstateNew.ChannelId != null) - { - gld.voiceStates[vstateNew.UserId] = vstateNew; - } - - if (gld.members.TryGetValue(uid, out DiscordMember? mbr)) - { - mbr.IsMuted = vstateNew.IsServerMuted; - mbr.IsDeafened = vstateNew.IsServerDeafened; - } - else - { - TransportMember transportMbr = vstateNew.TransportMember; - UpdateUser(new DiscordUser(transportMbr.User) { Discord = this }, gid, gld, transportMbr); - } - - VoiceStateUpdatedEventArgs ea = new() - { - SessionId = vstateNew.SessionId, - - Before = vstateOld, - After = vstateNew - }; - - await this.dispatcher.DispatchAsync(this, ea); - } - - internal async Task OnVoiceServerUpdateEventAsync(string endpoint, string token, DiscordGuild guild) - { - VoiceServerUpdatedEventArgs ea = new() - { - Endpoint = endpoint, - VoiceToken = token, - Guild = guild - }; - - await this.dispatcher.DispatchAsync(this, ea); - } - - #endregion - - #region Thread - - internal async Task OnThreadCreateEventAsync(DiscordThreadChannel thread) - { - thread.Discord = this; - InternalGetCachedGuild(thread.GuildId).threads[thread.Id] = thread; - - await this.dispatcher.DispatchAsync(this, new ThreadCreatedEventArgs - { - Thread = thread, - Guild = thread.Guild, - Parent = thread.Parent - }); - } - - internal async Task OnThreadUpdateEventAsync(DiscordThreadChannel thread) - { - if (thread == null) - { - return; - } - - DiscordThreadChannel threadOld; - ThreadUpdatedEventArgs updateEvent; - - thread.Discord = this; - - DiscordGuild guild = thread.Guild; - guild.Discord = this; - - DiscordThreadChannel cthread = InternalGetCachedThread(thread.Id, thread.GuildId); - - if (cthread != null) //thread is cached - { - threadOld = new DiscordThreadChannel - { - Discord = this, - GuildId = cthread.GuildId, - CreatorId = cthread.CreatorId, - ParentId = cthread.ParentId, - Id = cthread.Id, - Name = cthread.Name, - Type = cthread.Type, - LastMessageId = cthread.LastMessageId, - MessageCount = cthread.MessageCount, - MemberCount = cthread.MemberCount, - ThreadMetadata = cthread.ThreadMetadata, - CurrentMember = cthread.CurrentMember, - }; - - updateEvent = new ThreadUpdatedEventArgs - { - ThreadAfter = thread, - ThreadBefore = threadOld, - Guild = thread.Guild, - Parent = thread.Parent - }; - } - else - { - updateEvent = new ThreadUpdatedEventArgs - { - ThreadAfter = thread, - Guild = thread.Guild, - Parent = thread.Parent - }; - guild.threads[thread.Id] = thread; - } - - await this.dispatcher.DispatchAsync(this, updateEvent); - } - - internal async Task OnThreadDeleteEventAsync(DiscordThreadChannel thread) - { - if (thread == null) - { - return; - } - - thread.Discord = this; - - DiscordGuild gld = thread.Guild; - if (gld.threads.TryRemove(thread.Id, out DiscordThreadChannel? cachedThread)) - { - thread = cachedThread; - } - - await this.dispatcher.DispatchAsync(this, new ThreadDeletedEventArgs - { - Thread = thread, - Guild = thread.Guild, - Parent = thread.Parent - }); - } - - internal async Task OnThreadListSyncEventAsync(DiscordGuild guild, IReadOnlyList channel_ids, IReadOnlyList threads, IReadOnlyList members) - { - guild.Discord = this; - IEnumerable channels = channel_ids.Select(x => guild.GetChannel(x) ?? new DiscordChannel { Id = x, GuildId = guild.Id }); //getting channel objects - - foreach (DiscordChannel? channel in channels) - { - channel.Discord = this; - } - - foreach (DiscordThreadChannel thread in threads) - { - thread.Discord = this; - guild.threads[thread.Id] = thread; - } - - foreach (DiscordThreadChannelMember member in members) - { - member.Discord = this; - member.guild_id = guild.Id; - - DiscordThreadChannel? thread = threads.SingleOrDefault(x => x.Id == member.ThreadId); - if (thread != null) - { - thread.CurrentMember = member; - } - } - - await this.dispatcher.DispatchAsync(this, new ThreadListSyncedEventArgs - { - Guild = guild, - Channels = channels.ToList().AsReadOnly(), - Threads = threads, - CurrentMembers = members.ToList().AsReadOnly() - }); - } - - internal async Task OnThreadMemberUpdateEventAsync(DiscordGuild guild, DiscordThreadChannelMember member) - { - member.Discord = this; - - DiscordThreadChannel thread = InternalGetCachedThread(member.ThreadId, guild.Id); - member.guild_id = guild.Id; - thread.CurrentMember = member; - guild.threads[thread.Id] = thread; - - await this.dispatcher.DispatchAsync(this, new ThreadMemberUpdatedEventArgs - { - ThreadMember = member, - Thread = thread - }); - } - - internal async Task OnThreadMembersUpdateEventAsync(DiscordGuild guild, ulong thread_id, IReadOnlyList addedMembers, IReadOnlyList removed_member_ids, int member_count) - { - DiscordThreadChannel? thread = InternalGetCachedThread(thread_id, guild.Id) ?? new DiscordThreadChannel - { - Id = thread_id, - GuildId = guild.Id, - }; - thread.Discord = this; - guild.Discord = this; - thread.MemberCount = member_count; - - List removedMembers = []; - if (removed_member_ids != null) - { - foreach (ulong? removedId in removed_member_ids) - { - removedMembers.Add(guild.members.TryGetValue(removedId.Value, out DiscordMember? member) ? member : new DiscordMember { Id = removedId.Value, guild_id = guild.Id, Discord = this }); - } - - if (removed_member_ids.Contains(this.CurrentUser.Id)) //indicates the bot was removed from the thread - { - thread.CurrentMember = null; - } - } - else - { - removed_member_ids = Array.Empty(); - } - - if (addedMembers != null) - { - foreach (DiscordThreadChannelMember threadMember in addedMembers) - { - threadMember.Discord = this; - threadMember.guild_id = guild.Id; - } - - if (addedMembers.Any(member => member.Id == this.CurrentUser.Id)) - { - thread.CurrentMember = addedMembers.Single(member => member.Id == this.CurrentUser.Id); - } - } - else - { - addedMembers = Array.Empty(); - } - - ThreadMembersUpdatedEventArgs threadMembersUpdateArg = new() - { - Guild = guild, - Thread = thread, - AddedMembers = addedMembers, - RemovedMembers = removedMembers, - MemberCount = member_count - }; - - await this.dispatcher.DispatchAsync(this, threadMembersUpdateArg); - } - - #endregion - - #region Integration - - internal async Task OnIntegrationCreateAsync(DiscordIntegration integration, ulong guild_id) - { - DiscordGuild? guild = InternalGetCachedGuild(guild_id) ?? new DiscordGuild - { - Id = guild_id, - Discord = this - }; - - IntegrationCreatedEventArgs ea = new() - { - Guild = guild, - Integration = integration - }; - - await this.dispatcher.DispatchAsync(this, ea); - } - - internal async Task OnIntegrationUpdateAsync(DiscordIntegration integration, ulong guild_id) - { - DiscordGuild? guild = InternalGetCachedGuild(guild_id) ?? new DiscordGuild - { - Id = guild_id, - Discord = this - }; - - IntegrationUpdatedEventArgs ea = new() - { - Guild = guild, - Integration = integration - }; - - await this.dispatcher.DispatchAsync(this, ea); - } - - internal async Task OnIntegrationDeleteAsync(ulong integration_id, ulong guild_id, ulong? application_id) - { - DiscordGuild? guild = InternalGetCachedGuild(guild_id) ?? new DiscordGuild - { - Id = guild_id, - Discord = this - }; - - IntegrationDeletedEventArgs ea = new() - { - Guild = guild, - Applicationid = application_id, - IntegrationId = integration_id - }; - - await this.dispatcher.DispatchAsync(this, ea); - } - - #endregion - - #region Commands - - internal async Task OnApplicationCommandPermissionsUpdateAsync(JObject obj) - { - ApplicationCommandPermissionsUpdatedEventArgs ev = obj.ToObject()!; - - await this.dispatcher.DispatchAsync(this, ev); - } - - #endregion - - #region Stage Instance - - internal async Task OnStageInstanceCreateAsync(DiscordStageInstance instance) - { - instance.Discord = this; - - DiscordGuild guild = InternalGetCachedGuild(instance.GuildId); - - guild.stageInstances[instance.Id] = instance; - - StageInstanceCreatedEventArgs eventArgs = new() - { - StageInstance = instance - }; - - await this.dispatcher.DispatchAsync(this, eventArgs); - } - - internal async Task OnStageInstanceUpdateAsync(DiscordStageInstance instance) - { - instance.Discord = this; - - DiscordGuild guild = InternalGetCachedGuild(instance.GuildId); - - if (!guild.stageInstances.TryRemove(instance.Id, out DiscordStageInstance? oldInstance)) - { - oldInstance = new DiscordStageInstance { Id = instance.Id, GuildId = instance.GuildId, ChannelId = instance.ChannelId }; - } - - guild.stageInstances[instance.Id] = instance; - - StageInstanceUpdatedEventArgs eventArgs = new() - { - StageInstanceBefore = oldInstance, - StageInstanceAfter = instance - }; - - await this.dispatcher.DispatchAsync(this, eventArgs); - } - - internal async Task OnStageInstanceDeleteAsync(DiscordStageInstance instance) - { - instance.Discord = this; - - DiscordGuild guild = InternalGetCachedGuild(instance.GuildId); - - guild.stageInstances.TryRemove(instance.Id, out _); - - StageInstanceDeletedEventArgs eventArgs = new() - { - StageInstance = instance - }; - - await this.dispatcher.DispatchAsync(this, eventArgs); - } - - #endregion - - #region Misc - - internal async Task OnApplicationAuthorizedAsync(DiscordWebhookEventBody body) - { - ApplicationAuthorizedPayload payload = body.Data.ToDiscordObject(); - - DiscordGuild? guild = payload.Guild; - DiscordUser user = payload.User; - - UpdateUserCache(user); - - if (guild is not null) - { - if (this.guilds.TryGetValue(guild.Id, out DiscordGuild? cachedGuild)) - { - guild = cachedGuild; - } - - guild.Discord = this; - - if (guild.Members.TryGetValue(user.Id, out DiscordMember? member)) - { - user = member; - } - } - else - { - user.Discord = this; - } - - ApplicationAuthorizedEventArgs eventArgs = new() - { - Guild = guild, - IntegrationType = payload.IntegrationType, - Scopes = payload.Scopes, - User = user, - Timestamp = body.Timestamp - }; - - await this.dispatcher.DispatchAsync(this, eventArgs); - } - - internal async Task OnInteractionCreateAsync(ulong? guildId, ulong channelId, TransportUser user, TransportMember member, DiscordChannel? channel, DiscordInteraction interaction) - { - DiscordUser usr = new(user) { Discord = this }; - - interaction.ChannelId = channelId; - interaction.GuildId = guildId; - interaction.Discord = this; - interaction.Data.Discord = this; - - if (member is not null && guildId is not null && interaction.Guild is not null) - { - usr = new DiscordMember(member) { guild_id = guildId.Value, Discord = this }; - UpdateUser(usr, guildId, interaction.Guild, member); - } - else - { - UpdateUserCache(usr); - } - - interaction.User = usr; - - DiscordInteractionResolvedCollection resolved = interaction.Data.Resolved; - if (resolved != null) - { - if (resolved.Users != null) - { - foreach (KeyValuePair c in resolved.Users) - { - c.Value.Discord = this; - UpdateUserCache(c.Value); - } - } - - if (resolved.Members != null) - { - foreach (KeyValuePair c in resolved.Members) - { - c.Value.Discord = this; - c.Value.Id = c.Key; - c.Value.guild_id = guildId.Value; - c.Value.User.Discord = this; - - UpdateUserCache(c.Value.User); - } - } - - if (resolved.Channels != null) - { - foreach (KeyValuePair c in resolved.Channels) - { - c.Value.Discord = this; - UpdateChannelCache(c.Value); - if (guildId.HasValue) - { - c.Value.GuildId = guildId.Value; - } - } - } - - if (resolved.Roles != null) - { - foreach (KeyValuePair c in resolved.Roles) - { - c.Value.Discord = this; - - if (guildId.HasValue) - { - c.Value.guild_id = guildId.Value; - if (this.guilds.TryGetValue(guildId.Value, out DiscordGuild? guild)) - { - guild.roles.TryAdd(c.Value.Id, c.Value); - } - } - } - } - - if (resolved.Messages != null) - { - foreach (KeyValuePair m in resolved.Messages) - { - m.Value.Discord = this; - - if (guildId.HasValue) - { - m.Value.guildId = guildId.Value; - } - - this.MessageCache?.Add(m.Value); - } - } - } - - UpdateChannelCache(channel); - - if (interaction.Type is DiscordInteractionType.Component) - { - - interaction.Message.Discord = this; - interaction.Message.ChannelId = interaction.ChannelId; - ComponentInteractionCreatedEventArgs cea = new() - { - Message = interaction.Message, - Interaction = interaction - }; - - await this.dispatcher.DispatchAsync(this, cea); - } - else if (interaction.Type is DiscordInteractionType.ModalSubmit) - { - ModalSubmittedEventArgs mea = new(interaction); - - await this.dispatcher.DispatchAsync(this, mea); - } - else if (interaction.Data.Type is DiscordApplicationCommandType.MessageContextMenu or DiscordApplicationCommandType.UserContextMenu) // Context-Menu. // - { - ulong targetId = interaction.Data.Target.Value; - DiscordUser targetUser = null; - DiscordMember targetMember = null; - DiscordMessage targetMessage = null; - - interaction.Data.Resolved.Messages?.TryGetValue(targetId, out targetMessage); - interaction.Data.Resolved.Members?.TryGetValue(targetId, out targetMember); - interaction.Data.Resolved.Users?.TryGetValue(targetId, out targetUser); - - ContextMenuInteractionCreatedEventArgs ctea = new() - { - Interaction = interaction, - TargetUser = targetMember ?? targetUser, - TargetMessage = targetMessage, - Type = interaction.Data.Type, - }; - - await this.dispatcher.DispatchAsync(this, ctea); - } - - InteractionCreatedEventArgs ea = new() - { - Interaction = interaction - }; - - await this.dispatcher.DispatchAsync(this, ea); - } - - internal async Task OnTypingStartEventAsync(ulong userId, ulong channelId, DiscordChannel channel, ulong? guildId, DateTimeOffset started, TransportMember mbr) - { - if (channel == null) - { - channel = new DiscordChannel - { - Discord = this, - Id = channelId, - GuildId = guildId ?? default, - }; - } - - DiscordGuild guild = InternalGetCachedGuild(guildId); - DiscordUser usr = UpdateUser(new DiscordUser { Id = userId, Discord = this }, guildId, guild, mbr); - - TypingStartedEventArgs ea = new() - { - Channel = channel, - User = usr, - Guild = guild, - StartedAt = started - }; - await this.dispatcher.DispatchAsync(this, ea); - } - - internal async Task OnWebhooksUpdateAsync(DiscordChannel channel, DiscordGuild guild) - { - WebhooksUpdatedEventArgs ea = new() - { - Channel = channel, - Guild = guild - }; - await this.dispatcher.DispatchAsync(this, ea); - } - - internal async Task OnStickersUpdatedAsync(IEnumerable newStickers, JObject raw) - { - DiscordGuild guild = InternalGetCachedGuild((ulong)raw["guild_id"]); - ConcurrentDictionary oldStickers = new(guild.stickers); - - guild.stickers.Clear(); - - foreach (DiscordMessageSticker nst in newStickers) - { - if (nst.User != null) - { - nst.User.Discord = this; - } - - nst.Discord = this; - - guild.stickers[nst.Id] = nst; - } - - GuildStickersUpdatedEventArgs sea = new() - { - Guild = guild, - StickersBefore = oldStickers, - StickersAfter = guild.Stickers - }; - - await this.dispatcher.DispatchAsync(this, sea); - } - - internal async Task OnUnknownEventAsync(GatewayPayload payload) - { - UnknownEventArgs ea = new() { EventName = payload.EventName, Json = (payload.Data as JObject)?.ToString() }; - await this.dispatcher.DispatchAsync(this, ea); - } - - internal async Task OnUnknownWebhookEventAsync(DiscordWebhookEventBody body) - { - UnknownEventArgs eventArgs = new() - { - EventName = body.Type.ToString(), - Json = body.Data?.ToString() - }; - - await this.dispatcher.DispatchAsync(this, eventArgs); - } - - #endregion - - #region AutoModeration - internal async Task OnAutoModerationRuleCreateAsync(DiscordAutoModerationRule ruleCreated) - { - ruleCreated.Discord = this; - await this.dispatcher.DispatchAsync(this, new AutoModerationRuleCreatedEventArgs - { - Rule = ruleCreated - }); - } - - internal async Task OnAutoModerationRuleUpdatedAsync(DiscordAutoModerationRule ruleUpdated) - { - ruleUpdated.Discord = this; - await this.dispatcher.DispatchAsync(this, new AutoModerationRuleUpdatedEventArgs - { - Rule = ruleUpdated - }); - } - - internal async Task OnAutoModerationRuleDeletedAsync(DiscordAutoModerationRule ruleDeleted) - { - ruleDeleted.Discord = this; - await this.dispatcher.DispatchAsync(this, new AutoModerationRuleDeletedEventArgs - { - Rule = ruleDeleted - }); - } - - internal async Task OnAutoModerationRuleExecutedAsync(DiscordAutoModerationActionExecution ruleExecuted) - { - await this.dispatcher.DispatchAsync(this, new AutoModerationRuleExecutedEventArgs - { - Rule = ruleExecuted - }); - } - #endregion - - #region Entitlements - - private async Task OnEntitlementCreatedAsync(DiscordEntitlement entitlement) - => await this.dispatcher.DispatchAsync(this, new EntitlementCreatedEventArgs { Entitlement = entitlement }); - - private async Task OnWebhookEntitlementCreateAsync(DiscordWebhookEventBody body) - { - await this.dispatcher.DispatchAsync - ( - this, - new EntitlementCreatedEventArgs - { - Entitlement = body.Data.ToDiscordObject(), - Timestamp = body.Timestamp - } - ); - } - - private async Task OnEntitlementUpdatedAsync(DiscordEntitlement entitlement) - => await this.dispatcher.DispatchAsync(this, new EntitlementUpdatedEventArgs { Entitlement = entitlement }); - - private async Task OnEntitlementDeletedAsync(DiscordEntitlement entitlement) - => await this.dispatcher.DispatchAsync(this, new EntitlementDeletedEventArgs { Entitlement = entitlement }); - - #endregion - - #endregion -} +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Globalization; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using DSharpPlus.Entities; +using DSharpPlus.Entities.AuditLogs; +using DSharpPlus.EventArgs; +using DSharpPlus.Net.Abstractions; +using DSharpPlus.Net.InboundWebhooks; +using DSharpPlus.Net.InboundWebhooks.Payloads; +using DSharpPlus.Net.Serialization; + +using Microsoft.Extensions.Logging; +using Newtonsoft.Json.Linq; + +namespace DSharpPlus; + +public sealed partial class DiscordClient +{ + #region Private Fields + + private string sessionId; + private string? gatewayResumeUrl; + private bool guildDownloadCompleted = false; + + #endregion + + #region Dispatch Handler + + private async Task ReceiveGatewayEventsAsync() + { + while (!this.eventReader.Completion.IsCompleted) + { + GatewayPayload payload = await this.eventReader.ReadAsync(); + + try + { + await HandleDispatchAsync(payload); + } + catch (Exception ex) + { + this.Logger.LogError(ex, "Dispatch threw an exception: "); + } + } + } + + internal async Task HandleDispatchAsync(GatewayPayload payload) + { + if (payload.Data is not JObject dat) + { + this.Logger.LogWarning(LoggerEvents.WebSocketReceive, "Invalid payload body (this message is probably safe to ignore); opcode: {Op} event: {Event}; payload: {Payload}", payload.OpCode, payload.EventName, payload.Data); + return; + } + + if (payload.OpCode is not GatewayOpCode.Dispatch) + { + return; + } + + DiscordChannel chn; + DiscordThreadChannel thread; + ulong gid; + ulong cid; + TransportUser usr; + TransportMember mbr = default; + TransportUser refUsr = default; + TransportMember refMbr = default; + JToken rawMbr; + JToken? rawRefMsg = dat["referenced_message"]; + JArray rawMembers; + JArray rawPresences; + + switch (payload.EventName.ToLowerInvariant()) + { + #region Gateway Status + + case "ready": + JArray? glds = (JArray?)dat["guilds"]; + JArray? dmcs = (JArray?)dat["private_channels"]; + + int readyShardId = payload is ShardIdContainingGatewayPayload { ShardId: { } id } ? id : 0; + + await OnReadyEventAsync(dat.ToDiscordObject(), glds, dmcs, readyShardId); + break; + + case "resumed": + int resumedShardId = payload is ShardIdContainingGatewayPayload { ShardId: { } otherId } ? otherId : 0; + + await OnResumedAsync(resumedShardId); + break; + + #endregion + + #region Channel + + case "channel_create": + chn = dat.ToDiscordObject(); + await OnChannelCreateEventAsync(chn); + break; + + case "channel_update": + await OnChannelUpdateEventAsync(dat.ToDiscordObject()); + break; + + case "channel_delete": + bool isPrivate = dat["is_private"]?.ToObject() ?? false; + + chn = isPrivate ? dat.ToDiscordObject() : dat.ToDiscordObject(); + await OnChannelDeleteEventAsync(chn); + break; + + case "channel_pins_update": + cid = (ulong)dat["channel_id"]; + string? ts = (string)dat["last_pin_timestamp"]; + await OnChannelPinsUpdateAsync((ulong?)dat["guild_id"], cid, ts != null ? DateTimeOffset.Parse(ts, CultureInfo.InvariantCulture) : default(DateTimeOffset?)); + break; + + #endregion + + #region Scheduled Guild Events + + case "guild_scheduled_event_create": + DiscordScheduledGuildEvent cevt = dat.ToDiscordObject(); + await OnScheduledGuildEventCreateEventAsync(cevt); + break; + case "guild_scheduled_event_delete": + DiscordScheduledGuildEvent devt = dat.ToDiscordObject(); + await OnScheduledGuildEventDeleteEventAsync(devt); + break; + case "guild_scheduled_event_update": + DiscordScheduledGuildEvent uevt = dat.ToDiscordObject(); + await OnScheduledGuildEventUpdateEventAsync(uevt); + break; + case "guild_scheduled_event_user_add": + gid = (ulong)dat["guild_id"]; + ulong uid = (ulong)dat["user_id"]; + ulong eid = (ulong)dat["guild_scheduled_event_id"]; + await OnScheduledGuildEventUserAddEventAsync(gid, eid, uid); + break; + case "guild_scheduled_event_user_remove": + gid = (ulong)dat["guild_id"]; + uid = (ulong)dat["user_id"]; + eid = (ulong)dat["guild_scheduled_event_id"]; + await OnScheduledGuildEventUserRemoveEventAsync(gid, eid, uid); + break; + #endregion + + #region Guild + + case "guild_create": + + rawMembers = (JArray)dat["members"]; + rawPresences = (JArray)dat["presences"]; + dat.Remove("members"); + dat.Remove("presences"); + + await OnGuildCreateEventAsync(dat.ToDiscordObject(), rawMembers, rawPresences.ToDiscordObject>()); + break; + + case "guild_update": + + rawMembers = (JArray)dat["members"]; + dat.Remove("members"); + + await OnGuildUpdateEventAsync(dat.ToDiscordObject(), rawMembers); + break; + + case "guild_delete": + dat.Remove("members"); + + await OnGuildDeleteEventAsync(dat.ToDiscordObject()); + break; + + case "guild_emojis_update": + gid = (ulong)dat["guild_id"]; + IEnumerable ems = dat["emojis"].ToDiscordObject>(); + await OnGuildEmojisUpdateEventAsync(this.guilds[gid], ems); + break; + + case "guild_integrations_update": + gid = (ulong)dat["guild_id"]; + + // discord fires this event inconsistently if the current user leaves a guild. + if (!this.guilds.TryGetValue(gid, out DiscordGuild value)) + { + return; + } + + await OnGuildIntegrationsUpdateEventAsync(value); + break; + + case "guild_audit_log_entry_create": + gid = (ulong)dat["guild_id"]; + DiscordGuild guild = this.guilds[gid]; + AuditLogAction auditLogAction = dat.ToDiscordObject(); + DiscordAuditLogEntry entry = await AuditLogParser.ParseAuditLogEntryAsync(guild, auditLogAction); + await OnGuildAuditLogEntryCreateEventAsync(guild, entry); + break; + + #endregion + + #region Guild Ban + + case "guild_ban_add": + usr = dat["user"].ToDiscordObject(); + gid = (ulong)dat["guild_id"]; + await OnGuildBanAddEventAsync(usr, this.guilds[gid]); + break; + + case "guild_ban_remove": + usr = dat["user"].ToDiscordObject(); + gid = (ulong)dat["guild_id"]; + await OnGuildBanRemoveEventAsync(usr, this.guilds[gid]); + break; + + #endregion + + #region Guild Member + + case "guild_member_add": + gid = (ulong)dat["guild_id"]; + await OnGuildMemberAddEventAsync(dat.ToDiscordObject(), this.guilds[gid]); + break; + + case "guild_member_remove": + gid = (ulong)dat["guild_id"]; + usr = dat["user"].ToDiscordObject(); + + if (!this.guilds.TryGetValue(gid, out value)) + { + // discord fires this event inconsistently if the current user leaves a guild. + if (usr.Id != this.CurrentUser.Id) + { + this.Logger.LogError(LoggerEvents.WebSocketReceive, "Could not find {Guild} in guild cache", gid); + } + + return; + } + + await OnGuildMemberRemoveEventAsync(usr, value); + break; + + case "guild_member_update": + gid = (ulong)dat["guild_id"]; + await OnGuildMemberUpdateEventAsync(dat.ToDiscordObject(), this.guilds[gid]); + break; + + case "guild_members_chunk": + await OnGuildMembersChunkEventAsync(dat); + break; + + #endregion + + #region Guild Role + + case "guild_role_create": + gid = (ulong)dat["guild_id"]; + await OnGuildRoleCreateEventAsync(dat["role"].ToDiscordObject(), this.guilds[gid]); + break; + + case "guild_role_update": + gid = (ulong)dat["guild_id"]; + await OnGuildRoleUpdateEventAsync(dat["role"].ToDiscordObject(), this.guilds[gid]); + break; + + case "guild_role_delete": + gid = (ulong)dat["guild_id"]; + await OnGuildRoleDeleteEventAsync((ulong)dat["role_id"], this.guilds[gid]); + break; + + #endregion + + #region Invite + + case "invite_create": + gid = (ulong)dat["guild_id"]; + cid = (ulong)dat["channel_id"]; + await OnInviteCreateEventAsync(cid, gid, dat.ToDiscordObject()); + break; + + case "invite_delete": + gid = (ulong)dat["guild_id"]; + cid = (ulong)dat["channel_id"]; + await OnInviteDeleteEventAsync(cid, gid, dat); + break; + + #endregion + + #region Message + + case "message_create": + rawMbr = dat["member"]; + + if (rawMbr != null) + { + mbr = rawMbr.ToDiscordObject(); + } + + if (rawRefMsg != null && rawRefMsg.HasValues) + { + if (rawRefMsg.SelectToken("author") != null) + { + refUsr = rawRefMsg.SelectToken("author").ToDiscordObject(); + } + + if (rawRefMsg.SelectToken("member") != null) + { + refMbr = rawRefMsg.SelectToken("member").ToDiscordObject(); + } + } + + TransportUser author = dat["author"].ToDiscordObject(); + dat.Remove("author"); + dat.Remove("member"); + + await OnMessageCreateEventAsync(dat.ToDiscordObject(), author, mbr, refUsr, refMbr); + break; + + case "message_update": + rawMbr = dat["member"]; + + if (rawMbr != null) + { + mbr = rawMbr.ToDiscordObject(); + } + + if (rawRefMsg != null && rawRefMsg.HasValues) + { + if (rawRefMsg.SelectToken("author") != null) + { + refUsr = rawRefMsg.SelectToken("author").ToDiscordObject(); + } + + if (rawRefMsg.SelectToken("member") != null) + { + refMbr = rawRefMsg.SelectToken("member").ToDiscordObject(); + } + } + + await OnMessageUpdateEventAsync(dat.ToDiscordObject(), dat["author"]?.ToDiscordObject(), mbr, refUsr, refMbr); + break; + + // delete event does *not* include message object + case "message_delete": + await OnMessageDeleteEventAsync((ulong)dat["id"], (ulong)dat["channel_id"], (ulong?)dat["guild_id"]); + break; + + case "message_delete_bulk": + await OnMessageBulkDeleteEventAsync(dat["ids"].ToDiscordObject(), (ulong)dat["channel_id"], (ulong?)dat["guild_id"]); + break; + + case "message_poll_vote_add": + await OnMessagePollVoteEventAsync(dat.ToDiscordObject(), true); + break; + + case "message_poll_vote_remove": + await OnMessagePollVoteEventAsync(dat.ToDiscordObject(), false); + break; + + #endregion + + #region Message Reaction + + case "message_reaction_add": + rawMbr = dat["member"]; + + if (rawMbr != null) + { + mbr = rawMbr.ToDiscordObject(); + } + + await OnMessageReactionAddAsync((ulong)dat["user_id"], (ulong)dat["message_id"], (ulong)dat["channel_id"], (ulong?)dat["guild_id"], mbr, dat["emoji"].ToDiscordObject()); + break; + + case "message_reaction_remove": + await OnMessageReactionRemoveAsync((ulong)dat["user_id"], (ulong)dat["message_id"], (ulong)dat["channel_id"], (ulong?)dat["guild_id"], dat["emoji"].ToDiscordObject()); + break; + + case "message_reaction_remove_all": + await OnMessageReactionRemoveAllAsync((ulong)dat["message_id"], (ulong)dat["channel_id"], (ulong?)dat["guild_id"]); + break; + + case "message_reaction_remove_emoji": + await OnMessageReactionRemoveEmojiAsync((ulong)dat["message_id"], (ulong)dat["channel_id"], (ulong)dat["guild_id"], dat["emoji"]); + break; + + #endregion + + #region User/Presence Update + + case "presence_update": + // Presences are a mess. I'm not touching this. ~Velvet + await OnPresenceUpdateEventAsync(dat, (JObject)dat["user"]); + break; + + case "user_settings_update": + await OnUserSettingsUpdateEventAsync(dat.ToDiscordObject()); + break; + + case "user_update": + await OnUserUpdateEventAsync(dat.ToDiscordObject()); + break; + + #endregion + + #region Voice + + case "voice_state_update": + await OnVoiceStateUpdateEventAsync(dat); + break; + + case "voice_server_update": + gid = (ulong)dat["guild_id"]; + await OnVoiceServerUpdateEventAsync((string)dat["endpoint"], (string)dat["token"], this.guilds[gid]); + break; + + #endregion + + #region Thread + + case "thread_create": + thread = dat.ToDiscordObject(); + await OnThreadCreateEventAsync(thread); + break; + + case "thread_update": + thread = dat.ToDiscordObject(); + await OnThreadUpdateEventAsync(thread); + break; + + case "thread_delete": + thread = dat.ToDiscordObject(); + await OnThreadDeleteEventAsync(thread); + break; + + case "thread_list_sync": + gid = (ulong)dat["guild_id"]; //get guild + await OnThreadListSyncEventAsync(this.guilds[gid], dat["channel_ids"].ToDiscordObject>(), dat["threads"].ToDiscordObject>(), dat["members"].ToDiscordObject>()); + break; + + case "thread_member_update": + gid = (ulong)dat["guild_id"]; + await OnThreadMemberUpdateEventAsync(this.guilds[gid], dat.ToDiscordObject()); + break; + + case "thread_members_update": + gid = (ulong)dat["guild_id"]; + await OnThreadMembersUpdateEventAsync(this.guilds[gid], (ulong)dat["id"], dat["added_members"]?.ToDiscordObject>(), dat["removed_member_ids"]?.ToDiscordObject>(), (int)dat["member_count"]); + break; + + #endregion + + #region Interaction/Integration/Application + + case "interaction_create": + + rawMbr = dat["member"]; + + if (rawMbr != null) + { + mbr = dat["member"].ToDiscordObject(); + usr = mbr.User; + } + else + { + usr = dat["user"].ToDiscordObject(); + } + + JToken? rawChannel = dat["channel"]; + DiscordChannel? channel = null; + if (rawChannel is not null) + { + channel = rawChannel.ToDiscordObject(); + channel.Discord = this; + } + + // Re: Removing re-serialized data: This one is probably fine? + // The user on the object is marked with [JsonIgnore]. + + cid = (ulong)dat["channel_id"]; + await OnInteractionCreateAsync((ulong?)dat["guild_id"], cid, usr, mbr, channel, dat.ToDiscordObject()); + break; + + case "integration_create": + await OnIntegrationCreateAsync(dat.ToDiscordObject(), (ulong)dat["guild_id"]); + break; + + case "integration_update": + await OnIntegrationUpdateAsync(dat.ToDiscordObject(), (ulong)dat["guild_id"]); + break; + + case "integration_delete": + await OnIntegrationDeleteAsync((ulong)dat["id"], (ulong)dat["guild_id"], (ulong?)dat["application_id"]); + break; + + case "application_command_permissions_update": + await OnApplicationCommandPermissionsUpdateAsync(dat); + break; + #endregion + + #region Stage Instance + + case "stage_instance_create": + await OnStageInstanceCreateAsync(dat.ToDiscordObject()); + break; + + case "stage_instance_update": + await OnStageInstanceUpdateAsync(dat.ToDiscordObject()); + break; + + case "stage_instance_delete": + await OnStageInstanceDeleteAsync(dat.ToDiscordObject()); + break; + + #endregion + + #region Misc + + case "gift_code_update": //Not supposed to be dispatched to bots + break; + + case "embedded_activity_update": //Not supposed to be dispatched to bots + break; + + case "typing_start": + cid = (ulong)dat["channel_id"]; + rawMbr = dat["member"]; + + if (rawMbr != null) + { + mbr = rawMbr.ToDiscordObject(); + } + + ulong? guildId = (ulong?)dat["guild_id"]; + await OnTypingStartEventAsync((ulong)dat["user_id"], cid, InternalGetCachedChannel(cid, guildId)!, guildId, Utilities.GetDateTimeOffset((long)dat["timestamp"]), mbr); + break; + + case "webhooks_update": + gid = (ulong)dat["guild_id"]; + cid = (ulong)dat["channel_id"]; + await OnWebhooksUpdateAsync(this.guilds[gid].GetChannel(cid), this.guilds[gid]); + break; + + case "guild_stickers_update": + IEnumerable strs = dat["stickers"].ToDiscordObject>(); + await OnStickersUpdatedAsync(strs, dat); + break; + + default: + await OnUnknownEventAsync(payload); + if (this.Configuration.LogUnknownEvents) + { + this.Logger.LogWarning(LoggerEvents.WebSocketReceive, "Unknown event: {EventName}\npayload: {@Payload}", payload.EventName, payload.Data); + } + + break; + + #endregion + + #region AutoModeration + case "auto_moderation_rule_create": + await OnAutoModerationRuleCreateAsync(dat.ToDiscordObject()); + break; + + case "auto_moderation_rule_update": + await OnAutoModerationRuleUpdatedAsync(dat.ToDiscordObject()); + break; + + case "auto_moderation_rule_delete": + await OnAutoModerationRuleDeletedAsync(dat.ToDiscordObject()); + break; + + case "auto_moderation_action_execution": + await OnAutoModerationRuleExecutedAsync(dat.ToDiscordObject()); + break; + #endregion + + #region Entitlements + case "entitlement_create": + await OnEntitlementCreatedAsync(dat.ToDiscordObject()); + break; + + case "entitlement_update": + await OnEntitlementUpdatedAsync(dat.ToDiscordObject()); + break; + + case "entitlement_delete": + await OnEntitlementDeletedAsync(dat.ToDiscordObject()); + break; + #endregion + } + } + + #endregion + + #region Webhook Events + + private async Task ReceiveWebhookEventsAsync() + { + while (!this.webhookEventReader.Completion.IsCompleted) + { + DiscordWebhookEvent payload = await this.webhookEventReader.ReadAsync(); + + try + { + await HandleWebhookDispatchAsync(payload); + } + catch (Exception ex) + { + this.Logger.LogError(ex, "Dispatch threw an exception: "); + } + } + } + + private async Task ReceiveInteractionEventsAsync() + { + while (!this.interactionEventReader.Completion.IsCompleted) + { + DiscordHttpInteractionPayload payload = await this.interactionEventReader.ReadAsync(); + DiscordHttpInteraction interaction = payload.ProtoInteraction; + interaction.Discord = this; + + ulong? guildId = interaction.GuildId; + ulong channelId = interaction.ChannelId; + + JToken rawMember = payload.Data["member"]; + TransportMember? transportMember = null; + TransportUser transportUser; + if (rawMember != null) + { + transportMember = payload.Data["member"].ToDiscordObject(); + transportUser = transportMember.User; + } + else + { + transportUser = payload.Data["user"].ToDiscordObject(); + } + + DiscordChannel channel = interaction.Channel; + channel.Discord = this; + + await OnInteractionCreateAsync(guildId, channelId, transportUser, transportMember, channel, interaction); + } + } + + private Task HandleWebhookDispatchAsync(DiscordWebhookEvent @event) + { + if (@event.ApplicationID != this.CurrentApplication.Id) + { + this.Logger.LogCritical + ( + "The application event webhook received an event for application {OtherId}, which is different from the current application.", + @event.ApplicationID + ); + + return Task.CompletedTask; + } + + if (@event.Type == DiscordWebhookEventType.Ping) + { + return Task.CompletedTask; + } + + DiscordWebhookEventBody body = @event.Event; + + _ = body.Type switch + { + DiscordWebhookEventBodyType.ApplicationAuthorized => OnApplicationAuthorizedAsync(body), + DiscordWebhookEventBodyType.EntitlementCreate => OnWebhookEntitlementCreateAsync(body), + _ => OnUnknownWebhookEventAsync(body) + }; + + return Task.CompletedTask; + } + + #endregion + + #region Events + + #region Gateway + + internal async Task OnReadyEventAsync(ReadyPayload ready, JArray rawGuilds, JArray rawDmChannels, int shardId) + { + TransportUser rusr = ready.CurrentUser; + this.CurrentUser = new DiscordUser(rusr) + { + Discord = this + }; + + this.sessionId = ready.SessionId; + this.gatewayResumeUrl = ready.ResumeGatewayUrl; + Dictionary rawGuildIndex = rawGuilds.ToDictionary(xt => (ulong)xt["id"], xt => (JObject)xt); + + this.privateChannels.Clear(); + foreach (JToken rawChannel in rawDmChannels) + { + DiscordDmChannel channel = rawChannel.ToDiscordObject(); + + channel.Discord = this; + + //xdc.recipients = + // .Select(xtu => this.InternalGetCachedUser(xtu.Id) ?? new DiscordUser(xtu) { Discord = this }) + // .ToList(); + + IEnumerable recipsRaw = rawChannel["recipients"].ToDiscordObject>(); + List recipients = []; + foreach (TransportUser xr in recipsRaw) + { + DiscordUser xu = new(xr) { Discord = this }; + xu = UpdateUserCache(xu); + + recipients.Add(xu); + } + + channel.Recipients = recipients; + + this.privateChannels[channel.Id] = channel; + } + + List guilds = rawGuilds.ToDiscordObject>().ToList(); + foreach (DiscordGuild guild in guilds) + { + guild.Discord = this; + guild.channels ??= new ConcurrentDictionary(); + guild.threads ??= new ConcurrentDictionary(); + + foreach (DiscordChannel xc in guild.Channels.Values) + { + xc.GuildId = guild.Id; + xc.Discord = this; + foreach (DiscordOverwrite xo in xc.permissionOverwrites) + { + xo.Discord = this; + xo.channelId = xc.Id; + } + } + + foreach (DiscordThreadChannel xt in guild.Threads.Values) + { + xt.GuildId = guild.Id; + xt.Discord = this; + } + + guild.roles ??= new ConcurrentDictionary(); + + foreach (DiscordRole xr in guild.Roles.Values) + { + xr.Discord = this; + xr.guild_id = guild.Id; + } + + JObject rawGuild = rawGuildIndex[guild.Id]; + JArray? rawMembers = (JArray)rawGuild["members"]; + + guild.members?.Clear(); + guild.members ??= new ConcurrentDictionary(); + + if (rawMembers != null) + { + foreach (JToken xj in rawMembers) + { + TransportMember xtm = xj.ToDiscordObject(); + + DiscordUser xu = new(xtm.User) { Discord = this }; + xu = UpdateUserCache(xu); + + guild.members[xtm.User.Id] = new DiscordMember(xtm) { Discord = this, guild_id = guild.Id }; + } + } + + guild.emojis ??= new ConcurrentDictionary(); + + foreach (DiscordEmoji xe in guild.Emojis.Values) + { + xe.Discord = this; + } + + guild.voiceStates ??= new ConcurrentDictionary(); + + foreach (DiscordVoiceState xvs in guild.VoiceStates.Values) + { + xvs.Discord = this; + } + + this.guilds[guild.Id] = guild; + } + + await this.dispatcher.DispatchAsync + ( + this, + new() + { + ShardId = shardId, + GuildIds = [.. guilds.Select(guild => guild.Id)] + } + ); + + if (!guilds.Any() && this.orchestrator.AllShardsConnected) + { + this.guildDownloadCompleted = true; + GuildDownloadCompletedEventArgs ea = new(this.Guilds); + + await this.dispatcher.DispatchAsync(this, ea); + } + } + + internal async Task OnResumedAsync(int shardId) + { + await this.dispatcher.DispatchAsync + ( + this, + new() + { + ShardId = shardId + } + ); + } + + #endregion + + #region Channel + + internal async Task OnChannelCreateEventAsync(DiscordChannel channel) + { + channel.Discord = this; + foreach (DiscordOverwrite xo in channel.permissionOverwrites) + { + xo.Discord = this; + xo.channelId = channel.Id; + } + + this.guilds[channel.GuildId.Value].channels[channel.Id] = channel; + + await this.dispatcher.DispatchAsync(this, new ChannelCreatedEventArgs + { + Channel = channel, + Guild = channel.Guild + }); + } + + internal async Task OnChannelUpdateEventAsync(DiscordChannel channel) + { + if (channel == null) + { + return; + } + + channel.Discord = this; + + DiscordGuild? gld = channel.Guild; + + DiscordChannel? channel_new = InternalGetCachedChannel(channel.Id, channel.GuildId); + DiscordChannel channel_old = null!; + + if (channel_new != null) + { + channel_old = new DiscordChannel + { + Bitrate = channel_new.Bitrate, + Discord = this, + GuildId = channel_new.GuildId, + Id = channel_new.Id, + //IsPrivate = channel_new.IsPrivate, + LastMessageId = channel_new.LastMessageId, + Name = channel_new.Name, + permissionOverwrites = new List(channel_new.permissionOverwrites), + Position = channel_new.Position, + Topic = channel_new.Topic, + Type = channel_new.Type, + UserLimit = channel_new.UserLimit, + ParentId = channel_new.ParentId, + IsNSFW = channel_new.IsNSFW, + PerUserRateLimit = channel_new.PerUserRateLimit, + RtcRegionId = channel_new.RtcRegionId, + QualityMode = channel_new.QualityMode + }; + + channel_new.Bitrate = channel.Bitrate; + channel_new.Name = channel.Name; + channel_new.Position = channel.Position; + channel_new.Topic = channel.Topic; + channel_new.UserLimit = channel.UserLimit; + channel_new.ParentId = channel.ParentId; + channel_new.IsNSFW = channel.IsNSFW; + channel_new.PerUserRateLimit = channel.PerUserRateLimit; + channel_new.Type = channel.Type; + channel_new.RtcRegionId = channel.RtcRegionId; + channel_new.QualityMode = channel.QualityMode; + + channel_new.permissionOverwrites.Clear(); + + foreach (DiscordOverwrite po in channel.permissionOverwrites) + { + po.Discord = this; + po.channelId = channel.Id; + } + + channel_new.permissionOverwrites.AddRange(channel.permissionOverwrites); + } + else if (gld != null) + { + gld.channels[channel.Id] = channel; + } + + await this.dispatcher.DispatchAsync(this, new ChannelUpdatedEventArgs + { + ChannelAfter = channel_new, + Guild = gld, + ChannelBefore = channel_old + }); + } + + internal async Task OnChannelDeleteEventAsync(DiscordChannel channel) + { + if (channel == null) + { + return; + } + + channel.Discord = this; + + //if (channel.IsPrivate) + if (channel.Type is DiscordChannelType.Group or DiscordChannelType.Private) + { + DiscordDmChannel? dmChannel = channel as DiscordDmChannel; + + _ = this.privateChannels.TryRemove(dmChannel.Id, out _); + + await this.dispatcher.DispatchAsync(this, new DmChannelDeletedEventArgs + { + Channel = dmChannel + }); + } + else + { + DiscordGuild gld = channel.Guild; + + if (gld.channels.TryRemove(channel.Id, out DiscordChannel? cachedChannel)) + { + channel = cachedChannel; + } + + await this.dispatcher.DispatchAsync(this, new ChannelDeletedEventArgs + { + Channel = channel, + Guild = gld + }); + } + } + + internal async Task OnChannelPinsUpdateAsync(ulong? guildId, ulong channelId, DateTimeOffset? lastPinTimestamp) + { + DiscordGuild guild = InternalGetCachedGuild(guildId); + DiscordChannel? channel = InternalGetCachedChannel(channelId, guildId) ?? InternalGetCachedThread(channelId, guildId); + + if (channel == null) + { + channel = new DiscordDmChannel + { + Id = channelId, + Discord = this, + Type = DiscordChannelType.Private, + Recipients = Array.Empty() + }; + + DiscordDmChannel chn = (DiscordDmChannel)channel; + + this.privateChannels[channelId] = chn; + } + + ChannelPinsUpdatedEventArgs ea = new() + { + Guild = guild, + Channel = channel, + LastPinTimestamp = lastPinTimestamp + }; + + await this.dispatcher.DispatchAsync(this, ea); + } + + #endregion + + #region Scheduled Guild Events + + private async Task OnScheduledGuildEventCreateEventAsync(DiscordScheduledGuildEvent evt) + { + evt.Discord = this; + + if (evt.Creator != null) + { + evt.Creator.Discord = this; + UpdateUserCache(evt.Creator); + } + + evt.Guild.scheduledEvents[evt.Id] = evt; + + await this.dispatcher.DispatchAsync(this, new ScheduledGuildEventCreatedEventArgs + { + Event = evt + }); + } + + private async Task OnScheduledGuildEventDeleteEventAsync(DiscordScheduledGuildEvent evt) + { + DiscordGuild guild = InternalGetCachedGuild(evt.GuildId); + + if (guild == null) // ??? // + { + return; + } + + guild.scheduledEvents.TryRemove(evt.Id, out _); + + evt.Discord = this; + + if (evt.Creator != null) + { + evt.Creator.Discord = this; + UpdateUserCache(evt.Creator); + } + + await this.dispatcher.DispatchAsync(this, new ScheduledGuildEventDeletedEventArgs + { + Event = evt + }); + } + + private async Task OnScheduledGuildEventUpdateEventAsync(DiscordScheduledGuildEvent evt) + { + evt.Discord = this; + + if (evt.Creator != null) + { + evt.Creator.Discord = this; + UpdateUserCache(evt.Creator); + } + + DiscordGuild guild = InternalGetCachedGuild(evt.GuildId); + guild.scheduledEvents.TryGetValue(evt.GuildId, out DiscordScheduledGuildEvent? oldEvt); + + evt.Guild.scheduledEvents[evt.Id] = evt; + + if (evt.Status is DiscordScheduledGuildEventStatus.Completed) + { + await this.dispatcher.DispatchAsync(this, new ScheduledGuildEventCompletedEventArgs() + { + Event = evt + }); + } + else + { + await this.dispatcher.DispatchAsync(this, new ScheduledGuildEventUpdatedEventArgs() + { + EventBefore = oldEvt, + EventAfter = evt + }); + } + } + + private async Task OnScheduledGuildEventUserAddEventAsync(ulong guildId, ulong eventId, ulong userId) + { + DiscordGuild guild = InternalGetCachedGuild(guildId); + DiscordScheduledGuildEvent evt = guild.scheduledEvents.GetOrAdd(eventId, new DiscordScheduledGuildEvent() + { + Id = eventId, + GuildId = guildId, + Discord = this, + UserCount = 0 + }); + + evt.UserCount++; + + DiscordUser user = + guild.Members.TryGetValue(userId, out DiscordMember? mbr) ? mbr : + GetCachedOrEmptyUserInternal(userId) ?? new DiscordUser() { Id = userId, Discord = this }; + + await this.dispatcher.DispatchAsync(this, new ScheduledGuildEventUserAddedEventArgs() + { + Event = evt, + User = user + }); + } + + private async Task OnScheduledGuildEventUserRemoveEventAsync(ulong guildId, ulong eventId, ulong userId) + { + DiscordGuild guild = InternalGetCachedGuild(guildId); + DiscordScheduledGuildEvent evt = guild.scheduledEvents.GetOrAdd(eventId, new DiscordScheduledGuildEvent() + { + Id = eventId, + GuildId = guildId, + Discord = this, + UserCount = 0 + }); + + evt.UserCount = evt.UserCount is 0 ? 0 : evt.UserCount - 1; + + DiscordUser user = + guild.Members.TryGetValue(userId, out DiscordMember? mbr) ? mbr : + GetCachedOrEmptyUserInternal(userId) ?? new DiscordUser() { Id = userId, Discord = this }; + + await this.dispatcher.DispatchAsync(this, new ScheduledGuildEventUserRemovedEventArgs() + { + Event = evt, + User = user + }); + } + + #endregion + + #region Guild + + internal async Task OnGuildCreateEventAsync(DiscordGuild guild, JArray rawMembers, IEnumerable presences) + { + if (presences != null) + { + foreach (DiscordPresence xp in presences) + { + xp.Discord = this; + xp.GuildId = guild.Id; + xp.Activity = new DiscordActivity(xp.RawActivity); + + if (xp.RawActivities != null) + { + xp.internalActivities = new DiscordActivity[xp.RawActivities.Length]; + for (int i = 0; i < xp.RawActivities.Length; i++) + { + xp.internalActivities[i] = new DiscordActivity(xp.RawActivities[i]); + } + } + + this.presences[xp.User.Id] = xp; + } + } + + bool exists = this.guilds.TryGetValue(guild.Id, out DiscordGuild? foundGuild); + + guild.Discord = this; + guild.IsUnavailable = false; + DiscordGuild eventGuild = guild; + + if (exists) + { + guild = foundGuild; + } + + guild.channels ??= new ConcurrentDictionary(); + guild.threads ??= new ConcurrentDictionary(); + guild.roles ??= new ConcurrentDictionary(); + guild.emojis ??= new ConcurrentDictionary(); + guild.stickers ??= new ConcurrentDictionary(); + guild.voiceStates ??= new ConcurrentDictionary(); + guild.members ??= new ConcurrentDictionary(); + guild.stageInstances ??= new ConcurrentDictionary(); + guild.scheduledEvents ??= new ConcurrentDictionary(); + + UpdateCachedGuild(eventGuild, rawMembers); + + guild.JoinedAt = eventGuild.JoinedAt; + guild.IsLarge = eventGuild.IsLarge; + guild.MemberCount = Math.Max(eventGuild.MemberCount, guild.members.Count); + guild.IsUnavailable = eventGuild.IsUnavailable; + guild.PremiumSubscriptionCount = eventGuild.PremiumSubscriptionCount; + guild.PremiumTier = eventGuild.PremiumTier; + guild.Banner = eventGuild.Banner; + guild.VanityUrlCode = eventGuild.VanityUrlCode; + guild.Description = eventGuild.Description; + guild.IsNSFW = eventGuild.IsNSFW; + + foreach (KeyValuePair kvp in eventGuild.voiceStates ??= new()) + { + guild.voiceStates[kvp.Key] = kvp.Value; + } + + foreach (DiscordScheduledGuildEvent xe in guild.scheduledEvents.Values) + { + xe.Discord = this; + + if (xe.Creator != null) + { + xe.Creator.Discord = this; + } + } + + foreach (DiscordChannel xc in guild.channels.Values) + { + xc.GuildId = guild.Id; + xc.Discord = this; + foreach (DiscordOverwrite xo in xc.permissionOverwrites) + { + xo.Discord = this; + xo.channelId = xc.Id; + } + } + + foreach (DiscordThreadChannel xt in guild.threads.Values) + { + xt.GuildId = guild.Id; + xt.Discord = this; + } + + foreach (DiscordEmoji xe in guild.emojis.Values) + { + xe.Discord = this; + } + + foreach (DiscordMessageSticker xs in guild.stickers.Values) + { + xs.Discord = this; + } + + foreach (DiscordVoiceState xvs in guild.voiceStates.Values) + { + xvs.Discord = this; + } + + foreach (DiscordRole xr in guild.roles.Values) + { + xr.Discord = this; + xr.guild_id = guild.Id; + } + + foreach (DiscordStageInstance instance in guild.stageInstances.Values) + { + instance.Discord = this; + } + + bool old = Volatile.Read(ref this.guildDownloadCompleted); + bool dcompl = this.guilds.Values.All(xg => !xg.IsUnavailable) && !this.guildDownloadCompleted; + + if (exists) + { + await this.dispatcher.DispatchAsync(this, new GuildAvailableEventArgs + { + Guild = guild + }); + } + else + { + await this.dispatcher.DispatchAsync(this, new GuildCreatedEventArgs + { + Guild = guild + }); + } + + if (dcompl && !old && this.orchestrator.AllShardsConnected) + { + this.guildDownloadCompleted = true; + await this.dispatcher.DispatchAsync(this, new GuildDownloadCompletedEventArgs(this.Guilds)); + } + } + + internal async Task OnGuildUpdateEventAsync(DiscordGuild guild, JArray rawMembers) + { + DiscordGuild oldGuild; + + if (!this.guilds.TryGetValue(guild.Id, out DiscordGuild gld)) + { + this.guilds[guild.Id] = guild; + oldGuild = null; + } + else + { + oldGuild = new DiscordGuild + { + Discord = gld.Discord, + Name = gld.Name, + AfkChannelId = gld.AfkChannelId, + AfkTimeout = gld.AfkTimeout, + DefaultMessageNotifications = gld.DefaultMessageNotifications, + ExplicitContentFilter = gld.ExplicitContentFilter, + Features = gld.Features, + IconHash = gld.IconHash, + Id = gld.Id, + IsLarge = gld.IsLarge, + isSynced = gld.isSynced, + IsUnavailable = gld.IsUnavailable, + JoinedAt = gld.JoinedAt, + MemberCount = gld.MemberCount, + MaxMembers = gld.MaxMembers, + MaxPresences = gld.MaxPresences, + ApproximateMemberCount = gld.ApproximateMemberCount, + ApproximatePresenceCount = gld.ApproximatePresenceCount, + MaxVideoChannelUsers = gld.MaxVideoChannelUsers, + DiscoverySplashHash = gld.DiscoverySplashHash, + PreferredLocale = gld.PreferredLocale, + MfaLevel = gld.MfaLevel, + OwnerId = gld.OwnerId, + SplashHash = gld.SplashHash, + SystemChannelId = gld.SystemChannelId, + SystemChannelFlags = gld.SystemChannelFlags, + WidgetEnabled = gld.WidgetEnabled, + WidgetChannelId = gld.WidgetChannelId, + VerificationLevel = gld.VerificationLevel, + RulesChannelId = gld.RulesChannelId, + PublicUpdatesChannelId = gld.PublicUpdatesChannelId, + voiceRegionId = gld.voiceRegionId, + PremiumProgressBarEnabled = gld.PremiumProgressBarEnabled, + IsNSFW = gld.IsNSFW, + channels = new ConcurrentDictionary(), + threads = new ConcurrentDictionary(), + emojis = new ConcurrentDictionary(), + members = new ConcurrentDictionary(), + roles = new ConcurrentDictionary(), + voiceStates = new ConcurrentDictionary() + }; + + foreach (KeyValuePair kvp in gld.channels ??= new()) + { + oldGuild.channels[kvp.Key] = kvp.Value; + } + + foreach (KeyValuePair kvp in gld.threads ??= new()) + { + oldGuild.threads[kvp.Key] = kvp.Value; + } + + foreach (KeyValuePair kvp in gld.emojis ??= new()) + { + oldGuild.emojis[kvp.Key] = kvp.Value; + } + + foreach (KeyValuePair kvp in gld.roles ??= new()) + { + oldGuild.roles[kvp.Key] = kvp.Value; + } + //new ConcurrentDictionary() + foreach (KeyValuePair kvp in gld.voiceStates ??= new()) + { + oldGuild.voiceStates[kvp.Key] = kvp.Value; + } + + foreach (KeyValuePair kvp in gld.members ??= new()) + { + oldGuild.members[kvp.Key] = kvp.Value; + } + } + + guild.Discord = this; + guild.IsUnavailable = false; + DiscordGuild eventGuild = guild; + guild = this.guilds[eventGuild.Id]; + guild.channels ??= new ConcurrentDictionary(); + guild.threads ??= new ConcurrentDictionary(); + guild.roles ??= new ConcurrentDictionary(); + guild.emojis ??= new ConcurrentDictionary(); + guild.voiceStates ??= new ConcurrentDictionary(); + guild.members ??= new ConcurrentDictionary(); + UpdateCachedGuild(eventGuild, rawMembers); + + foreach (DiscordChannel xc in guild.channels.Values) + { + xc.GuildId = guild.Id; + xc.Discord = this; + foreach (DiscordOverwrite xo in xc.permissionOverwrites) + { + xo.Discord = this; + xo.channelId = xc.Id; + } + } + + foreach (DiscordThreadChannel xc in guild.threads.Values) + { + xc.GuildId = guild.Id; + xc.Discord = this; + } + + foreach (DiscordEmoji xe in guild.emojis.Values) + { + xe.Discord = this; + } + + foreach (DiscordVoiceState xvs in guild.voiceStates.Values) + { + xvs.Discord = this; + } + + foreach (DiscordRole xr in guild.roles.Values) + { + xr.Discord = this; + xr.guild_id = guild.Id; + } + + await this.dispatcher.DispatchAsync(this, new GuildUpdatedEventArgs + { + GuildBefore = oldGuild, + GuildAfter = guild + }); + } + + internal async Task OnGuildDeleteEventAsync(DiscordGuild guild) + { + if (guild.IsUnavailable) + { + if (!this.guilds.TryGetValue(guild.Id, out DiscordGuild? gld)) + { + return; + } + + gld.IsUnavailable = true; + + await this.dispatcher.DispatchAsync(this, new GuildUnavailableEventArgs + { + Guild = guild, + Unavailable = true + }); + } + else + { + if (!this.guilds.TryRemove(guild.Id, out DiscordGuild? gld)) + { + return; + } + + await this.dispatcher.DispatchAsync(this, new GuildDeletedEventArgs + { + Guild = gld + }); + } + } + + internal async Task OnGuildEmojisUpdateEventAsync(DiscordGuild guild, IEnumerable newEmojis) + { + ConcurrentDictionary oldEmojis = new(guild.emojis); + guild.emojis.Clear(); + + foreach (DiscordEmoji emoji in newEmojis) + { + emoji.Discord = this; + guild.emojis[emoji.Id] = emoji; + } + + GuildEmojisUpdatedEventArgs ea = new() + { + Guild = guild, + EmojisAfter = guild.Emojis, + EmojisBefore = oldEmojis + }; + + await this.dispatcher.DispatchAsync(this, ea); + } + + internal async Task OnGuildIntegrationsUpdateEventAsync(DiscordGuild guild) + { + GuildIntegrationsUpdatedEventArgs ea = new() + { + Guild = guild + }; + + await this.dispatcher.DispatchAsync(this, ea); + } + + private async Task OnGuildAuditLogEntryCreateEventAsync(DiscordGuild guild, DiscordAuditLogEntry auditLogEntry) + { + GuildAuditLogCreatedEventArgs ea = new() + { + Guild = guild, + AuditLogEntry = auditLogEntry + }; + + await this.dispatcher.DispatchAsync(this, ea); + } + + #endregion + + #region Guild Ban + + internal async Task OnGuildBanAddEventAsync(TransportUser user, DiscordGuild guild) + { + DiscordUser usr = new(user) { Discord = this }; + usr = UpdateUserCache(usr); + + if (!guild.Members.TryGetValue(user.Id, out DiscordMember? mbr)) + { + mbr = new DiscordMember(usr) { Discord = this, guild_id = guild.Id }; + } + + GuildBanAddedEventArgs ea = new() + { + Guild = guild, + Member = mbr + }; + + await this.dispatcher.DispatchAsync(this, ea); + } + + internal async Task OnGuildBanRemoveEventAsync(TransportUser user, DiscordGuild guild) + { + DiscordUser usr = new(user) { Discord = this }; + usr = UpdateUserCache(usr); + + if (!guild.Members.TryGetValue(user.Id, out DiscordMember? mbr)) + { + mbr = new DiscordMember(usr) { Discord = this, guild_id = guild.Id }; + } + + GuildBanRemovedEventArgs ea = new() + { + Guild = guild, + Member = mbr + }; + + await this.dispatcher.DispatchAsync(this, ea); + } + + #endregion + + #region Guild Member + + internal async Task OnGuildMemberAddEventAsync(TransportMember member, DiscordGuild guild) + { + DiscordUser usr = new(member.User) { Discord = this }; + UpdateUserCache(usr); + + DiscordMember mbr = new(member) + { + Discord = this, + guild_id = guild.Id + }; + + guild.members[mbr.Id] = mbr; + guild.MemberCount++; + + GuildMemberAddedEventArgs ea = new() + { + Guild = guild, + Member = mbr + }; + + await this.dispatcher.DispatchAsync(this, ea); + } + + internal async Task OnGuildMemberRemoveEventAsync(TransportUser user, DiscordGuild guild) + { + DiscordUser usr = new(user); + + if (!guild.members.TryRemove(user.Id, out DiscordMember? mbr)) + { + mbr = new DiscordMember(usr) { Discord = this, guild_id = guild.Id }; + } + + guild.MemberCount--; + + UpdateUserCache(usr); + + GuildMemberRemovedEventArgs ea = new() + { + Guild = guild, + Member = mbr + }; + await this.dispatcher.DispatchAsync(this, ea); + } + + internal async Task OnGuildMemberUpdateEventAsync(TransportMember member, DiscordGuild guild) + { + DiscordUser userAfter = new(member.User) { Discord = this }; + _ = UpdateUserCache(userAfter); + + DiscordMember memberAfter = new(member) { Discord = this, guild_id = guild.Id }; + + if (!guild.Members.TryGetValue(member.User.Id, out DiscordMember? memberBefore)) + { + memberBefore = new DiscordMember(member) { Discord = this, guild_id = guild.Id }; + } + + guild.members.AddOrUpdate(member.User.Id, memberAfter, (_, _) => memberAfter); + + GuildMemberUpdatedEventArgs ea = new() + { + Guild = guild, + MemberAfter = memberAfter, + MemberBefore = memberBefore, + }; + + await this.dispatcher.DispatchAsync(this, ea); + } + + internal async Task OnGuildMembersChunkEventAsync(JObject dat) + { + DiscordGuild guild = this.Guilds[(ulong)dat["guild_id"]]; + int chunkIndex = (int)dat["chunk_index"]; + int chunkCount = (int)dat["chunk_count"]; + string? nonce = (string)dat["nonce"]; + + HashSet mbrs = []; + HashSet pres = []; + + TransportMember[] members = dat["members"].ToDiscordObject(); + + int memCount = members.Length; + for (int i = 0; i < memCount; i++) + { + DiscordMember mbr = new(members[i]) { Discord = this, guild_id = guild.Id }; + + if (!this.UserCache.ContainsKey(mbr.Id)) + { + this.UserCache[mbr.Id] = new DiscordUser(members[i].User) { Discord = this }; + } + + guild.members[mbr.Id] = mbr; + + mbrs.Add(mbr); + } + + guild.MemberCount = guild.members.Count; + + GuildMembersChunkedEventArgs ea = new() + { + Guild = guild, + Members = new ReadOnlySet(mbrs), + ChunkIndex = chunkIndex, + ChunkCount = chunkCount, + Nonce = nonce, + }; + + if (dat["presences"] != null) + { + DiscordPresence[] presences = dat["presences"].ToDiscordObject(); + + int presCount = presences.Length; + for (int i = 0; i < presCount; i++) + { + DiscordPresence xp = presences[i]; + xp.Discord = this; + xp.Activity = new DiscordActivity(xp.RawActivity); + + if (xp.RawActivities != null) + { + xp.internalActivities = new DiscordActivity[xp.RawActivities.Length]; + for (int j = 0; j < xp.RawActivities.Length; j++) + { + xp.internalActivities[j] = new DiscordActivity(xp.RawActivities[j]); + } + } + + pres.Add(xp); + } + + ea.Presences = new ReadOnlySet(pres); + } + + if (dat["not_found"] != null) + { + ISet nf = dat["not_found"].ToDiscordObject>(); + ea.NotFound = new ReadOnlySet(nf); + } + + _ = DispatchGuildMembersChunkForIteratorsAsync(ea); + + await this.dispatcher.DispatchAsync(this, ea); + } + + #endregion + + #region Guild Role + + internal async Task OnGuildRoleCreateEventAsync(DiscordRole role, DiscordGuild guild) + { + role.Discord = this; + role.guild_id = guild.Id; + + guild.roles[role.Id] = role; + + GuildRoleCreatedEventArgs ea = new() + { + Guild = guild, + Role = role + }; + + await this.dispatcher.DispatchAsync(this, ea); + } + + internal async Task OnGuildRoleUpdateEventAsync(DiscordRole role, DiscordGuild guild) + { + DiscordRole newRole = await guild.GetRoleAsync(role.Id); + DiscordRole oldRole = new() + { + guild_id = guild.Id, + color = newRole.color, + Discord = this, + IsHoisted = newRole.IsHoisted, + Id = newRole.Id, + IsManaged = newRole.IsManaged, + IsMentionable = newRole.IsMentionable, + Name = newRole.Name, + Permissions = newRole.Permissions, + Position = newRole.Position, + IconHash = newRole.IconHash, + emoji = newRole.emoji + }; + + newRole.guild_id = guild.Id; + newRole.color = role.color; + newRole.IsHoisted = role.IsHoisted; + newRole.IsManaged = role.IsManaged; + newRole.IsMentionable = role.IsMentionable; + newRole.Name = role.Name; + newRole.Permissions = role.Permissions; + newRole.Position = role.Position; + newRole.emoji = role.emoji; + newRole.IconHash = role.IconHash; + + GuildRoleUpdatedEventArgs ea = new() + { + Guild = guild, + RoleAfter = newRole, + RoleBefore = oldRole + }; + + await this.dispatcher.DispatchAsync(this, ea); + } + + internal async Task OnGuildRoleDeleteEventAsync(ulong roleId, DiscordGuild guild) + { + if (!guild.roles.TryRemove(roleId, out DiscordRole? role)) + { + this.Logger.LogWarning("Attempted to delete a nonexistent role ({RoleId}) from guild ({Guild}).", roleId, guild); + } + + GuildRoleDeletedEventArgs ea = new() + { + Guild = guild, + Role = role + }; + + await this.dispatcher.DispatchAsync(this, ea); + } + + #endregion + + #region Invite + + internal async Task OnInviteCreateEventAsync(ulong channelId, ulong guildId, DiscordInvite invite) + { + DiscordGuild guild = InternalGetCachedGuild(guildId); + DiscordChannel channel = InternalGetCachedChannel(channelId, guildId); + + invite.Discord = this; + + guild.invites[invite.Code] = invite; + + InviteCreatedEventArgs ea = new() + { + Channel = channel, + Guild = guild, + Invite = invite + }; + + await this.dispatcher.DispatchAsync(this, ea); + } + + internal async Task OnInviteDeleteEventAsync(ulong channelId, ulong guildId, JToken dat) + { + DiscordGuild guild = InternalGetCachedGuild(guildId); + DiscordChannel channel = InternalGetCachedChannel(channelId, guildId); + + if (!guild.invites.TryRemove(dat["code"].ToString(), out DiscordInvite? invite)) + { + invite = dat.ToDiscordObject(); + invite.Discord = this; + } + + invite.IsRevoked = true; + + InviteDeletedEventArgs ea = new() + { + Channel = channel, + Guild = guild, + Invite = invite + }; + + await this.dispatcher.DispatchAsync(this, ea); + } + + #endregion + + #region Message + + internal async Task OnMessageCreateEventAsync(DiscordMessage message, TransportUser author, TransportMember member, TransportUser referenceAuthor, TransportMember referenceMember) + { + message.Discord = this; + PopulateMessageReactionsAndCache(message, author, member); + message.PopulateMentions(); + + if (message.ReferencedMessage != null) + { + message.ReferencedMessage.Discord = this; + PopulateMessageReactionsAndCache(message.ReferencedMessage, referenceAuthor, referenceMember); + message.ReferencedMessage.PopulateMentions(); + } + + if (message.MessageSnapshots != null) + { + foreach (DiscordMessageSnapshot snapshot in message.MessageSnapshots) + { + if (snapshot?.Message != null) + { + snapshot.Message.PopulateMentions(); + } + } + } + + foreach (DiscordMessageSticker sticker in message.Stickers) + { + sticker.Discord = this; + } + + MessageCreatedEventArgs ea = new() + { + Message = message, + + MentionedUsers = new ReadOnlyCollection(message.mentionedUsers), + MentionedRoles = message.mentionedRoles != null ? new ReadOnlyCollection(message.mentionedRoles) : null, + MentionedChannels = message.mentionedChannels != null ? new ReadOnlyCollection(message.mentionedChannels) : null + }; + + await this.dispatcher.DispatchAsync(this, ea); + } + + internal async Task OnMessageUpdateEventAsync(DiscordMessage message, TransportUser author, TransportMember member, TransportUser referenceAuthor, TransportMember referenceMember) + { + message.Discord = this; + DiscordMessage event_message = message; + + DiscordMessage oldmsg = null; + + if (!this.MessageCache.TryGet(event_message.Id, out message)) // previous message was not in cache + { + message = event_message; + PopulateMessageReactionsAndCache(message, author, member); + + if (message.ReferencedMessage != null) + { + message.ReferencedMessage.Discord = this; + PopulateMessageReactionsAndCache(message.ReferencedMessage, referenceAuthor, referenceMember); + message.ReferencedMessage.PopulateMentions(); + } + + if (message.MessageSnapshots != null) + { + foreach (DiscordMessageSnapshot snapshot in message.MessageSnapshots) + { + if (snapshot?.Message != null) + { + snapshot.Message.PopulateMentions(); + } + } + } + } + else // previous message was fetched in cache + { + oldmsg = new DiscordMessage(message); + + // cached message is updated with information from the event message + message.EditedTimestamp = event_message.EditedTimestamp; + if (event_message.Content != null) + { + message.Content = event_message.Content; + } + + message.embeds.Clear(); + message.embeds.AddRange(event_message.embeds); + message.attachments.Clear(); + message.attachments.AddRange(event_message.attachments); + message.Pinned = event_message.Pinned; + message.IsTTS = event_message.IsTTS; + message.Poll = event_message.Poll; + + // Mentions + message.mentionedUsers.Clear(); + message.mentionedUsers.AddRange(event_message.mentionedUsers ?? []); + message.mentionedRoles.Clear(); + message.mentionedRoles.AddRange(event_message.mentionedRoles ?? []); + message.mentionedChannels.Clear(); + message.mentionedChannels.AddRange(event_message.mentionedChannels ?? []); + message.MentionEveryone = event_message.MentionEveryone; + } + + message.PopulateMentions(); + + MessageUpdatedEventArgs ea = new() + { + Message = message, + MessageBefore = oldmsg, + MentionedUsers = new ReadOnlyCollection(message.mentionedUsers), + MentionedRoles = message.mentionedRoles != null ? new ReadOnlyCollection(message.mentionedRoles) : null, + MentionedChannels = message.mentionedChannels != null ? new ReadOnlyCollection(message.mentionedChannels) : null + }; + + await this.dispatcher.DispatchAsync(this, ea); + } + + internal async Task OnMessageDeleteEventAsync(ulong messageId, ulong channelId, ulong? guildId) + { + DiscordGuild guild = InternalGetCachedGuild(guildId); + DiscordChannel? channel = InternalGetCachedChannel(channelId, guildId) ?? InternalGetCachedThread(channelId, guildId); + + if (channel == null) + { + channel = new DiscordDmChannel + { + Id = channelId, + Discord = this, + Type = DiscordChannelType.Private, + Recipients = Array.Empty() + + }; + + this.privateChannels[channelId] = (DiscordDmChannel)channel; + } + + if (!this.MessageCache.TryGet(messageId, out DiscordMessage? msg)) + { + msg = new DiscordMessage + { + Id = messageId, + ChannelId = channelId, + Discord = this, + }; + } + + this.MessageCache?.Remove(msg.Id); + + MessageDeletedEventArgs ea = new() + { + Message = msg, + Channel = channel, + Guild = guild, + }; + + await this.dispatcher.DispatchAsync(this, ea); + } + + private async Task OnMessagePollVoteEventAsync(DiscordPollVoteUpdate voteUpdate, bool wasAdded) + { + voteUpdate.WasAdded = wasAdded; + voteUpdate.client = this; + + MessagePollVotedEventArgs ea = new() + { + PollVoteUpdate = voteUpdate + }; + + await this.dispatcher.DispatchAsync(this, ea); + } + + internal async Task OnMessageBulkDeleteEventAsync(ulong[] messageIds, ulong channelId, ulong? guildId) + { + DiscordChannel? channel = InternalGetCachedChannel(channelId, guildId) ?? InternalGetCachedThread(channelId, guildId); + + List msgs = new(messageIds.Length); + foreach (ulong messageId in messageIds) + { + if (!this.MessageCache.TryGet(messageId, out DiscordMessage? msg)) + { + msg = new DiscordMessage + { + Id = messageId, + ChannelId = channelId, + Discord = this, + }; + } + + this.MessageCache?.Remove(msg.Id); + + msgs.Add(msg); + } + + DiscordGuild guild = InternalGetCachedGuild(guildId); + + MessagesBulkDeletedEventArgs ea = new() + { + Channel = channel, + Messages = new ReadOnlyCollection(msgs), + Guild = guild + }; + + await this.dispatcher.DispatchAsync(this, ea); + } + + #endregion + + #region Message Reaction + + internal async Task OnMessageReactionAddAsync(ulong userId, ulong messageId, ulong channelId, ulong? guildId, TransportMember mbr, DiscordEmoji emoji) + { + DiscordChannel? channel = InternalGetCachedChannel(channelId, guildId) ?? InternalGetCachedThread(channelId, guildId); + DiscordGuild? guild = InternalGetCachedGuild(guildId); + + emoji.Discord = this; + + DiscordUser usr = null!; + usr = !TryGetCachedUserInternal(userId, out usr) + ? UpdateUser(new DiscordUser { Id = userId, Discord = this }, guildId, guild, mbr) + : UpdateUser(usr, guild?.Id, guild, mbr); + + if (channel == null) + { + channel = new DiscordDmChannel + { + Id = channelId, + Discord = this, + Type = DiscordChannelType.Private, + Recipients = new DiscordUser[] { usr } + }; + this.privateChannels[channelId] = (DiscordDmChannel)channel; + } + + if (!this.MessageCache.TryGet(messageId, out DiscordMessage? msg)) + { + msg = new DiscordMessage + { + Id = messageId, + ChannelId = channelId, + Discord = this, + reactions = [] + }; + } + + DiscordReaction? react = msg.reactions.FirstOrDefault(xr => xr.Emoji == emoji); + + if (react == null) + { + msg.reactions.Add(react = new DiscordReaction + { + Count = 1, + Emoji = emoji, + IsMe = this.CurrentUser.Id == userId + }); + } + else + { + react.Count++; + react.IsMe |= this.CurrentUser.Id == userId; + } + + MessageReactionAddedEventArgs ea = new() + { + Message = msg, + User = usr, + Guild = guild, + Emoji = emoji + }; + + await this.dispatcher.DispatchAsync(this, ea); + } + + internal async Task OnMessageReactionRemoveAsync(ulong userId, ulong messageId, ulong channelId, ulong? guildId, DiscordEmoji emoji) + { + DiscordChannel? channel = InternalGetCachedChannel(channelId, guildId) ?? InternalGetCachedThread(channelId, guildId); + + emoji.Discord = this; + + if (!this.UserCache.TryGetValue(userId, out DiscordUser? usr)) + { + usr = new DiscordUser { Id = userId, Discord = this }; + } + + if (channel == null) + { + channel = new DiscordDmChannel + { + Id = channelId, + Discord = this, + Type = DiscordChannelType.Private, + Recipients = new DiscordUser[] { usr } + }; + this.privateChannels[channelId] = (DiscordDmChannel)channel; + } + + if (channel?.Guild != null) + { + usr = channel.Guild.Members.TryGetValue(userId, out DiscordMember? member) + ? member + : new DiscordMember(usr) { Discord = this, guild_id = channel.GuildId.Value }; + } + + if (!this.MessageCache.TryGet(messageId, out DiscordMessage? msg)) + { + msg = new DiscordMessage + { + Id = messageId, + ChannelId = channelId, + Discord = this + }; + } + + DiscordReaction? react = msg.reactions?.FirstOrDefault(xr => xr.Emoji == emoji); + if (react != null) + { + react.Count--; + react.IsMe &= this.CurrentUser.Id != userId; + + if (msg.reactions != null && react.Count <= 0) // shit happens + { + for (int i = 0; i < msg.reactions.Count; i++) + { + if (msg.reactions[i].Emoji == emoji) + { + msg.reactions.RemoveAt(i); + break; + } + } + } + } + + DiscordGuild guild = InternalGetCachedGuild(guildId); + + MessageReactionRemovedEventArgs ea = new() + { + Message = msg, + User = usr, + Guild = guild, + Emoji = emoji + }; + await this.dispatcher.DispatchAsync(this, ea); + } + + internal async Task OnMessageReactionRemoveAllAsync(ulong messageId, ulong channelId, ulong? guildId) + { + _ = InternalGetCachedChannel(channelId, guildId) ?? InternalGetCachedThread(channelId, guildId); + + if (!this.MessageCache.TryGet(messageId, out DiscordMessage? msg)) + { + msg = new DiscordMessage + { + Id = messageId, + ChannelId = channelId, + Discord = this + }; + } + + msg.reactions?.Clear(); + + MessageReactionsClearedEventArgs ea = new() + { + Message = msg, + }; + + await this.dispatcher.DispatchAsync(this, ea); + } + + internal async Task OnMessageReactionRemoveEmojiAsync(ulong messageId, ulong channelId, ulong guildId, JToken dat) + { + DiscordGuild guild = InternalGetCachedGuild(guildId); + DiscordChannel? channel = InternalGetCachedChannel(channelId, guildId) ?? InternalGetCachedThread(channelId, guildId); + + if (channel == null) + { + channel = new DiscordDmChannel + { + Id = channelId, + Discord = this, + Type = DiscordChannelType.Private, + Recipients = Array.Empty() + }; + this.privateChannels[channelId] = (DiscordDmChannel)channel; + } + + if (!this.MessageCache.TryGet(messageId, out DiscordMessage? msg)) + { + msg = new DiscordMessage + { + Id = messageId, + ChannelId = channelId, + Discord = this + }; + } + + DiscordEmoji partialEmoji = dat.ToDiscordObject(); + + if (!guild.emojis.TryGetValue(partialEmoji.Id, out DiscordEmoji? emoji)) + { + emoji = partialEmoji; + emoji.Discord = this; + } + + msg.reactions?.RemoveAll(r => r.Emoji.Equals(emoji)); + + MessageReactionRemovedEmojiEventArgs ea = new() + { + Message = msg, + Channel = channel, + Guild = guild, + Emoji = emoji + }; + + await this.dispatcher.DispatchAsync(this, ea); + } + + #endregion + + #region User/Presence Update + + internal async Task OnPresenceUpdateEventAsync(JObject rawPresence, JObject rawUser) + { + ulong uid = (ulong)rawUser["id"]; + DiscordPresence old = null; + + if (this.presences.TryGetValue(uid, out DiscordPresence? presence)) + { + old = new DiscordPresence(presence); + DiscordJson.PopulateObject(rawPresence, presence); + } + else + { + presence = rawPresence.ToDiscordObject(); + presence.Discord = this; + presence.Activity = new DiscordActivity(presence.RawActivity); + this.presences[presence.InternalUser.Id] = presence; + } + + // reuse arrays / avoid linq (this is a hot zone) + if (presence.Activities == null || rawPresence["activities"] == null) + { + presence.internalActivities = []; + } + else + { + if (presence.internalActivities.Length != presence.RawActivities.Length) + { + presence.internalActivities = new DiscordActivity[presence.RawActivities.Length]; + } + + for (int i = 0; i < presence.internalActivities.Length; i++) + { + presence.internalActivities[i] = new DiscordActivity(presence.RawActivities[i]); + } + + if (presence.internalActivities.Length > 0) + { + presence.RawActivity = presence.RawActivities[0]; + + if (presence.Activity != null) + { + presence.Activity.UpdateWith(presence.RawActivity); + } + else + { + presence.Activity = new DiscordActivity(presence.RawActivity); + } + } + else + { + presence.RawActivity = null; + presence.Activity = null; + } + } + + // Caching partial objects is not a good idea, but considering these + // Objects will most likely be GC'd immediately after this event, + // This probably isn't great for GC pressure because this is a hot zone. + _ = this.UserCache.TryGetValue(uid, out DiscordUser? usr); + + DiscordUser usrafter = usr ?? new DiscordUser(presence.InternalUser); + PresenceUpdatedEventArgs ea = new() + { + Status = presence.Status, + Activity = presence.Activity, + User = usr, + PresenceBefore = old, + PresenceAfter = presence, + UserBefore = old != null ? new DiscordUser(old.InternalUser) { Discord = this } : usrafter, + UserAfter = usrafter + }; + + await this.dispatcher.DispatchAsync(this, ea); + } + + internal async Task OnUserSettingsUpdateEventAsync(TransportUser user) + { + DiscordUser usr = new(user) { Discord = this }; + + UserSettingsUpdatedEventArgs ea = new() + { + User = usr + }; + + await this.dispatcher.DispatchAsync(this, ea); + } + + internal async Task OnUserUpdateEventAsync(TransportUser user) + { + DiscordUser usr_old = new() + { + AvatarHash = this.CurrentUser.AvatarHash, + Discord = this, + Discriminator = this.CurrentUser.Discriminator, + Email = this.CurrentUser.Email, + Id = this.CurrentUser.Id, + IsBot = this.CurrentUser.IsBot, + MfaEnabled = this.CurrentUser.MfaEnabled, + Username = this.CurrentUser.Username, + Verified = this.CurrentUser.Verified + }; + + this.CurrentUser.AvatarHash = user.AvatarHash; + this.CurrentUser.Discriminator = user.Discriminator; + this.CurrentUser.Email = user.Email; + this.CurrentUser.Id = user.Id; + this.CurrentUser.IsBot = user.IsBot; + this.CurrentUser.MfaEnabled = user.MfaEnabled; + this.CurrentUser.Username = user.Username; + this.CurrentUser.Verified = user.Verified; + + UserUpdatedEventArgs ea = new() + { + UserAfter = this.CurrentUser, + UserBefore = usr_old + }; + + await this.dispatcher.DispatchAsync(this, ea); + } + + #endregion + + #region Voice + + internal async Task OnVoiceStateUpdateEventAsync(JObject raw) + { + ulong gid = (ulong)raw["guild_id"]; + ulong uid = (ulong)raw["user_id"]; + DiscordGuild gld = this.guilds[gid]; + + DiscordVoiceState vstateNew = raw.ToDiscordObject(); + vstateNew.Discord = this; + + gld.voiceStates.TryRemove(uid, out DiscordVoiceState? vstateOld); + + if (vstateNew.ChannelId != null) + { + gld.voiceStates[vstateNew.UserId] = vstateNew; + } + + if (gld.members.TryGetValue(uid, out DiscordMember? mbr)) + { + mbr.IsMuted = vstateNew.IsServerMuted; + mbr.IsDeafened = vstateNew.IsServerDeafened; + } + else + { + TransportMember transportMbr = vstateNew.TransportMember; + UpdateUser(new DiscordUser(transportMbr.User) { Discord = this }, gid, gld, transportMbr); + } + + VoiceStateUpdatedEventArgs ea = new() + { + SessionId = vstateNew.SessionId, + + Before = vstateOld, + After = vstateNew + }; + + await this.dispatcher.DispatchAsync(this, ea); + } + + internal async Task OnVoiceServerUpdateEventAsync(string endpoint, string token, DiscordGuild guild) + { + VoiceServerUpdatedEventArgs ea = new() + { + Endpoint = endpoint, + VoiceToken = token, + Guild = guild + }; + + await this.dispatcher.DispatchAsync(this, ea); + } + + #endregion + + #region Thread + + internal async Task OnThreadCreateEventAsync(DiscordThreadChannel thread) + { + thread.Discord = this; + InternalGetCachedGuild(thread.GuildId).threads[thread.Id] = thread; + + await this.dispatcher.DispatchAsync(this, new ThreadCreatedEventArgs + { + Thread = thread, + Guild = thread.Guild, + Parent = thread.Parent + }); + } + + internal async Task OnThreadUpdateEventAsync(DiscordThreadChannel thread) + { + if (thread == null) + { + return; + } + + DiscordThreadChannel threadOld; + ThreadUpdatedEventArgs updateEvent; + + thread.Discord = this; + + DiscordGuild guild = thread.Guild; + guild.Discord = this; + + DiscordThreadChannel cthread = InternalGetCachedThread(thread.Id, thread.GuildId); + + if (cthread != null) //thread is cached + { + threadOld = new DiscordThreadChannel + { + Discord = this, + GuildId = cthread.GuildId, + CreatorId = cthread.CreatorId, + ParentId = cthread.ParentId, + Id = cthread.Id, + Name = cthread.Name, + Type = cthread.Type, + LastMessageId = cthread.LastMessageId, + MessageCount = cthread.MessageCount, + MemberCount = cthread.MemberCount, + ThreadMetadata = cthread.ThreadMetadata, + CurrentMember = cthread.CurrentMember, + }; + + updateEvent = new ThreadUpdatedEventArgs + { + ThreadAfter = thread, + ThreadBefore = threadOld, + Guild = thread.Guild, + Parent = thread.Parent + }; + } + else + { + updateEvent = new ThreadUpdatedEventArgs + { + ThreadAfter = thread, + Guild = thread.Guild, + Parent = thread.Parent + }; + guild.threads[thread.Id] = thread; + } + + await this.dispatcher.DispatchAsync(this, updateEvent); + } + + internal async Task OnThreadDeleteEventAsync(DiscordThreadChannel thread) + { + if (thread == null) + { + return; + } + + thread.Discord = this; + + DiscordGuild gld = thread.Guild; + if (gld.threads.TryRemove(thread.Id, out DiscordThreadChannel? cachedThread)) + { + thread = cachedThread; + } + + await this.dispatcher.DispatchAsync(this, new ThreadDeletedEventArgs + { + Thread = thread, + Guild = thread.Guild, + Parent = thread.Parent + }); + } + + internal async Task OnThreadListSyncEventAsync(DiscordGuild guild, IReadOnlyList channel_ids, IReadOnlyList threads, IReadOnlyList members) + { + guild.Discord = this; + IEnumerable channels = channel_ids.Select(x => guild.GetChannel(x) ?? new DiscordChannel { Id = x, GuildId = guild.Id }); //getting channel objects + + foreach (DiscordChannel? channel in channels) + { + channel.Discord = this; + } + + foreach (DiscordThreadChannel thread in threads) + { + thread.Discord = this; + guild.threads[thread.Id] = thread; + } + + foreach (DiscordThreadChannelMember member in members) + { + member.Discord = this; + member.guild_id = guild.Id; + + DiscordThreadChannel? thread = threads.SingleOrDefault(x => x.Id == member.ThreadId); + if (thread != null) + { + thread.CurrentMember = member; + } + } + + await this.dispatcher.DispatchAsync(this, new ThreadListSyncedEventArgs + { + Guild = guild, + Channels = channels.ToList().AsReadOnly(), + Threads = threads, + CurrentMembers = members.ToList().AsReadOnly() + }); + } + + internal async Task OnThreadMemberUpdateEventAsync(DiscordGuild guild, DiscordThreadChannelMember member) + { + member.Discord = this; + + DiscordThreadChannel thread = InternalGetCachedThread(member.ThreadId, guild.Id); + member.guild_id = guild.Id; + thread.CurrentMember = member; + guild.threads[thread.Id] = thread; + + await this.dispatcher.DispatchAsync(this, new ThreadMemberUpdatedEventArgs + { + ThreadMember = member, + Thread = thread + }); + } + + internal async Task OnThreadMembersUpdateEventAsync(DiscordGuild guild, ulong thread_id, IReadOnlyList addedMembers, IReadOnlyList removed_member_ids, int member_count) + { + DiscordThreadChannel? thread = InternalGetCachedThread(thread_id, guild.Id) ?? new DiscordThreadChannel + { + Id = thread_id, + GuildId = guild.Id, + }; + thread.Discord = this; + guild.Discord = this; + thread.MemberCount = member_count; + + List removedMembers = []; + if (removed_member_ids != null) + { + foreach (ulong? removedId in removed_member_ids) + { + removedMembers.Add(guild.members.TryGetValue(removedId.Value, out DiscordMember? member) ? member : new DiscordMember { Id = removedId.Value, guild_id = guild.Id, Discord = this }); + } + + if (removed_member_ids.Contains(this.CurrentUser.Id)) //indicates the bot was removed from the thread + { + thread.CurrentMember = null; + } + } + else + { + removed_member_ids = Array.Empty(); + } + + if (addedMembers != null) + { + foreach (DiscordThreadChannelMember threadMember in addedMembers) + { + threadMember.Discord = this; + threadMember.guild_id = guild.Id; + } + + if (addedMembers.Any(member => member.Id == this.CurrentUser.Id)) + { + thread.CurrentMember = addedMembers.Single(member => member.Id == this.CurrentUser.Id); + } + } + else + { + addedMembers = Array.Empty(); + } + + ThreadMembersUpdatedEventArgs threadMembersUpdateArg = new() + { + Guild = guild, + Thread = thread, + AddedMembers = addedMembers, + RemovedMembers = removedMembers, + MemberCount = member_count + }; + + await this.dispatcher.DispatchAsync(this, threadMembersUpdateArg); + } + + #endregion + + #region Integration + + internal async Task OnIntegrationCreateAsync(DiscordIntegration integration, ulong guild_id) + { + DiscordGuild? guild = InternalGetCachedGuild(guild_id) ?? new DiscordGuild + { + Id = guild_id, + Discord = this + }; + + IntegrationCreatedEventArgs ea = new() + { + Guild = guild, + Integration = integration + }; + + await this.dispatcher.DispatchAsync(this, ea); + } + + internal async Task OnIntegrationUpdateAsync(DiscordIntegration integration, ulong guild_id) + { + DiscordGuild? guild = InternalGetCachedGuild(guild_id) ?? new DiscordGuild + { + Id = guild_id, + Discord = this + }; + + IntegrationUpdatedEventArgs ea = new() + { + Guild = guild, + Integration = integration + }; + + await this.dispatcher.DispatchAsync(this, ea); + } + + internal async Task OnIntegrationDeleteAsync(ulong integration_id, ulong guild_id, ulong? application_id) + { + DiscordGuild? guild = InternalGetCachedGuild(guild_id) ?? new DiscordGuild + { + Id = guild_id, + Discord = this + }; + + IntegrationDeletedEventArgs ea = new() + { + Guild = guild, + Applicationid = application_id, + IntegrationId = integration_id + }; + + await this.dispatcher.DispatchAsync(this, ea); + } + + #endregion + + #region Commands + + internal async Task OnApplicationCommandPermissionsUpdateAsync(JObject obj) + { + ApplicationCommandPermissionsUpdatedEventArgs ev = obj.ToObject()!; + + await this.dispatcher.DispatchAsync(this, ev); + } + + #endregion + + #region Stage Instance + + internal async Task OnStageInstanceCreateAsync(DiscordStageInstance instance) + { + instance.Discord = this; + + DiscordGuild guild = InternalGetCachedGuild(instance.GuildId); + + guild.stageInstances[instance.Id] = instance; + + StageInstanceCreatedEventArgs eventArgs = new() + { + StageInstance = instance + }; + + await this.dispatcher.DispatchAsync(this, eventArgs); + } + + internal async Task OnStageInstanceUpdateAsync(DiscordStageInstance instance) + { + instance.Discord = this; + + DiscordGuild guild = InternalGetCachedGuild(instance.GuildId); + + if (!guild.stageInstances.TryRemove(instance.Id, out DiscordStageInstance? oldInstance)) + { + oldInstance = new DiscordStageInstance { Id = instance.Id, GuildId = instance.GuildId, ChannelId = instance.ChannelId }; + } + + guild.stageInstances[instance.Id] = instance; + + StageInstanceUpdatedEventArgs eventArgs = new() + { + StageInstanceBefore = oldInstance, + StageInstanceAfter = instance + }; + + await this.dispatcher.DispatchAsync(this, eventArgs); + } + + internal async Task OnStageInstanceDeleteAsync(DiscordStageInstance instance) + { + instance.Discord = this; + + DiscordGuild guild = InternalGetCachedGuild(instance.GuildId); + + guild.stageInstances.TryRemove(instance.Id, out _); + + StageInstanceDeletedEventArgs eventArgs = new() + { + StageInstance = instance + }; + + await this.dispatcher.DispatchAsync(this, eventArgs); + } + + #endregion + + #region Misc + + internal async Task OnApplicationAuthorizedAsync(DiscordWebhookEventBody body) + { + ApplicationAuthorizedPayload payload = body.Data.ToDiscordObject(); + + DiscordGuild? guild = payload.Guild; + DiscordUser user = payload.User; + + UpdateUserCache(user); + + if (guild is not null) + { + if (this.guilds.TryGetValue(guild.Id, out DiscordGuild? cachedGuild)) + { + guild = cachedGuild; + } + + guild.Discord = this; + + if (guild.Members.TryGetValue(user.Id, out DiscordMember? member)) + { + user = member; + } + } + else + { + user.Discord = this; + } + + ApplicationAuthorizedEventArgs eventArgs = new() + { + Guild = guild, + IntegrationType = payload.IntegrationType, + Scopes = payload.Scopes, + User = user, + Timestamp = body.Timestamp + }; + + await this.dispatcher.DispatchAsync(this, eventArgs); + } + + internal async Task OnInteractionCreateAsync(ulong? guildId, ulong channelId, TransportUser user, TransportMember member, DiscordChannel? channel, DiscordInteraction interaction) + { + DiscordUser usr = new(user) { Discord = this }; + + interaction.ChannelId = channelId; + interaction.GuildId = guildId; + interaction.Discord = this; + interaction.Data.Discord = this; + + if (member is not null && guildId is not null && interaction.Guild is not null) + { + usr = new DiscordMember(member) { guild_id = guildId.Value, Discord = this }; + UpdateUser(usr, guildId, interaction.Guild, member); + } + else + { + UpdateUserCache(usr); + } + + interaction.User = usr; + + DiscordInteractionResolvedCollection resolved = interaction.Data.Resolved; + if (resolved != null) + { + if (resolved.Users != null) + { + foreach (KeyValuePair c in resolved.Users) + { + c.Value.Discord = this; + UpdateUserCache(c.Value); + } + } + + if (resolved.Members != null) + { + foreach (KeyValuePair c in resolved.Members) + { + c.Value.Discord = this; + c.Value.Id = c.Key; + c.Value.guild_id = guildId.Value; + c.Value.User.Discord = this; + + UpdateUserCache(c.Value.User); + } + } + + if (resolved.Channels != null) + { + foreach (KeyValuePair c in resolved.Channels) + { + c.Value.Discord = this; + UpdateChannelCache(c.Value); + if (guildId.HasValue) + { + c.Value.GuildId = guildId.Value; + } + } + } + + if (resolved.Roles != null) + { + foreach (KeyValuePair c in resolved.Roles) + { + c.Value.Discord = this; + + if (guildId.HasValue) + { + c.Value.guild_id = guildId.Value; + if (this.guilds.TryGetValue(guildId.Value, out DiscordGuild? guild)) + { + guild.roles.TryAdd(c.Value.Id, c.Value); + } + } + } + } + + if (resolved.Messages != null) + { + foreach (KeyValuePair m in resolved.Messages) + { + m.Value.Discord = this; + + if (guildId.HasValue) + { + m.Value.guildId = guildId.Value; + } + + this.MessageCache?.Add(m.Value); + } + } + } + + UpdateChannelCache(channel); + + if (interaction.Type is DiscordInteractionType.Component) + { + + interaction.Message.Discord = this; + interaction.Message.ChannelId = interaction.ChannelId; + ComponentInteractionCreatedEventArgs cea = new() + { + Message = interaction.Message, + Interaction = interaction + }; + + await this.dispatcher.DispatchAsync(this, cea); + } + else if (interaction.Type is DiscordInteractionType.ModalSubmit) + { + ModalSubmittedEventArgs mea = new(interaction); + + await this.dispatcher.DispatchAsync(this, mea); + } + else if (interaction.Data.Type is DiscordApplicationCommandType.MessageContextMenu or DiscordApplicationCommandType.UserContextMenu) // Context-Menu. // + { + ulong targetId = interaction.Data.Target.Value; + DiscordUser targetUser = null; + DiscordMember targetMember = null; + DiscordMessage targetMessage = null; + + interaction.Data.Resolved.Messages?.TryGetValue(targetId, out targetMessage); + interaction.Data.Resolved.Members?.TryGetValue(targetId, out targetMember); + interaction.Data.Resolved.Users?.TryGetValue(targetId, out targetUser); + + ContextMenuInteractionCreatedEventArgs ctea = new() + { + Interaction = interaction, + TargetUser = targetMember ?? targetUser, + TargetMessage = targetMessage, + Type = interaction.Data.Type, + }; + + await this.dispatcher.DispatchAsync(this, ctea); + } + + InteractionCreatedEventArgs ea = new() + { + Interaction = interaction + }; + + await this.dispatcher.DispatchAsync(this, ea); + } + + internal async Task OnTypingStartEventAsync(ulong userId, ulong channelId, DiscordChannel channel, ulong? guildId, DateTimeOffset started, TransportMember mbr) + { + if (channel == null) + { + channel = new DiscordChannel + { + Discord = this, + Id = channelId, + GuildId = guildId ?? default, + }; + } + + DiscordGuild guild = InternalGetCachedGuild(guildId); + DiscordUser usr = UpdateUser(new DiscordUser { Id = userId, Discord = this }, guildId, guild, mbr); + + TypingStartedEventArgs ea = new() + { + Channel = channel, + User = usr, + Guild = guild, + StartedAt = started + }; + await this.dispatcher.DispatchAsync(this, ea); + } + + internal async Task OnWebhooksUpdateAsync(DiscordChannel channel, DiscordGuild guild) + { + WebhooksUpdatedEventArgs ea = new() + { + Channel = channel, + Guild = guild + }; + await this.dispatcher.DispatchAsync(this, ea); + } + + internal async Task OnStickersUpdatedAsync(IEnumerable newStickers, JObject raw) + { + DiscordGuild guild = InternalGetCachedGuild((ulong)raw["guild_id"]); + ConcurrentDictionary oldStickers = new(guild.stickers); + + guild.stickers.Clear(); + + foreach (DiscordMessageSticker nst in newStickers) + { + if (nst.User != null) + { + nst.User.Discord = this; + } + + nst.Discord = this; + + guild.stickers[nst.Id] = nst; + } + + GuildStickersUpdatedEventArgs sea = new() + { + Guild = guild, + StickersBefore = oldStickers, + StickersAfter = guild.Stickers + }; + + await this.dispatcher.DispatchAsync(this, sea); + } + + internal async Task OnUnknownEventAsync(GatewayPayload payload) + { + UnknownEventArgs ea = new() { EventName = payload.EventName, Json = (payload.Data as JObject)?.ToString() }; + await this.dispatcher.DispatchAsync(this, ea); + } + + internal async Task OnUnknownWebhookEventAsync(DiscordWebhookEventBody body) + { + UnknownEventArgs eventArgs = new() + { + EventName = body.Type.ToString(), + Json = body.Data?.ToString() + }; + + await this.dispatcher.DispatchAsync(this, eventArgs); + } + + #endregion + + #region AutoModeration + internal async Task OnAutoModerationRuleCreateAsync(DiscordAutoModerationRule ruleCreated) + { + ruleCreated.Discord = this; + await this.dispatcher.DispatchAsync(this, new AutoModerationRuleCreatedEventArgs + { + Rule = ruleCreated + }); + } + + internal async Task OnAutoModerationRuleUpdatedAsync(DiscordAutoModerationRule ruleUpdated) + { + ruleUpdated.Discord = this; + await this.dispatcher.DispatchAsync(this, new AutoModerationRuleUpdatedEventArgs + { + Rule = ruleUpdated + }); + } + + internal async Task OnAutoModerationRuleDeletedAsync(DiscordAutoModerationRule ruleDeleted) + { + ruleDeleted.Discord = this; + await this.dispatcher.DispatchAsync(this, new AutoModerationRuleDeletedEventArgs + { + Rule = ruleDeleted + }); + } + + internal async Task OnAutoModerationRuleExecutedAsync(DiscordAutoModerationActionExecution ruleExecuted) + { + await this.dispatcher.DispatchAsync(this, new AutoModerationRuleExecutedEventArgs + { + Rule = ruleExecuted + }); + } + #endregion + + #region Entitlements + + private async Task OnEntitlementCreatedAsync(DiscordEntitlement entitlement) + => await this.dispatcher.DispatchAsync(this, new EntitlementCreatedEventArgs { Entitlement = entitlement }); + + private async Task OnWebhookEntitlementCreateAsync(DiscordWebhookEventBody body) + { + await this.dispatcher.DispatchAsync + ( + this, + new EntitlementCreatedEventArgs + { + Entitlement = body.Data.ToDiscordObject(), + Timestamp = body.Timestamp + } + ); + } + + private async Task OnEntitlementUpdatedAsync(DiscordEntitlement entitlement) + => await this.dispatcher.DispatchAsync(this, new EntitlementUpdatedEventArgs { Entitlement = entitlement }); + + private async Task OnEntitlementDeletedAsync(DiscordEntitlement entitlement) + => await this.dispatcher.DispatchAsync(this, new EntitlementDeletedEventArgs { Entitlement = entitlement }); + + #endregion + + #endregion +} diff --git a/DSharpPlus/Clients/DiscordClient.cs b/DSharpPlus/Clients/DiscordClient.cs index c7dadd3c0d..bfdc964032 100644 --- a/DSharpPlus/Clients/DiscordClient.cs +++ b/DSharpPlus/Clients/DiscordClient.cs @@ -1,1346 +1,1346 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Text; -using System.Threading; -using System.Threading.Channels; -using System.Threading.Tasks; - -using DSharpPlus.Clients; -using DSharpPlus.Entities; -using DSharpPlus.EventArgs; -using DSharpPlus.Exceptions; -using DSharpPlus.Net; -using DSharpPlus.Net.Abstractions; -using DSharpPlus.Net.Gateway; -using DSharpPlus.Net.InboundWebhooks; -using DSharpPlus.Net.Models; -using DSharpPlus.Net.Serialization; -using DSharpPlus.Net.WebSocket; - -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -using Newtonsoft.Json.Linq; - -namespace DSharpPlus; - -/// -/// A Discord API wrapper. -/// -public sealed partial class DiscordClient : BaseDiscordClient -{ - internal static readonly DateTimeOffset discordEpoch = new(2015, 1, 1, 0, 0, 0, TimeSpan.Zero); - private static readonly ConcurrentDictionary socketLocks = []; - - internal bool isShard = false; - internal IMessageCacheProvider? MessageCache { get; } - private readonly IClientErrorHandler errorHandler; - private readonly IShardOrchestrator orchestrator; - private readonly ChannelReader eventReader; - private readonly ChannelReader webhookEventReader; - private readonly ChannelReader interactionEventReader; - private readonly IEventDispatcher dispatcher; - - private readonly ConcurrentDictionary> guildMembersChunkedEvents = []; - - private StatusUpdate? status = null; - private readonly string token; - - private readonly ManualResetEventSlim connectionLock = new(true); - - /// - /// Gets the service provider used within this Discord application. - /// - public IServiceProvider ServiceProvider { get; internal set; } - - /// - /// Gets whether this client is connected to the gateway. - /// - public bool AllShardsConnected - => this.orchestrator.AllShardsConnected; - - /// - /// Gets a dictionary of DM channels that have been cached by this client. The dictionary's key is the channel - /// ID. - /// - public IReadOnlyDictionary PrivateChannels => this.privateChannels; - internal ConcurrentDictionary privateChannels = new(); - - /// - /// 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 - /// GuildAvailable or GuildDownloadCompleted events haven't been fired yet) - /// - public override IReadOnlyDictionary Guilds => this.guilds; - internal ConcurrentDictionary guilds = new(); - - /// - /// Gets the latency in the connection to a specific guild. - /// - public TimeSpan GetConnectionLatency(ulong guildId) - => this.orchestrator.GetConnectionLatency(guildId); - - /// - /// Gets the collection of presences held by this client. - /// - public IReadOnlyDictionary Presences - => this.presences; - - internal Dictionary presences = []; - - [ActivatorUtilitiesConstructor] - public DiscordClient - ( - ILogger logger, - DiscordApiClient apiClient, - IMessageCacheProvider messageCacheProvider, - IServiceProvider serviceProvider, - IEventDispatcher eventDispatcher, - IClientErrorHandler errorHandler, - IOptions configuration, - IOptions token, - IShardOrchestrator shardOrchestrator, - IOptions gatewayOptions, - - [FromKeyedServices("DSharpPlus.Gateway.EventChannel")] - Channel eventChannel, - - [FromKeyedServices("DSharpPlus.Webhooks.EventChannel")] - Channel webhookEventChannel, - - [FromKeyedServices("DSharpPlus.Interactions.EventChannel")] - Channel interactionEventChannel - ) - : base() - { - this.Logger = logger; - this.MessageCache = messageCacheProvider; - this.ServiceProvider = serviceProvider; - this.ApiClient = apiClient; - this.errorHandler = errorHandler; - this.Configuration = configuration.Value; - this.token = token.Value.GetToken(); - this.orchestrator = shardOrchestrator; - this.eventReader = eventChannel.Reader; - this.dispatcher = eventDispatcher; - this.webhookEventReader = webhookEventChannel.Reader; - this.interactionEventReader = interactionEventChannel.Reader; - - this.ApiClient.SetClient(this); - this.Intents = gatewayOptions.Value.Intents; - - this.guilds.Clear(); - } - - #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, DiscordUserStatus? status = null, DateTimeOffset? idlesince = null) - { - // method checks if its already initialized - await InitializeAsync(); - - // Check if connection lock is already set, and set it if it isn't - if (!this.connectionLock.Wait(0)) - { - throw new InvalidOperationException("This client is already connected."); - } - - this.connectionLock.Set(); - - if (activity == null && status == null && idlesince == null) - { - this.status = null; - } - else - { - long? since_unix = idlesince != null ? Utilities.GetUnixTime(idlesince.Value) : null; - this.status = new StatusUpdate() - { - Activity = new TransportActivity(activity), - Status = status ?? DiscordUserStatus.Online, - IdleSince = since_unix, - IsAFK = idlesince != null, - activity = activity - }; - } - - this.Logger.LogInformation(LoggerEvents.Startup, "DSharpPlus; version {Version}", this.VersionString); - - await this.dispatcher.DispatchAsync(this, new ClientStartedEventArgs()); - - _ = ReceiveGatewayEventsAsync(); - _ = ReceiveWebhookEventsAsync(); - _ = ReceiveInteractionEventsAsync(); - - await this.orchestrator.StartAsync(activity, status, idlesince); - } - - /// - /// Sends a raw payload to the gateway. This method is not recommended for use unless you know what you're doing. - /// - /// The opcode to send to the Discord gateway. - /// The data to serialize. - /// The guild this payload originates from. Pass 0 for shard-independent payloads. - /// - /// This method should not be used unless you know what you're doing. Instead, look towards the other - /// explicitly implemented methods which come with client-side validation. - /// - /// A task representing the payload being sent. - [Experimental("DSP0004")] - public async Task SendPayloadAsync(GatewayOpCode opCode, object? data, ulong guildId) - { - GatewayPayload payload = new() - { - OpCode = opCode, - Data = data - }; - - string payloadString = DiscordJson.SerializeObject(payload); - await this.orchestrator.SendOutboundEventAsync(Encoding.UTF8.GetBytes(payloadString), guildId); - } - - /// - /// Reconnects all shards to the gateway. - /// - public async Task ReconnectAsync() - => await this.orchestrator.ReconnectAsync(); - - /// - /// Disconnects from the gateway - /// - /// - public async Task DisconnectAsync() - { - await this.orchestrator.StopAsync(); - await this.dispatcher.DispatchAsync(this, new ClientStoppedEventArgs()); - } - - #endregion - - #region Public REST Methods - - /// - /// Gets a sticker. - /// - /// The ID of the sticker. - /// The specified sticker - public async Task GetStickerAsync(ulong stickerId) - => await this.ApiClient.GetStickerAsync(stickerId); - - /// - /// Gets a collection of sticker packs that may be used by nitro users. - /// - /// - public async Task> GetStickerPacksAsync() - => await this.ApiClient.GetStickerPacksAsync(); - - /// - /// Gets a user - /// - /// ID of the user - /// Whether to always make a REST request and update cache. Passing true will update the user, updating stale properties such as . - /// - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public async Task GetUserAsync(ulong userId, bool updateCache = false) - { - if (!updateCache && TryGetCachedUserInternal(userId, out DiscordUser? usr)) - { - return usr; - } - - usr = await this.ApiClient.GetUserAsync(userId); - - // See BaseDiscordClient.UpdateUser for why this is done like this. - this.UserCache.AddOrUpdate(userId, usr, (_, _) => usr); - - return usr; - } - - /// - /// Gets a channel - /// - /// The ID of the channel to get. - /// - /// 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) - => InternalGetCachedThread(id) ?? InternalGetCachedChannel(id) ?? await this.ApiClient.GetChannelAsync(id); - - /// - /// Sends a message - /// - /// Channel to send to. - /// Message content to send. - /// The Discord 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 async Task SendMessageAsync(DiscordChannel channel, string content) - => await this.ApiClient.CreateMessageAsync(channel.Id, content, embeds: null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false, suppressNotifications: false); - - /// - /// Sends a message - /// - /// Channel to send to. - /// Embed to attach to the message. - /// The Discord 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 async Task SendMessageAsync(DiscordChannel channel, DiscordEmbed embed) - => await this.ApiClient.CreateMessageAsync(channel.Id, null, embed != null ? new[] { embed } : null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false, suppressNotifications: false); - - /// - /// Sends a message - /// - /// Channel to send to. - /// Message content to send. - /// Embed to attach to the message. - /// The Discord 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 async Task SendMessageAsync(DiscordChannel channel, string content, DiscordEmbed embed) - => await this.ApiClient.CreateMessageAsync(channel.Id, content, embed != null ? new[] { embed } : null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false, suppressNotifications: false); - - /// - /// Sends a message - /// - /// Channel to send to. - /// The Discord Message builder. - /// The Discord 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 async Task SendMessageAsync(DiscordChannel channel, DiscordMessageBuilder builder) - => await this.ApiClient.CreateMessageAsync(channel.Id, builder); - - /// - /// Sends a message - /// - /// Channel to send to. - /// The Discord Message builder. - /// The Discord 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 async Task SendMessageAsync(DiscordChannel channel, Action action) - { - DiscordMessageBuilder builder = new(); - action(builder); - - return await 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 async Task CreateGuildAsync - ( - string name, - string? region = null, - Optional icon = default, - DiscordVerificationLevel? verificationLevel = null, - DiscordDefaultMessageNotifications? defaultMessageNotifications = null, - DiscordSystemChannelFlags? systemChannelFlags = null - ) - { - Optional iconb64 = Optional.FromNoValue(); - - if (icon.HasValue && icon.Value != null) - { - using InlineMediaTool imgtool = new(icon.Value); - iconb64 = imgtool.GetBase64(); - } - else if (icon.HasValue) - { - iconb64 = null; - } - - return await 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 async Task CreateGuildFromTemplateAsync(string code, string name, Optional icon = default) - { - Optional iconb64 = Optional.FromNoValue(); - - if (icon.HasValue && icon.Value != null) - { - using InlineMediaTool imgtool = new(icon.Value); - iconb64 = imgtool.GetBase64(); - } - else if (icon.HasValue) - { - iconb64 = null; - } - - return await this.ApiClient.CreateGuildFromTemplateAsync(code, name, iconb64); - } - - /// - /// 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.guilds.TryGetValue(id, out DiscordGuild? guild) && (!withCounts.HasValue || !withCounts.Value)) - { - return guild; - } - - guild = await this.ApiClient.GetGuildAsync(id, withCounts); - IReadOnlyList channels = await this.ApiClient.GetGuildChannelsAsync(guild.Id); - foreach (DiscordChannel channel in channels) - { - guild.channels[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 async Task GetGuildPreviewAsync(ulong id) - => await 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 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 async Task GetInviteByCodeAsync(string code, bool? withCounts = null, bool? withExpiration = null) - => await this.ApiClient.GetInviteAsync(code, withCounts, withExpiration); - - /// - /// Gets a list of connections - /// - /// - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public async Task> GetConnectionsAsync() - => await this.ApiClient.GetUsersConnectionsAsync(); - - /// - /// Gets a webhook - /// - /// The ID of webhook to get. - /// - /// Thrown when the webhook does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public async Task GetWebhookAsync(ulong id) - => await this.ApiClient.GetWebhookAsync(id); - - /// - /// Gets a webhook - /// - /// The ID of webhook to get. - /// - /// - /// Thrown when the webhook does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public async Task GetWebhookWithTokenAsync(ulong id, string token) - => await 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. - /// - /// The ID of the shard whose status should be updated. Defaults to null, which will update all shards controlled by - /// this DiscordClient. - /// - public async Task UpdateStatusAsync(DiscordActivity activity = null, DiscordUserStatus? userStatus = null, DateTimeOffset? idleSince = null, int? shardId = null) - { - StatusUpdate update = new() - { - Activity = new(activity), - IdleSince = idleSince?.ToUnixTimeMilliseconds() - }; - - if (userStatus is not null) - { - update.Status = userStatus.Value; - } - - GatewayPayload gatewayPayload = new() {OpCode = GatewayOpCode.StatusUpdate, Data = update}; - - string payload = DiscordJson.SerializeObject(gatewayPayload); - - if (shardId is null) - { - await this.orchestrator.BroadcastOutboundEventAsync(Encoding.UTF8.GetBytes(payload)); - } - else - { - // this is a bit of a hack. x % n, for any x < n, will always return x, so we can just pass the shard ID - // as guild ID. this won't be very graceful if you pass an invalid ID, though. - await this.orchestrator.SendOutboundEventAsync(Encoding.UTF8.GetBytes(payload), (ulong)shardId.Value); - } - } - - /// - /// Edits current user. - /// - /// New username. - /// New avatar. - /// New banner. - /// - /// 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 ModifyCurrentUserAsync(string username = null, Optional avatar = default, Optional banner = default) - { - Optional avatarBase64 = Optional.FromNoValue(); - if (avatar.HasValue && avatar.Value != null) - { - using InlineMediaTool imgtool = new(avatar.Value); - avatarBase64 = imgtool.GetBase64(); - } - else if (avatar.HasValue) - { - avatarBase64 = null; - } - - Optional bannerBase64 = Optional.FromNoValue(); - if (banner.HasValue && banner.Value != null) - { - using InlineMediaTool imgtool = new(banner.Value); - bannerBase64 = imgtool.GetBase64(); - } - else if (banner.HasValue) - { - bannerBase64 = null; - } - - TransportUser usr = await this.ApiClient.ModifyCurrentUserAsync(username, avatarBase64, bannerBase64); - - 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 async Task GetTemplateAsync(string code) - => await this.ApiClient.GetTemplateAsync(code); - - /// - /// Gets all the global application commands for this application. - /// - /// Whether to include localizations in the response. - /// A list of global application commands. - public async Task> GetGlobalApplicationCommandsAsync(bool withLocalizations = false) => - await this.ApiClient.GetGlobalApplicationCommandsAsync(this.CurrentApplication.Id, withLocalizations); - - /// - /// Overwrites the existing global application commands. New commands are automatically created and missing commands are automatically deleted. - /// - /// The list of commands to overwrite with. - /// The list of global commands. - public async Task> BulkOverwriteGlobalApplicationCommandsAsync(IEnumerable commands) => - await this.ApiClient.BulkOverwriteGlobalApplicationCommandsAsync(this.CurrentApplication.Id, commands); - - /// - /// Creates or overwrites a global application command. - /// - /// The command to create. - /// The created command. - public async Task CreateGlobalApplicationCommandAsync(DiscordApplicationCommand command) => - await 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 async Task GetGlobalApplicationCommandAsync(ulong commandId) => - await this.ApiClient.GetGlobalApplicationCommandAsync(this.CurrentApplication.Id, commandId); - - /// - /// Gets a global application command by its name. - /// - /// The name of the command to get. - /// Whether to include localizations in the response. - /// The command with the name. - public async Task GetGlobalApplicationCommandAsync(string commandName, bool withLocalizations = false) - { - foreach (DiscordApplicationCommand command in await this.ApiClient.GetGlobalApplicationCommandsAsync(this.CurrentApplication.Id, withLocalizations)) - { - if (command.Name == commandName) - { - return command; - } - } - - return null; - } - - /// - /// 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) - { - ApplicationCommandEditModel mdl = new(); - action(mdl); - - ulong applicationId = this.CurrentApplication?.Id ?? (await GetCurrentApplicationAsync()).Id; - - return await this.ApiClient.EditGlobalApplicationCommandAsync - ( - applicationId, - commandId, - mdl.Name, - mdl.Description, - mdl.Options, - mdl.DefaultPermission, - mdl.NSFW, - mdl.NameLocalizations, - mdl.DescriptionLocalizations, - mdl.AllowDMUsage, - mdl.DefaultMemberPermissions, - mdl.AllowedContexts, - mdl.IntegrationTypes - ); - } - - /// - /// Deletes a global application command. - /// - /// The ID of the command to delete. - public async Task DeleteGlobalApplicationCommandAsync(ulong commandId) => - await this.ApiClient.DeleteGlobalApplicationCommandAsync(this.CurrentApplication.Id, commandId); - - /// - /// Gets all the application commands for a guild. - /// - /// The ID of the guild to get application commands for. - /// Whether to include localizations in the response. - /// A list of application commands in the guild. - public async Task> GetGuildApplicationCommandsAsync(ulong guildId, bool withLocalizations = false) => - await this.ApiClient.GetGuildApplicationCommandsAsync(this.CurrentApplication.Id, guildId, withLocalizations); - - /// - /// Overwrites the existing application commands in a guild. New commands are automatically created and missing commands are automatically deleted. - /// - /// The ID of the guild. - /// The list of commands to overwrite with. - /// The list of guild commands. - public async Task> BulkOverwriteGuildApplicationCommandsAsync(ulong guildId, IEnumerable commands) => - await 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 async Task CreateGuildApplicationCommandAsync(ulong guildId, DiscordApplicationCommand command) => - await 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 async Task GetGuildApplicationCommandAsync(ulong guildId, ulong commandId) => - await 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) - { - ApplicationCommandEditModel mdl = new(); - action(mdl); - - ulong applicationId = this.CurrentApplication?.Id ?? (await GetCurrentApplicationAsync()).Id; - - return await this.ApiClient.EditGuildApplicationCommandAsync - ( - applicationId, - guildId, - commandId, - mdl.Name, - mdl.Description, - mdl.Options, - mdl.DefaultPermission, - mdl.NSFW, - mdl.NameLocalizations, - mdl.DescriptionLocalizations, - mdl.AllowDMUsage, - mdl.DefaultMemberPermissions, - mdl.AllowedContexts, - mdl.IntegrationTypes - ); - } - - /// - /// Deletes a application command in a guild. - /// - /// The ID of the guild to delete the application command in. - /// The ID of the command. - public async Task DeleteGuildApplicationCommandAsync(ulong guildId, ulong commandId) => - await this.ApiClient.DeleteGuildApplicationCommandAsync(this.CurrentApplication.Id, guildId, commandId); - - /// - /// Returns a list of guilds before a certain guild. This will execute one API request per 200 guilds. - /// The amount of guilds to fetch. - /// The ID of the guild before which we fetch the guilds - /// Whether to include approximate member and presence counts in the returned guilds. - /// Cancels the enumeration before doing the next api request - /// - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public IAsyncEnumerable GetGuildsBeforeAsync(ulong before, int limit = 200, bool? withCount = null, CancellationToken cancellationToken = default) - => GetGuildsInternalAsync(limit, before, withCount: withCount, cancellationToken: cancellationToken); - - /// - /// Returns a list of guilds after a certain guild. This will execute one API request per 200 guilds. - /// The amount of guilds to fetch. - /// The ID of the guild after which we fetch the guilds. - /// Whether to include approximate member and presence counts in the returned guilds. - /// Cancels the enumeration before doing the next api request - /// - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public IAsyncEnumerable GetGuildsAfterAsync(ulong after, int limit = 200, bool? withCount = null, CancellationToken cancellationToken = default) - => GetGuildsInternalAsync(limit, after: after, withCount: withCount, cancellationToken: cancellationToken); - - /// - /// Returns a list of guilds the bot is in. This will execute one API request per 200 guilds. - /// The amount of guilds to fetch. - /// Whether to include approximate member and presence counts in the returned guilds. - /// Cancels the enumeration before doing the next api request - /// - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public IAsyncEnumerable GetGuildsAsync(int limit = 200, bool? withCount = null, CancellationToken cancellationToken = default) => - GetGuildsInternalAsync(limit, withCount: withCount, cancellationToken: cancellationToken); - - /// - /// Creates a new emoji owned by the current application. - /// - /// The name of the emoji. - /// The image of the emoji. - /// The created emoji. - public async ValueTask CreateApplicationEmojiAsync(string name, Stream image) - { - if (string.IsNullOrWhiteSpace(name)) - { - throw new ArgumentNullException(nameof(name)); - } - - name = name.Trim(); - if (name.Length is < 2 or > 50) - { - throw new ArgumentException("Emoji name needs to be between 2 and 50 characters long."); - } - - ArgumentNullException.ThrowIfNull(image); - - string? image64 = null; - - using (InlineMediaTool imgtool = new(image)) - { - image64 = imgtool.GetBase64(); - } - - return await this.ApiClient.CreateApplicationEmojiAsync(this.CurrentApplication.Id, name, image64); - } - - /// - /// Gets an emoji owned by the current application. - /// - /// The ID of the emoji - /// Whether to skip the cache. - /// The emoji. - public async ValueTask GetApplicationEmojiAsync(ulong emojiId, bool skipCache = false) - { - if (!skipCache && this.CurrentApplication.ApplicationEmojis.TryGetValue(emojiId, out DiscordEmoji? emoji)) - { - return emoji; - } - - DiscordEmoji result = await this.ApiClient.GetApplicationEmojiAsync(this.CurrentApplication.Id, emojiId); - - this.CurrentApplication.ApplicationEmojis[emojiId] = result; - - return result; - } - - /// - /// Gets all emojis created or owned by the current application. - /// - /// All emojis associated with the current application. - /// This includes emojis uploaded by the owner or members of the team the application is on, if applicable. - public async ValueTask> GetApplicationEmojisAsync() - { - IReadOnlyList result = await this.ApiClient.GetApplicationEmojisAsync(this.CurrentApplication.Id); - - foreach (DiscordEmoji emoji in result) - { - this.CurrentApplication.ApplicationEmojis[emoji.Id] = emoji; - } - - return result; - } - - /// - /// Modifies an existing application emoji. - /// - /// The ID of the emoji. - /// The new name of the emoji. - /// The updated emoji. - public async ValueTask ModifyApplicationEmojiAsync(ulong emojiId, string name) - => await this.ApiClient.ModifyApplicationEmojiAsync(this.CurrentApplication.Id, emojiId, name); - - /// - /// Deletes an emoji. - /// - /// The ID of the emoji to delete. - public async ValueTask DeleteApplicationEmojiAsync(ulong emojiId) - => await this.ApiClient.DeleteApplicationEmojiAsync(this.CurrentApplication.Id, emojiId); - - private async IAsyncEnumerable GetGuildsInternalAsync - ( - int limit = 200, - ulong? before = null, - ulong? after = null, - bool? withCount = null, - [EnumeratorCancellation] - CancellationToken cancellationToken = default - ) - { - if (limit < 0) - { - throw new ArgumentException("Cannot get a negative number of guilds."); - } - - if (limit == 0) - { - yield break; - } - - int remaining = limit; - ulong? last = null; - bool isbefore = before != null; - - int lastCount; - do - { - if (cancellationToken.IsCancellationRequested) - { - yield break; - } - - int fetchSize = remaining > 200 ? 200 : remaining; - IReadOnlyList fetchedGuilds = await this.ApiClient.GetGuildsAsync(fetchSize, isbefore ? last ?? before : null, !isbefore ? last ?? after : null, withCount); - - lastCount = fetchedGuilds.Count; - remaining -= lastCount; - - //We sort the returned guilds by ID so that they are in order in case Discord switches the order AGAIN. - DiscordGuild[] sortedGuildsArray = [.. fetchedGuilds]; - Array.Sort(sortedGuildsArray, (x, y) => x.Id.CompareTo(y.Id)); - - if (!isbefore) - { - foreach (DiscordGuild guild in sortedGuildsArray) - { - yield return guild; - } - last = sortedGuildsArray.LastOrDefault()?.Id; - } - else - { - for (int i = sortedGuildsArray.Length - 1; i >= 0; i--) - { - yield return sortedGuildsArray[i]; - } - last = sortedGuildsArray.FirstOrDefault()?.Id; - } - } - while (remaining > 0 && lastCount is > 0 and 100); - } - #endregion - - [StackTraceHidden] - internal ChannelReader RegisterGuildMemberChunksEnumerator(ulong guildId, string? nonce) - { - Int128 nonceKey = new(guildId, (ulong)(nonce?.GetHashCode() ?? 0)); - Channel channel = Channel.CreateUnbounded(new UnboundedChannelOptions { SingleWriter = true, SingleReader = true }); - - if (!this.guildMembersChunkedEvents.TryAdd(nonceKey, channel)) - { - throw new InvalidOperationException("A guild member chunk request for the given guild and nonce has already been registered."); - } - - return channel.Reader; - } - - private async ValueTask DispatchGuildMembersChunkForIteratorsAsync(GuildMembersChunkedEventArgs eventArgs) - { - if (this.guildMembersChunkedEvents.Count is 0) - { - return; - } - - Int128 code = new(eventArgs.Guild.Id, (ulong)(eventArgs.Nonce?.GetHashCode() ?? 0)); - - if (!this.guildMembersChunkedEvents.TryGetValue(code, out Channel? eventChannel)) - { - return; - } - - await eventChannel.Writer.WriteAsync(eventArgs); - - // Discord docs state that 0 <= chunk_index < chunk_count, so add one - // Basically, chunks are zero-based. - if (eventArgs.ChunkIndex + 1 == eventArgs.ChunkCount) - { - this.guildMembersChunkedEvents.Remove(code, out _); - eventChannel.Writer.Complete(); - } - } - - #region Internal Caching Methods - - internal DiscordThreadChannel? InternalGetCachedThread(ulong threadId) - { - foreach (DiscordGuild guild in this.Guilds.Values) - { - if (guild.Threads.TryGetValue(threadId, out DiscordThreadChannel? foundThread)) - { - return foundThread; - } - } - - return null; - } - - internal DiscordThreadChannel? InternalGetCachedThread(ulong threadId, ulong? guildId) - { - if (!guildId.HasValue) - { - return InternalGetCachedThread(threadId); - } - - if (this.guilds.TryGetValue(guildId.Value, out DiscordGuild? guild)) - { - return guild.Threads.GetValueOrDefault(threadId); - } - - return null; - } - - internal DiscordChannel InternalGetCachedChannel(ulong channelId) - { - if (this.privateChannels?.TryGetValue(channelId, out DiscordDmChannel? foundDmChannel) == true) - { - return foundDmChannel; - } - - foreach (DiscordGuild guild in this.Guilds.Values) - { - if (guild.Channels.TryGetValue(channelId, out DiscordChannel? foundChannel)) - { - return foundChannel; - } - } - - return null; - } - - internal DiscordChannel? InternalGetCachedChannel(ulong channelId, ulong? guildId) - { - if (guildId is not ulong nonNullGuildID) - { - return this.privateChannels.GetValueOrDefault(channelId) ?? InternalGetCachedChannel(channelId); - } - - if (this.guilds.TryGetValue(nonNullGuildID, out DiscordGuild? guild)) - { - return guild.Channels.GetValueOrDefault(channelId); - } - - return InternalGetCachedChannel(channelId); - } - - internal DiscordGuild InternalGetCachedGuild(ulong? guildId) - { - if (this.guilds != null && guildId.HasValue) - { - if (this.guilds.TryGetValue(guildId.Value, out DiscordGuild? guild)) - { - return guild; - } - } - - return null; - } - - private void UpdateMessage(DiscordMessage message, TransportUser author, DiscordGuild guild, TransportMember member) - { - if (author != null) - { - DiscordUser usr = new(author) { Discord = this }; - - if (member != null) - { - member.User = author; - } - - message.Author = UpdateUser(usr, guild?.Id, guild, member); - } - - DiscordChannel? channel = InternalGetCachedChannel(message.ChannelId, message.guildId) ?? InternalGetCachedThread(message.ChannelId, message.guildId); - - if (channel != null) - { - return; - } - - channel = !message.guildId.HasValue - ? new DiscordDmChannel - { - Id = message.ChannelId, - Discord = this, - Type = DiscordChannelType.Private, - Recipients = [message.Author] - } - : new DiscordChannel - { - Id = message.ChannelId, - GuildId = guild.Id, - Discord = this - }; - - UpdateChannelCache(channel); - - message.Channel = channel; - } - - 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 }; - - UpdateUserCache(usr); - - usr = new DiscordMember(mbr) { Discord = this, guild_id = guildId.Value }; - } - - DiscordIntents intents = this.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?.members.TryGetValue(usr.Id, out member) == false) - { - if (intents.HasIntent(DiscordIntents.GuildMembers) || this.Configuration.AlwaysCacheMembers) // member can be updated by events, so cache it - { - guild.members.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?.members.TryUpdate(usr.Id, (DiscordMember)usr, member); - } - } - } - } - else if (usr.Username != null) // check if not a skeleton user - { - UpdateUserCache(usr); - } - - return usr; - } - - private void UpdateCachedGuild(DiscordGuild newGuild, JArray rawMembers) - { - if (this.disposed) - { - return; - } - - if (!this.guilds.TryGetValue(newGuild.Id, out DiscordGuild guild)) - { - guild = newGuild; - this.guilds[newGuild.Id] = guild; - } - - if (newGuild.channels != null && !newGuild.channels.IsEmpty) - { - foreach (DiscordChannel channel in newGuild.channels.Values) - { - if (guild.channels.TryGetValue(channel.Id, out _)) - { - continue; - } - - foreach (DiscordOverwrite overwrite in channel.permissionOverwrites) - { - overwrite.Discord = this; - overwrite.channelId = channel.Id; - } - - guild.channels[channel.Id] = channel; - } - } - if (newGuild.threads != null && !newGuild.threads.IsEmpty) - { - foreach (DiscordThreadChannel thread in newGuild.threads.Values) - { - if (guild.threads.TryGetValue(thread.Id, out _)) - { - continue; - } - - guild.threads[thread.Id] = thread; - } - } - - foreach (DiscordEmoji newEmoji in newGuild.emojis.Values) - { - _ = guild.emojis.GetOrAdd(newEmoji.Id, _ => newEmoji); - } - - foreach (DiscordMessageSticker newSticker in newGuild.stickers.Values) - { - _ = guild.stickers.GetOrAdd(newSticker.Id, _ => newSticker); - } - - if (rawMembers != null) - { - guild.members.Clear(); - - foreach (JToken xj in rawMembers) - { - TransportMember xtm = xj.ToDiscordObject(); - - DiscordUser xu = new(xtm.User) { Discord = this }; - UpdateUserCache(xu); - - guild.members[xtm.User.Id] = new DiscordMember(xtm) { Discord = this, guild_id = guild.Id }; - } - } - - foreach (DiscordRole role in newGuild.roles.Values) - { - if (guild.roles.TryGetValue(role.Id, out _)) - { - continue; - } - - role.guild_id = guild.Id; - guild.roles[role.Id] = role; - } - - if (newGuild.stageInstances != null) - { - foreach (DiscordStageInstance newStageInstance in newGuild.stageInstances.Values) - { - _ = guild.stageInstances.GetOrAdd(newStageInstance.Id, _ => newStageInstance); - } - } - - guild.Name = newGuild.Name; - guild.AfkChannelId = newGuild.AfkChannelId; - guild.AfkTimeout = newGuild.AfkTimeout; - guild.DefaultMessageNotifications = newGuild.DefaultMessageNotifications; - guild.Features = newGuild.Features; - 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.Banner = newGuild.Banner; - guild.Description = newGuild.Description; - guild.VanityUrlCode = newGuild.VanityUrlCode; - guild.Banner = newGuild.Banner; - 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.PremiumProgressBarEnabled = newGuild.PremiumProgressBarEnabled; - - // 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; - } - - private void PopulateMessageReactionsAndCache(DiscordMessage message, TransportUser author, TransportMember member) - { - DiscordGuild guild = message.Channel?.Guild ?? InternalGetCachedGuild(message.guildId); - UpdateMessage(message, author, guild, member); - message.reactions ??= []; - - foreach (DiscordReaction xr in message.reactions) - { - xr.Emoji.Discord = this; - } - - if (message.Channel is not null) - { - this.MessageCache?.Add(message); - } - } - - // Ensures the channel is cached: - // - DM -> _privateChannels dict on DiscordClient - // - Thread -> DiscordGuild#_threads - // - _ -> DiscordGuild#_channels - private void UpdateChannelCache(DiscordChannel? channel) - { - if (channel is null) - { - return; - } - - switch (channel) - { - case DiscordDmChannel dmChannel: - this.privateChannels.TryAdd(channel.Id, dmChannel); - break; - case DiscordThreadChannel threadChannel: - if (this.guilds.TryGetValue(channel.GuildId!.Value, out DiscordGuild? guild)) - { - guild.threads.TryAdd(channel.Id, threadChannel); - } - break; - default: - if (this.guilds.TryGetValue(channel.GuildId!.Value, out guild)) - { - guild.channels.TryAdd(channel.Id, channel); - } - break; - } - } - - #endregion - - #region Disposal - - private bool disposed; - - /// - /// Disposes your DiscordClient. - /// - public override void Dispose() - { - if (this.disposed) - { - return; - } - - this.disposed = true; - - DisconnectAsync().GetAwaiter().GetResult(); - this.ApiClient?.rest?.Dispose(); - this.CurrentUser = null!; - - this.guilds = null!; - this.privateChannels = null!; - } - - #endregion -} +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; + +using DSharpPlus.Clients; +using DSharpPlus.Entities; +using DSharpPlus.EventArgs; +using DSharpPlus.Exceptions; +using DSharpPlus.Net; +using DSharpPlus.Net.Abstractions; +using DSharpPlus.Net.Gateway; +using DSharpPlus.Net.InboundWebhooks; +using DSharpPlus.Net.Models; +using DSharpPlus.Net.Serialization; +using DSharpPlus.Net.WebSocket; + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +using Newtonsoft.Json.Linq; + +namespace DSharpPlus; + +/// +/// A Discord API wrapper. +/// +public sealed partial class DiscordClient : BaseDiscordClient +{ + internal static readonly DateTimeOffset discordEpoch = new(2015, 1, 1, 0, 0, 0, TimeSpan.Zero); + private static readonly ConcurrentDictionary socketLocks = []; + + internal bool isShard = false; + internal IMessageCacheProvider? MessageCache { get; } + private readonly IClientErrorHandler errorHandler; + private readonly IShardOrchestrator orchestrator; + private readonly ChannelReader eventReader; + private readonly ChannelReader webhookEventReader; + private readonly ChannelReader interactionEventReader; + private readonly IEventDispatcher dispatcher; + + private readonly ConcurrentDictionary> guildMembersChunkedEvents = []; + + private StatusUpdate? status = null; + private readonly string token; + + private readonly ManualResetEventSlim connectionLock = new(true); + + /// + /// Gets the service provider used within this Discord application. + /// + public IServiceProvider ServiceProvider { get; internal set; } + + /// + /// Gets whether this client is connected to the gateway. + /// + public bool AllShardsConnected + => this.orchestrator.AllShardsConnected; + + /// + /// Gets a dictionary of DM channels that have been cached by this client. The dictionary's key is the channel + /// ID. + /// + public IReadOnlyDictionary PrivateChannels => this.privateChannels; + internal ConcurrentDictionary privateChannels = new(); + + /// + /// 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 + /// GuildAvailable or GuildDownloadCompleted events haven't been fired yet) + /// + public override IReadOnlyDictionary Guilds => this.guilds; + internal ConcurrentDictionary guilds = new(); + + /// + /// Gets the latency in the connection to a specific guild. + /// + public TimeSpan GetConnectionLatency(ulong guildId) + => this.orchestrator.GetConnectionLatency(guildId); + + /// + /// Gets the collection of presences held by this client. + /// + public IReadOnlyDictionary Presences + => this.presences; + + internal Dictionary presences = []; + + [ActivatorUtilitiesConstructor] + public DiscordClient + ( + ILogger logger, + DiscordApiClient apiClient, + IMessageCacheProvider messageCacheProvider, + IServiceProvider serviceProvider, + IEventDispatcher eventDispatcher, + IClientErrorHandler errorHandler, + IOptions configuration, + IOptions token, + IShardOrchestrator shardOrchestrator, + IOptions gatewayOptions, + + [FromKeyedServices("DSharpPlus.Gateway.EventChannel")] + Channel eventChannel, + + [FromKeyedServices("DSharpPlus.Webhooks.EventChannel")] + Channel webhookEventChannel, + + [FromKeyedServices("DSharpPlus.Interactions.EventChannel")] + Channel interactionEventChannel + ) + : base() + { + this.Logger = logger; + this.MessageCache = messageCacheProvider; + this.ServiceProvider = serviceProvider; + this.ApiClient = apiClient; + this.errorHandler = errorHandler; + this.Configuration = configuration.Value; + this.token = token.Value.GetToken(); + this.orchestrator = shardOrchestrator; + this.eventReader = eventChannel.Reader; + this.dispatcher = eventDispatcher; + this.webhookEventReader = webhookEventChannel.Reader; + this.interactionEventReader = interactionEventChannel.Reader; + + this.ApiClient.SetClient(this); + this.Intents = gatewayOptions.Value.Intents; + + this.guilds.Clear(); + } + + #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, DiscordUserStatus? status = null, DateTimeOffset? idlesince = null) + { + // method checks if its already initialized + await InitializeAsync(); + + // Check if connection lock is already set, and set it if it isn't + if (!this.connectionLock.Wait(0)) + { + throw new InvalidOperationException("This client is already connected."); + } + + this.connectionLock.Set(); + + if (activity == null && status == null && idlesince == null) + { + this.status = null; + } + else + { + long? since_unix = idlesince != null ? Utilities.GetUnixTime(idlesince.Value) : null; + this.status = new StatusUpdate() + { + Activity = new TransportActivity(activity), + Status = status ?? DiscordUserStatus.Online, + IdleSince = since_unix, + IsAFK = idlesince != null, + activity = activity + }; + } + + this.Logger.LogInformation(LoggerEvents.Startup, "DSharpPlus; version {Version}", this.VersionString); + + await this.dispatcher.DispatchAsync(this, new ClientStartedEventArgs()); + + _ = ReceiveGatewayEventsAsync(); + _ = ReceiveWebhookEventsAsync(); + _ = ReceiveInteractionEventsAsync(); + + await this.orchestrator.StartAsync(activity, status, idlesince); + } + + /// + /// Sends a raw payload to the gateway. This method is not recommended for use unless you know what you're doing. + /// + /// The opcode to send to the Discord gateway. + /// The data to serialize. + /// The guild this payload originates from. Pass 0 for shard-independent payloads. + /// + /// This method should not be used unless you know what you're doing. Instead, look towards the other + /// explicitly implemented methods which come with client-side validation. + /// + /// A task representing the payload being sent. + [Experimental("DSP0004")] + public async Task SendPayloadAsync(GatewayOpCode opCode, object? data, ulong guildId) + { + GatewayPayload payload = new() + { + OpCode = opCode, + Data = data + }; + + string payloadString = DiscordJson.SerializeObject(payload); + await this.orchestrator.SendOutboundEventAsync(Encoding.UTF8.GetBytes(payloadString), guildId); + } + + /// + /// Reconnects all shards to the gateway. + /// + public async Task ReconnectAsync() + => await this.orchestrator.ReconnectAsync(); + + /// + /// Disconnects from the gateway + /// + /// + public async Task DisconnectAsync() + { + await this.orchestrator.StopAsync(); + await this.dispatcher.DispatchAsync(this, new ClientStoppedEventArgs()); + } + + #endregion + + #region Public REST Methods + + /// + /// Gets a sticker. + /// + /// The ID of the sticker. + /// The specified sticker + public async Task GetStickerAsync(ulong stickerId) + => await this.ApiClient.GetStickerAsync(stickerId); + + /// + /// Gets a collection of sticker packs that may be used by nitro users. + /// + /// + public async Task> GetStickerPacksAsync() + => await this.ApiClient.GetStickerPacksAsync(); + + /// + /// Gets a user + /// + /// ID of the user + /// Whether to always make a REST request and update cache. Passing true will update the user, updating stale properties such as . + /// + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task GetUserAsync(ulong userId, bool updateCache = false) + { + if (!updateCache && TryGetCachedUserInternal(userId, out DiscordUser? usr)) + { + return usr; + } + + usr = await this.ApiClient.GetUserAsync(userId); + + // See BaseDiscordClient.UpdateUser for why this is done like this. + this.UserCache.AddOrUpdate(userId, usr, (_, _) => usr); + + return usr; + } + + /// + /// Gets a channel + /// + /// The ID of the channel to get. + /// + /// 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) + => InternalGetCachedThread(id) ?? InternalGetCachedChannel(id) ?? await this.ApiClient.GetChannelAsync(id); + + /// + /// Sends a message + /// + /// Channel to send to. + /// Message content to send. + /// The Discord 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 async Task SendMessageAsync(DiscordChannel channel, string content) + => await this.ApiClient.CreateMessageAsync(channel.Id, content, embeds: null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false, suppressNotifications: false); + + /// + /// Sends a message + /// + /// Channel to send to. + /// Embed to attach to the message. + /// The Discord 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 async Task SendMessageAsync(DiscordChannel channel, DiscordEmbed embed) + => await this.ApiClient.CreateMessageAsync(channel.Id, null, embed != null ? new[] { embed } : null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false, suppressNotifications: false); + + /// + /// Sends a message + /// + /// Channel to send to. + /// Message content to send. + /// Embed to attach to the message. + /// The Discord 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 async Task SendMessageAsync(DiscordChannel channel, string content, DiscordEmbed embed) + => await this.ApiClient.CreateMessageAsync(channel.Id, content, embed != null ? new[] { embed } : null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false, suppressNotifications: false); + + /// + /// Sends a message + /// + /// Channel to send to. + /// The Discord Message builder. + /// The Discord 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 async Task SendMessageAsync(DiscordChannel channel, DiscordMessageBuilder builder) + => await this.ApiClient.CreateMessageAsync(channel.Id, builder); + + /// + /// Sends a message + /// + /// Channel to send to. + /// The Discord Message builder. + /// The Discord 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 async Task SendMessageAsync(DiscordChannel channel, Action action) + { + DiscordMessageBuilder builder = new(); + action(builder); + + return await 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 async Task CreateGuildAsync + ( + string name, + string? region = null, + Optional icon = default, + DiscordVerificationLevel? verificationLevel = null, + DiscordDefaultMessageNotifications? defaultMessageNotifications = null, + DiscordSystemChannelFlags? systemChannelFlags = null + ) + { + Optional iconb64 = Optional.FromNoValue(); + + if (icon.HasValue && icon.Value != null) + { + using InlineMediaTool imgtool = new(icon.Value); + iconb64 = imgtool.GetBase64(); + } + else if (icon.HasValue) + { + iconb64 = null; + } + + return await 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 async Task CreateGuildFromTemplateAsync(string code, string name, Optional icon = default) + { + Optional iconb64 = Optional.FromNoValue(); + + if (icon.HasValue && icon.Value != null) + { + using InlineMediaTool imgtool = new(icon.Value); + iconb64 = imgtool.GetBase64(); + } + else if (icon.HasValue) + { + iconb64 = null; + } + + return await this.ApiClient.CreateGuildFromTemplateAsync(code, name, iconb64); + } + + /// + /// 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.guilds.TryGetValue(id, out DiscordGuild? guild) && (!withCounts.HasValue || !withCounts.Value)) + { + return guild; + } + + guild = await this.ApiClient.GetGuildAsync(id, withCounts); + IReadOnlyList channels = await this.ApiClient.GetGuildChannelsAsync(guild.Id); + foreach (DiscordChannel channel in channels) + { + guild.channels[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 async Task GetGuildPreviewAsync(ulong id) + => await 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 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 async Task GetInviteByCodeAsync(string code, bool? withCounts = null, bool? withExpiration = null) + => await this.ApiClient.GetInviteAsync(code, withCounts, withExpiration); + + /// + /// Gets a list of connections + /// + /// + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task> GetConnectionsAsync() + => await this.ApiClient.GetUsersConnectionsAsync(); + + /// + /// Gets a webhook + /// + /// The ID of webhook to get. + /// + /// Thrown when the webhook does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task GetWebhookAsync(ulong id) + => await this.ApiClient.GetWebhookAsync(id); + + /// + /// Gets a webhook + /// + /// The ID of webhook to get. + /// + /// + /// Thrown when the webhook does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task GetWebhookWithTokenAsync(ulong id, string token) + => await 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. + /// + /// The ID of the shard whose status should be updated. Defaults to null, which will update all shards controlled by + /// this DiscordClient. + /// + public async Task UpdateStatusAsync(DiscordActivity activity = null, DiscordUserStatus? userStatus = null, DateTimeOffset? idleSince = null, int? shardId = null) + { + StatusUpdate update = new() + { + Activity = new(activity), + IdleSince = idleSince?.ToUnixTimeMilliseconds() + }; + + if (userStatus is not null) + { + update.Status = userStatus.Value; + } + + GatewayPayload gatewayPayload = new() {OpCode = GatewayOpCode.StatusUpdate, Data = update}; + + string payload = DiscordJson.SerializeObject(gatewayPayload); + + if (shardId is null) + { + await this.orchestrator.BroadcastOutboundEventAsync(Encoding.UTF8.GetBytes(payload)); + } + else + { + // this is a bit of a hack. x % n, for any x < n, will always return x, so we can just pass the shard ID + // as guild ID. this won't be very graceful if you pass an invalid ID, though. + await this.orchestrator.SendOutboundEventAsync(Encoding.UTF8.GetBytes(payload), (ulong)shardId.Value); + } + } + + /// + /// Edits current user. + /// + /// New username. + /// New avatar. + /// New banner. + /// + /// 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 ModifyCurrentUserAsync(string username = null, Optional avatar = default, Optional banner = default) + { + Optional avatarBase64 = Optional.FromNoValue(); + if (avatar.HasValue && avatar.Value != null) + { + using InlineMediaTool imgtool = new(avatar.Value); + avatarBase64 = imgtool.GetBase64(); + } + else if (avatar.HasValue) + { + avatarBase64 = null; + } + + Optional bannerBase64 = Optional.FromNoValue(); + if (banner.HasValue && banner.Value != null) + { + using InlineMediaTool imgtool = new(banner.Value); + bannerBase64 = imgtool.GetBase64(); + } + else if (banner.HasValue) + { + bannerBase64 = null; + } + + TransportUser usr = await this.ApiClient.ModifyCurrentUserAsync(username, avatarBase64, bannerBase64); + + 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 async Task GetTemplateAsync(string code) + => await this.ApiClient.GetTemplateAsync(code); + + /// + /// Gets all the global application commands for this application. + /// + /// Whether to include localizations in the response. + /// A list of global application commands. + public async Task> GetGlobalApplicationCommandsAsync(bool withLocalizations = false) => + await this.ApiClient.GetGlobalApplicationCommandsAsync(this.CurrentApplication.Id, withLocalizations); + + /// + /// Overwrites the existing global application commands. New commands are automatically created and missing commands are automatically deleted. + /// + /// The list of commands to overwrite with. + /// The list of global commands. + public async Task> BulkOverwriteGlobalApplicationCommandsAsync(IEnumerable commands) => + await this.ApiClient.BulkOverwriteGlobalApplicationCommandsAsync(this.CurrentApplication.Id, commands); + + /// + /// Creates or overwrites a global application command. + /// + /// The command to create. + /// The created command. + public async Task CreateGlobalApplicationCommandAsync(DiscordApplicationCommand command) => + await 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 async Task GetGlobalApplicationCommandAsync(ulong commandId) => + await this.ApiClient.GetGlobalApplicationCommandAsync(this.CurrentApplication.Id, commandId); + + /// + /// Gets a global application command by its name. + /// + /// The name of the command to get. + /// Whether to include localizations in the response. + /// The command with the name. + public async Task GetGlobalApplicationCommandAsync(string commandName, bool withLocalizations = false) + { + foreach (DiscordApplicationCommand command in await this.ApiClient.GetGlobalApplicationCommandsAsync(this.CurrentApplication.Id, withLocalizations)) + { + if (command.Name == commandName) + { + return command; + } + } + + return null; + } + + /// + /// 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) + { + ApplicationCommandEditModel mdl = new(); + action(mdl); + + ulong applicationId = this.CurrentApplication?.Id ?? (await GetCurrentApplicationAsync()).Id; + + return await this.ApiClient.EditGlobalApplicationCommandAsync + ( + applicationId, + commandId, + mdl.Name, + mdl.Description, + mdl.Options, + mdl.DefaultPermission, + mdl.NSFW, + mdl.NameLocalizations, + mdl.DescriptionLocalizations, + mdl.AllowDMUsage, + mdl.DefaultMemberPermissions, + mdl.AllowedContexts, + mdl.IntegrationTypes + ); + } + + /// + /// Deletes a global application command. + /// + /// The ID of the command to delete. + public async Task DeleteGlobalApplicationCommandAsync(ulong commandId) => + await this.ApiClient.DeleteGlobalApplicationCommandAsync(this.CurrentApplication.Id, commandId); + + /// + /// Gets all the application commands for a guild. + /// + /// The ID of the guild to get application commands for. + /// Whether to include localizations in the response. + /// A list of application commands in the guild. + public async Task> GetGuildApplicationCommandsAsync(ulong guildId, bool withLocalizations = false) => + await this.ApiClient.GetGuildApplicationCommandsAsync(this.CurrentApplication.Id, guildId, withLocalizations); + + /// + /// Overwrites the existing application commands in a guild. New commands are automatically created and missing commands are automatically deleted. + /// + /// The ID of the guild. + /// The list of commands to overwrite with. + /// The list of guild commands. + public async Task> BulkOverwriteGuildApplicationCommandsAsync(ulong guildId, IEnumerable commands) => + await 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 async Task CreateGuildApplicationCommandAsync(ulong guildId, DiscordApplicationCommand command) => + await 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 async Task GetGuildApplicationCommandAsync(ulong guildId, ulong commandId) => + await 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) + { + ApplicationCommandEditModel mdl = new(); + action(mdl); + + ulong applicationId = this.CurrentApplication?.Id ?? (await GetCurrentApplicationAsync()).Id; + + return await this.ApiClient.EditGuildApplicationCommandAsync + ( + applicationId, + guildId, + commandId, + mdl.Name, + mdl.Description, + mdl.Options, + mdl.DefaultPermission, + mdl.NSFW, + mdl.NameLocalizations, + mdl.DescriptionLocalizations, + mdl.AllowDMUsage, + mdl.DefaultMemberPermissions, + mdl.AllowedContexts, + mdl.IntegrationTypes + ); + } + + /// + /// Deletes a application command in a guild. + /// + /// The ID of the guild to delete the application command in. + /// The ID of the command. + public async Task DeleteGuildApplicationCommandAsync(ulong guildId, ulong commandId) => + await this.ApiClient.DeleteGuildApplicationCommandAsync(this.CurrentApplication.Id, guildId, commandId); + + /// + /// Returns a list of guilds before a certain guild. This will execute one API request per 200 guilds. + /// The amount of guilds to fetch. + /// The ID of the guild before which we fetch the guilds + /// Whether to include approximate member and presence counts in the returned guilds. + /// Cancels the enumeration before doing the next api request + /// + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public IAsyncEnumerable GetGuildsBeforeAsync(ulong before, int limit = 200, bool? withCount = null, CancellationToken cancellationToken = default) + => GetGuildsInternalAsync(limit, before, withCount: withCount, cancellationToken: cancellationToken); + + /// + /// Returns a list of guilds after a certain guild. This will execute one API request per 200 guilds. + /// The amount of guilds to fetch. + /// The ID of the guild after which we fetch the guilds. + /// Whether to include approximate member and presence counts in the returned guilds. + /// Cancels the enumeration before doing the next api request + /// + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public IAsyncEnumerable GetGuildsAfterAsync(ulong after, int limit = 200, bool? withCount = null, CancellationToken cancellationToken = default) + => GetGuildsInternalAsync(limit, after: after, withCount: withCount, cancellationToken: cancellationToken); + + /// + /// Returns a list of guilds the bot is in. This will execute one API request per 200 guilds. + /// The amount of guilds to fetch. + /// Whether to include approximate member and presence counts in the returned guilds. + /// Cancels the enumeration before doing the next api request + /// + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public IAsyncEnumerable GetGuildsAsync(int limit = 200, bool? withCount = null, CancellationToken cancellationToken = default) => + GetGuildsInternalAsync(limit, withCount: withCount, cancellationToken: cancellationToken); + + /// + /// Creates a new emoji owned by the current application. + /// + /// The name of the emoji. + /// The image of the emoji. + /// The created emoji. + public async ValueTask CreateApplicationEmojiAsync(string name, Stream image) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentNullException(nameof(name)); + } + + name = name.Trim(); + if (name.Length is < 2 or > 50) + { + throw new ArgumentException("Emoji name needs to be between 2 and 50 characters long."); + } + + ArgumentNullException.ThrowIfNull(image); + + string? image64 = null; + + using (InlineMediaTool imgtool = new(image)) + { + image64 = imgtool.GetBase64(); + } + + return await this.ApiClient.CreateApplicationEmojiAsync(this.CurrentApplication.Id, name, image64); + } + + /// + /// Gets an emoji owned by the current application. + /// + /// The ID of the emoji + /// Whether to skip the cache. + /// The emoji. + public async ValueTask GetApplicationEmojiAsync(ulong emojiId, bool skipCache = false) + { + if (!skipCache && this.CurrentApplication.ApplicationEmojis.TryGetValue(emojiId, out DiscordEmoji? emoji)) + { + return emoji; + } + + DiscordEmoji result = await this.ApiClient.GetApplicationEmojiAsync(this.CurrentApplication.Id, emojiId); + + this.CurrentApplication.ApplicationEmojis[emojiId] = result; + + return result; + } + + /// + /// Gets all emojis created or owned by the current application. + /// + /// All emojis associated with the current application. + /// This includes emojis uploaded by the owner or members of the team the application is on, if applicable. + public async ValueTask> GetApplicationEmojisAsync() + { + IReadOnlyList result = await this.ApiClient.GetApplicationEmojisAsync(this.CurrentApplication.Id); + + foreach (DiscordEmoji emoji in result) + { + this.CurrentApplication.ApplicationEmojis[emoji.Id] = emoji; + } + + return result; + } + + /// + /// Modifies an existing application emoji. + /// + /// The ID of the emoji. + /// The new name of the emoji. + /// The updated emoji. + public async ValueTask ModifyApplicationEmojiAsync(ulong emojiId, string name) + => await this.ApiClient.ModifyApplicationEmojiAsync(this.CurrentApplication.Id, emojiId, name); + + /// + /// Deletes an emoji. + /// + /// The ID of the emoji to delete. + public async ValueTask DeleteApplicationEmojiAsync(ulong emojiId) + => await this.ApiClient.DeleteApplicationEmojiAsync(this.CurrentApplication.Id, emojiId); + + private async IAsyncEnumerable GetGuildsInternalAsync + ( + int limit = 200, + ulong? before = null, + ulong? after = null, + bool? withCount = null, + [EnumeratorCancellation] + CancellationToken cancellationToken = default + ) + { + if (limit < 0) + { + throw new ArgumentException("Cannot get a negative number of guilds."); + } + + if (limit == 0) + { + yield break; + } + + int remaining = limit; + ulong? last = null; + bool isbefore = before != null; + + int lastCount; + do + { + if (cancellationToken.IsCancellationRequested) + { + yield break; + } + + int fetchSize = remaining > 200 ? 200 : remaining; + IReadOnlyList fetchedGuilds = await this.ApiClient.GetGuildsAsync(fetchSize, isbefore ? last ?? before : null, !isbefore ? last ?? after : null, withCount); + + lastCount = fetchedGuilds.Count; + remaining -= lastCount; + + //We sort the returned guilds by ID so that they are in order in case Discord switches the order AGAIN. + DiscordGuild[] sortedGuildsArray = [.. fetchedGuilds]; + Array.Sort(sortedGuildsArray, (x, y) => x.Id.CompareTo(y.Id)); + + if (!isbefore) + { + foreach (DiscordGuild guild in sortedGuildsArray) + { + yield return guild; + } + last = sortedGuildsArray.LastOrDefault()?.Id; + } + else + { + for (int i = sortedGuildsArray.Length - 1; i >= 0; i--) + { + yield return sortedGuildsArray[i]; + } + last = sortedGuildsArray.FirstOrDefault()?.Id; + } + } + while (remaining > 0 && lastCount is > 0 and 100); + } + #endregion + + [StackTraceHidden] + internal ChannelReader RegisterGuildMemberChunksEnumerator(ulong guildId, string? nonce) + { + Int128 nonceKey = new(guildId, (ulong)(nonce?.GetHashCode() ?? 0)); + Channel channel = Channel.CreateUnbounded(new UnboundedChannelOptions { SingleWriter = true, SingleReader = true }); + + if (!this.guildMembersChunkedEvents.TryAdd(nonceKey, channel)) + { + throw new InvalidOperationException("A guild member chunk request for the given guild and nonce has already been registered."); + } + + return channel.Reader; + } + + private async ValueTask DispatchGuildMembersChunkForIteratorsAsync(GuildMembersChunkedEventArgs eventArgs) + { + if (this.guildMembersChunkedEvents.Count is 0) + { + return; + } + + Int128 code = new(eventArgs.Guild.Id, (ulong)(eventArgs.Nonce?.GetHashCode() ?? 0)); + + if (!this.guildMembersChunkedEvents.TryGetValue(code, out Channel? eventChannel)) + { + return; + } + + await eventChannel.Writer.WriteAsync(eventArgs); + + // Discord docs state that 0 <= chunk_index < chunk_count, so add one + // Basically, chunks are zero-based. + if (eventArgs.ChunkIndex + 1 == eventArgs.ChunkCount) + { + this.guildMembersChunkedEvents.Remove(code, out _); + eventChannel.Writer.Complete(); + } + } + + #region Internal Caching Methods + + internal DiscordThreadChannel? InternalGetCachedThread(ulong threadId) + { + foreach (DiscordGuild guild in this.Guilds.Values) + { + if (guild.Threads.TryGetValue(threadId, out DiscordThreadChannel? foundThread)) + { + return foundThread; + } + } + + return null; + } + + internal DiscordThreadChannel? InternalGetCachedThread(ulong threadId, ulong? guildId) + { + if (!guildId.HasValue) + { + return InternalGetCachedThread(threadId); + } + + if (this.guilds.TryGetValue(guildId.Value, out DiscordGuild? guild)) + { + return guild.Threads.GetValueOrDefault(threadId); + } + + return null; + } + + internal DiscordChannel InternalGetCachedChannel(ulong channelId) + { + if (this.privateChannels?.TryGetValue(channelId, out DiscordDmChannel? foundDmChannel) == true) + { + return foundDmChannel; + } + + foreach (DiscordGuild guild in this.Guilds.Values) + { + if (guild.Channels.TryGetValue(channelId, out DiscordChannel? foundChannel)) + { + return foundChannel; + } + } + + return null; + } + + internal DiscordChannel? InternalGetCachedChannel(ulong channelId, ulong? guildId) + { + if (guildId is not ulong nonNullGuildID) + { + return this.privateChannels.GetValueOrDefault(channelId) ?? InternalGetCachedChannel(channelId); + } + + if (this.guilds.TryGetValue(nonNullGuildID, out DiscordGuild? guild)) + { + return guild.Channels.GetValueOrDefault(channelId); + } + + return InternalGetCachedChannel(channelId); + } + + internal DiscordGuild InternalGetCachedGuild(ulong? guildId) + { + if (this.guilds != null && guildId.HasValue) + { + if (this.guilds.TryGetValue(guildId.Value, out DiscordGuild? guild)) + { + return guild; + } + } + + return null; + } + + private void UpdateMessage(DiscordMessage message, TransportUser author, DiscordGuild guild, TransportMember member) + { + if (author != null) + { + DiscordUser usr = new(author) { Discord = this }; + + if (member != null) + { + member.User = author; + } + + message.Author = UpdateUser(usr, guild?.Id, guild, member); + } + + DiscordChannel? channel = InternalGetCachedChannel(message.ChannelId, message.guildId) ?? InternalGetCachedThread(message.ChannelId, message.guildId); + + if (channel != null) + { + return; + } + + channel = !message.guildId.HasValue + ? new DiscordDmChannel + { + Id = message.ChannelId, + Discord = this, + Type = DiscordChannelType.Private, + Recipients = [message.Author] + } + : new DiscordChannel + { + Id = message.ChannelId, + GuildId = guild.Id, + Discord = this + }; + + UpdateChannelCache(channel); + + message.Channel = channel; + } + + 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 }; + + UpdateUserCache(usr); + + usr = new DiscordMember(mbr) { Discord = this, guild_id = guildId.Value }; + } + + DiscordIntents intents = this.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?.members.TryGetValue(usr.Id, out member) == false) + { + if (intents.HasIntent(DiscordIntents.GuildMembers) || this.Configuration.AlwaysCacheMembers) // member can be updated by events, so cache it + { + guild.members.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?.members.TryUpdate(usr.Id, (DiscordMember)usr, member); + } + } + } + } + else if (usr.Username != null) // check if not a skeleton user + { + UpdateUserCache(usr); + } + + return usr; + } + + private void UpdateCachedGuild(DiscordGuild newGuild, JArray rawMembers) + { + if (this.disposed) + { + return; + } + + if (!this.guilds.TryGetValue(newGuild.Id, out DiscordGuild guild)) + { + guild = newGuild; + this.guilds[newGuild.Id] = guild; + } + + if (newGuild.channels != null && !newGuild.channels.IsEmpty) + { + foreach (DiscordChannel channel in newGuild.channels.Values) + { + if (guild.channels.TryGetValue(channel.Id, out _)) + { + continue; + } + + foreach (DiscordOverwrite overwrite in channel.permissionOverwrites) + { + overwrite.Discord = this; + overwrite.channelId = channel.Id; + } + + guild.channels[channel.Id] = channel; + } + } + if (newGuild.threads != null && !newGuild.threads.IsEmpty) + { + foreach (DiscordThreadChannel thread in newGuild.threads.Values) + { + if (guild.threads.TryGetValue(thread.Id, out _)) + { + continue; + } + + guild.threads[thread.Id] = thread; + } + } + + foreach (DiscordEmoji newEmoji in newGuild.emojis.Values) + { + _ = guild.emojis.GetOrAdd(newEmoji.Id, _ => newEmoji); + } + + foreach (DiscordMessageSticker newSticker in newGuild.stickers.Values) + { + _ = guild.stickers.GetOrAdd(newSticker.Id, _ => newSticker); + } + + if (rawMembers != null) + { + guild.members.Clear(); + + foreach (JToken xj in rawMembers) + { + TransportMember xtm = xj.ToDiscordObject(); + + DiscordUser xu = new(xtm.User) { Discord = this }; + UpdateUserCache(xu); + + guild.members[xtm.User.Id] = new DiscordMember(xtm) { Discord = this, guild_id = guild.Id }; + } + } + + foreach (DiscordRole role in newGuild.roles.Values) + { + if (guild.roles.TryGetValue(role.Id, out _)) + { + continue; + } + + role.guild_id = guild.Id; + guild.roles[role.Id] = role; + } + + if (newGuild.stageInstances != null) + { + foreach (DiscordStageInstance newStageInstance in newGuild.stageInstances.Values) + { + _ = guild.stageInstances.GetOrAdd(newStageInstance.Id, _ => newStageInstance); + } + } + + guild.Name = newGuild.Name; + guild.AfkChannelId = newGuild.AfkChannelId; + guild.AfkTimeout = newGuild.AfkTimeout; + guild.DefaultMessageNotifications = newGuild.DefaultMessageNotifications; + guild.Features = newGuild.Features; + 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.Banner = newGuild.Banner; + guild.Description = newGuild.Description; + guild.VanityUrlCode = newGuild.VanityUrlCode; + guild.Banner = newGuild.Banner; + 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.PremiumProgressBarEnabled = newGuild.PremiumProgressBarEnabled; + + // 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; + } + + private void PopulateMessageReactionsAndCache(DiscordMessage message, TransportUser author, TransportMember member) + { + DiscordGuild guild = message.Channel?.Guild ?? InternalGetCachedGuild(message.guildId); + UpdateMessage(message, author, guild, member); + message.reactions ??= []; + + foreach (DiscordReaction xr in message.reactions) + { + xr.Emoji.Discord = this; + } + + if (message.Channel is not null) + { + this.MessageCache?.Add(message); + } + } + + // Ensures the channel is cached: + // - DM -> _privateChannels dict on DiscordClient + // - Thread -> DiscordGuild#_threads + // - _ -> DiscordGuild#_channels + private void UpdateChannelCache(DiscordChannel? channel) + { + if (channel is null) + { + return; + } + + switch (channel) + { + case DiscordDmChannel dmChannel: + this.privateChannels.TryAdd(channel.Id, dmChannel); + break; + case DiscordThreadChannel threadChannel: + if (this.guilds.TryGetValue(channel.GuildId!.Value, out DiscordGuild? guild)) + { + guild.threads.TryAdd(channel.Id, threadChannel); + } + break; + default: + if (this.guilds.TryGetValue(channel.GuildId!.Value, out guild)) + { + guild.channels.TryAdd(channel.Id, channel); + } + break; + } + } + + #endregion + + #region Disposal + + private bool disposed; + + /// + /// Disposes your DiscordClient. + /// + public override void Dispose() + { + if (this.disposed) + { + return; + } + + this.disposed = true; + + DisconnectAsync().GetAwaiter().GetResult(); + this.ApiClient?.rest?.Dispose(); + this.CurrentUser = null!; + + this.guilds = null!; + this.privateChannels = null!; + } + + #endregion +} diff --git a/DSharpPlus/Clients/DiscordWebhookClient.cs b/DSharpPlus/Clients/DiscordWebhookClient.cs index b3fcc6ea4b..7023861071 100644 --- a/DSharpPlus/Clients/DiscordWebhookClient.cs +++ b/DSharpPlus/Clients/DiscordWebhookClient.cs @@ -1,275 +1,275 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Globalization; -using System.Linq; -using System.Net; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using DSharpPlus.Entities; -using DSharpPlus.Exceptions; -using DSharpPlus.Logging; -using DSharpPlus.Metrics; -using DSharpPlus.Net; -using Microsoft.Extensions.Logging; - -namespace DSharpPlus; - -/// -/// Represents a webhook-only client. This client can be used to execute Discord webhooks. -/// -public partial class DiscordWebhookClient -{ - /// - /// Gets the logger for this client. - /// - public ILogger Logger { get; } - - /// - /// Gets the collection of registered webhooks. - /// - public IReadOnlyList Webhooks { get; } - - /// - /// Gets or sets the username override for registered webhooks. Note that this only takes effect when broadcasting. - /// - public string Username { get; set; } - - /// - /// Gets or set the avatar override for registered webhooks. Note that this only takes effect when broadcasting. - /// - public string AvatarUrl { get; set; } - - internal List hooks; - internal DiscordApiClient apiclient; - - internal LogLevel minimumLogLevel; - internal string logTimestampFormat; - - /// - /// Creates a new webhook client. - /// - public DiscordWebhookClient() - : this(null, null) - { } - - /// - /// Creates a new webhook client, with specified HTTP proxy, timeout, and logging settings. - /// - /// Proxy to use for HTTP connections. - /// Timeout to use for HTTP requests. Set to to disable timeouts. - /// The optional logging factory to use for this client. - /// The minimum logging level for messages. - /// The timestamp format to use for the logger. - public DiscordWebhookClient(IWebProxy proxy = null, TimeSpan? timeout = null, - ILoggerFactory loggerFactory = null, LogLevel minimumLogLevel = LogLevel.Information, string logTimestampFormat = "yyyy-MM-dd HH:mm:ss zzz") - { - this.minimumLogLevel = minimumLogLevel; - this.logTimestampFormat = logTimestampFormat; - - if (loggerFactory == null) - { - loggerFactory = new DefaultLoggerFactory(); - loggerFactory.AddProvider(new DefaultLoggerProvider(minimumLogLevel)); - } - - this.Logger = loggerFactory.CreateLogger(); - - TimeSpan parsedTimeout = timeout ?? TimeSpan.FromSeconds(10); - - this.apiclient = new DiscordApiClient(parsedTimeout, this.Logger); - this.hooks = []; - this.Webhooks = new ReadOnlyCollection(this.hooks); - } - - /// - /// Registers a webhook with this client. This retrieves a webhook based on the ID and token supplied. - /// - /// The ID of the webhook to add. - /// The token of the webhook to add. - /// The registered webhook. - public async Task AddWebhookAsync(ulong id, string token) - { - if (string.IsNullOrWhiteSpace(token)) - { - throw new ArgumentNullException(nameof(token)); - } - - token = token.Trim(); - - if (this.hooks.Any(x => x.Id == id)) - { - throw new InvalidOperationException("This webhook is registered with this client."); - } - - DiscordWebhook wh = await this.apiclient.GetWebhookWithTokenAsync(id, token); - this.hooks.Add(wh); - - return wh; - } - - /// - public RequestMetricsCollection GetRequestMetrics(bool sinceLastCall = false) - => this.apiclient.GetRequestMetrics(sinceLastCall); - - /// - /// Registers a webhook with this client. This retrieves a webhook from webhook URL. - /// - /// URL of the webhook to retrieve. This URL must contain both ID and token. - /// The registered webhook. - public Task AddWebhookAsync(Uri url) - { - ArgumentNullException.ThrowIfNull(url); - Match m = GetWebhookRegex().Match(url.ToString()); - if (!m.Success) - { - throw new ArgumentException("Invalid webhook URL supplied.", nameof(url)); - } - - Group idraw = m.Groups["id"]; - Group tokenraw = m.Groups["token"]; - if (!ulong.TryParse(idraw.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out ulong id)) - { - throw new ArgumentException("Invalid webhook URL supplied.", nameof(url)); - } - - string token = tokenraw.Value; - return AddWebhookAsync(id, token); - } - - /// - /// Registers a webhook with this client. This retrieves a webhook using the supplied full discord client. - /// - /// ID of the webhook to register. - /// Discord client to which the webhook will belong. - /// The registered webhook. - public async Task AddWebhookAsync(ulong id, BaseDiscordClient client) - { - ArgumentNullException.ThrowIfNull(client); - if (this.hooks.Any(x => x.Id == id)) - { - throw new ArgumentException("This webhook is already registered with this client."); - } - - DiscordWebhook wh = await client.ApiClient.GetWebhookAsync(id); - // personally I don't think we need to override anything. - // it would even make sense to keep the hook as-is, in case - // it's returned without a token for some bizarre reason - // remember -- discord is not really consistent - //var nwh = new DiscordWebhook() - //{ - // ApiClient = apiclient, - // AvatarHash = wh.AvatarHash, - // ChannelId = wh.ChannelId, - // GuildId = wh.GuildId, - // Id = wh.Id, - // Name = wh.Name, - // Token = wh.Token, - // User = wh.User, - // Discord = null - //}; - this.hooks.Add(wh); - - return wh; - } - - /// - /// Registers a webhook with this client. This reuses the supplied webhook object. - /// - /// Webhook to register. - /// The registered webhook. - public DiscordWebhook AddWebhook(DiscordWebhook webhook) - { - ArgumentNullException.ThrowIfNull(webhook); - if (this.hooks.Any(x => x.Id == webhook.Id)) - { - throw new ArgumentException("This webhook is already registered with this client."); - } - - // see line 128-131 for explanation - // For christ's sake, update the line numbers if they change. - //var nwh = new DiscordWebhook() - //{ - // ApiClient = apiclient, - // AvatarHash = webhook.AvatarHash, - // ChannelId = webhook.ChannelId, - // GuildId = webhook.GuildId, - // Id = webhook.Id, - // Name = webhook.Name, - // Token = webhook.Token, - // User = webhook.User, - // Discord = null - //}; - this.hooks.Add(webhook); - - return webhook; - } - - /// - /// Unregisters a webhook with this client. - /// - /// ID of the webhook to unregister. - /// The unregistered webhook. - public DiscordWebhook RemoveWebhook(ulong id) - { - if (!this.hooks.Any(x => x.Id == id)) - { - throw new ArgumentException("This webhook is not registered with this client."); - } - - DiscordWebhook wh = GetRegisteredWebhook(id); - this.hooks.Remove(wh); - return wh; - } - - /// - /// Gets a registered webhook with specified ID. - /// - /// ID of the registered webhook to retrieve. - /// The requested webhook. - public DiscordWebhook GetRegisteredWebhook(ulong id) - => this.hooks.FirstOrDefault(xw => xw.Id == id); - - /// - /// Broadcasts a message to all registered webhooks. - /// - /// Webhook builder filled with data to send. - /// - public async Task> BroadcastMessageAsync(DiscordWebhookBuilder builder) - { - List deadhooks = []; - Dictionary messages = []; - - foreach (DiscordWebhook hook in this.hooks) - { - try - { - messages.Add(hook, await hook.ExecuteAsync(builder)); - } - catch (NotFoundException) - { - deadhooks.Add(hook); - } - } - - // Removing dead webhooks from collection - foreach (DiscordWebhook xwh in deadhooks) - { - this.hooks.Remove(xwh); - } - - return messages; - } - - ~DiscordWebhookClient() - { - this.hooks?.Clear(); - this.hooks = null!; - this.apiclient.rest.Dispose(); - } - - [GeneratedRegex(@"(?:https?:\/\/)?discord(?:app)?.com\/api\/(?:v\d\/)?webhooks\/(?\d+)\/(?[A-Za-z0-9_\-]+)", RegexOptions.ECMAScript)] - private static partial Regex GetWebhookRegex(); -} - -// 9/11 would improve again +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Globalization; +using System.Linq; +using System.Net; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using DSharpPlus.Entities; +using DSharpPlus.Exceptions; +using DSharpPlus.Logging; +using DSharpPlus.Metrics; +using DSharpPlus.Net; +using Microsoft.Extensions.Logging; + +namespace DSharpPlus; + +/// +/// Represents a webhook-only client. This client can be used to execute Discord webhooks. +/// +public partial class DiscordWebhookClient +{ + /// + /// Gets the logger for this client. + /// + public ILogger Logger { get; } + + /// + /// Gets the collection of registered webhooks. + /// + public IReadOnlyList Webhooks { get; } + + /// + /// Gets or sets the username override for registered webhooks. Note that this only takes effect when broadcasting. + /// + public string Username { get; set; } + + /// + /// Gets or set the avatar override for registered webhooks. Note that this only takes effect when broadcasting. + /// + public string AvatarUrl { get; set; } + + internal List hooks; + internal DiscordApiClient apiclient; + + internal LogLevel minimumLogLevel; + internal string logTimestampFormat; + + /// + /// Creates a new webhook client. + /// + public DiscordWebhookClient() + : this(null, null) + { } + + /// + /// Creates a new webhook client, with specified HTTP proxy, timeout, and logging settings. + /// + /// Proxy to use for HTTP connections. + /// Timeout to use for HTTP requests. Set to to disable timeouts. + /// The optional logging factory to use for this client. + /// The minimum logging level for messages. + /// The timestamp format to use for the logger. + public DiscordWebhookClient(IWebProxy proxy = null, TimeSpan? timeout = null, + ILoggerFactory loggerFactory = null, LogLevel minimumLogLevel = LogLevel.Information, string logTimestampFormat = "yyyy-MM-dd HH:mm:ss zzz") + { + this.minimumLogLevel = minimumLogLevel; + this.logTimestampFormat = logTimestampFormat; + + if (loggerFactory == null) + { + loggerFactory = new DefaultLoggerFactory(); + loggerFactory.AddProvider(new DefaultLoggerProvider(minimumLogLevel)); + } + + this.Logger = loggerFactory.CreateLogger(); + + TimeSpan parsedTimeout = timeout ?? TimeSpan.FromSeconds(10); + + this.apiclient = new DiscordApiClient(parsedTimeout, this.Logger); + this.hooks = []; + this.Webhooks = new ReadOnlyCollection(this.hooks); + } + + /// + /// Registers a webhook with this client. This retrieves a webhook based on the ID and token supplied. + /// + /// The ID of the webhook to add. + /// The token of the webhook to add. + /// The registered webhook. + public async Task AddWebhookAsync(ulong id, string token) + { + if (string.IsNullOrWhiteSpace(token)) + { + throw new ArgumentNullException(nameof(token)); + } + + token = token.Trim(); + + if (this.hooks.Any(x => x.Id == id)) + { + throw new InvalidOperationException("This webhook is registered with this client."); + } + + DiscordWebhook wh = await this.apiclient.GetWebhookWithTokenAsync(id, token); + this.hooks.Add(wh); + + return wh; + } + + /// + public RequestMetricsCollection GetRequestMetrics(bool sinceLastCall = false) + => this.apiclient.GetRequestMetrics(sinceLastCall); + + /// + /// Registers a webhook with this client. This retrieves a webhook from webhook URL. + /// + /// URL of the webhook to retrieve. This URL must contain both ID and token. + /// The registered webhook. + public Task AddWebhookAsync(Uri url) + { + ArgumentNullException.ThrowIfNull(url); + Match m = GetWebhookRegex().Match(url.ToString()); + if (!m.Success) + { + throw new ArgumentException("Invalid webhook URL supplied.", nameof(url)); + } + + Group idraw = m.Groups["id"]; + Group tokenraw = m.Groups["token"]; + if (!ulong.TryParse(idraw.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out ulong id)) + { + throw new ArgumentException("Invalid webhook URL supplied.", nameof(url)); + } + + string token = tokenraw.Value; + return AddWebhookAsync(id, token); + } + + /// + /// Registers a webhook with this client. This retrieves a webhook using the supplied full discord client. + /// + /// ID of the webhook to register. + /// Discord client to which the webhook will belong. + /// The registered webhook. + public async Task AddWebhookAsync(ulong id, BaseDiscordClient client) + { + ArgumentNullException.ThrowIfNull(client); + if (this.hooks.Any(x => x.Id == id)) + { + throw new ArgumentException("This webhook is already registered with this client."); + } + + DiscordWebhook wh = await client.ApiClient.GetWebhookAsync(id); + // personally I don't think we need to override anything. + // it would even make sense to keep the hook as-is, in case + // it's returned without a token for some bizarre reason + // remember -- discord is not really consistent + //var nwh = new DiscordWebhook() + //{ + // ApiClient = apiclient, + // AvatarHash = wh.AvatarHash, + // ChannelId = wh.ChannelId, + // GuildId = wh.GuildId, + // Id = wh.Id, + // Name = wh.Name, + // Token = wh.Token, + // User = wh.User, + // Discord = null + //}; + this.hooks.Add(wh); + + return wh; + } + + /// + /// Registers a webhook with this client. This reuses the supplied webhook object. + /// + /// Webhook to register. + /// The registered webhook. + public DiscordWebhook AddWebhook(DiscordWebhook webhook) + { + ArgumentNullException.ThrowIfNull(webhook); + if (this.hooks.Any(x => x.Id == webhook.Id)) + { + throw new ArgumentException("This webhook is already registered with this client."); + } + + // see line 128-131 for explanation + // For christ's sake, update the line numbers if they change. + //var nwh = new DiscordWebhook() + //{ + // ApiClient = apiclient, + // AvatarHash = webhook.AvatarHash, + // ChannelId = webhook.ChannelId, + // GuildId = webhook.GuildId, + // Id = webhook.Id, + // Name = webhook.Name, + // Token = webhook.Token, + // User = webhook.User, + // Discord = null + //}; + this.hooks.Add(webhook); + + return webhook; + } + + /// + /// Unregisters a webhook with this client. + /// + /// ID of the webhook to unregister. + /// The unregistered webhook. + public DiscordWebhook RemoveWebhook(ulong id) + { + if (!this.hooks.Any(x => x.Id == id)) + { + throw new ArgumentException("This webhook is not registered with this client."); + } + + DiscordWebhook wh = GetRegisteredWebhook(id); + this.hooks.Remove(wh); + return wh; + } + + /// + /// Gets a registered webhook with specified ID. + /// + /// ID of the registered webhook to retrieve. + /// The requested webhook. + public DiscordWebhook GetRegisteredWebhook(ulong id) + => this.hooks.FirstOrDefault(xw => xw.Id == id); + + /// + /// Broadcasts a message to all registered webhooks. + /// + /// Webhook builder filled with data to send. + /// + public async Task> BroadcastMessageAsync(DiscordWebhookBuilder builder) + { + List deadhooks = []; + Dictionary messages = []; + + foreach (DiscordWebhook hook in this.hooks) + { + try + { + messages.Add(hook, await hook.ExecuteAsync(builder)); + } + catch (NotFoundException) + { + deadhooks.Add(hook); + } + } + + // Removing dead webhooks from collection + foreach (DiscordWebhook xwh in deadhooks) + { + this.hooks.Remove(xwh); + } + + return messages; + } + + ~DiscordWebhookClient() + { + this.hooks?.Clear(); + this.hooks = null!; + this.apiclient.rest.Dispose(); + } + + [GeneratedRegex(@"(?:https?:\/\/)?discord(?:app)?.com\/api\/(?:v\d\/)?webhooks\/(?\d+)\/(?[A-Za-z0-9_\-]+)", RegexOptions.ECMAScript)] + private static partial Regex GetWebhookRegex(); +} + +// 9/11 would improve again diff --git a/DSharpPlus/Clients/NullShardOrchestrator.cs b/DSharpPlus/Clients/NullShardOrchestrator.cs index 34509230bb..61d68c68e9 100644 --- a/DSharpPlus/Clients/NullShardOrchestrator.cs +++ b/DSharpPlus/Clients/NullShardOrchestrator.cs @@ -1,55 +1,55 @@ -using System; -using System.Threading.Tasks; -using DSharpPlus.Entities; - -namespace DSharpPlus.Clients; - -/// -/// Dummy orchestrator that does nothing. Useful for http interaction only clients. -/// -public sealed class NullShardOrchestrator : IShardOrchestrator -{ - /// - public bool AllShardsConnected { get; private set; } - - /// - public int TotalShardCount => 0; - - /// - public int ConnectedShardCount => 0; - - /// - public ValueTask BroadcastOutboundEventAsync(byte[] payload) => ValueTask.CompletedTask; - - /// - public TimeSpan GetConnectionLatency(ulong guildId) => TimeSpan.Zero; - - /// - public bool IsConnected(ulong guildId) => this.AllShardsConnected; - - /// - public ValueTask ReconnectAsync() - { - this.AllShardsConnected = true; - return ValueTask.CompletedTask; - } - - /// - /// Sends an outbound event to Discord. - /// - public ValueTask SendOutboundEventAsync(byte[] payload, ulong _) => ValueTask.CompletedTask; - - /// - public ValueTask StartAsync(DiscordActivity? activity, DiscordUserStatus? status, DateTimeOffset? idleSince) - { - this.AllShardsConnected = true; - return ValueTask.CompletedTask; - } - - /// - public ValueTask StopAsync() - { - this.AllShardsConnected = false; - return ValueTask.CompletedTask; - } -} +using System; +using System.Threading.Tasks; +using DSharpPlus.Entities; + +namespace DSharpPlus.Clients; + +/// +/// Dummy orchestrator that does nothing. Useful for http interaction only clients. +/// +public sealed class NullShardOrchestrator : IShardOrchestrator +{ + /// + public bool AllShardsConnected { get; private set; } + + /// + public int TotalShardCount => 0; + + /// + public int ConnectedShardCount => 0; + + /// + public ValueTask BroadcastOutboundEventAsync(byte[] payload) => ValueTask.CompletedTask; + + /// + public TimeSpan GetConnectionLatency(ulong guildId) => TimeSpan.Zero; + + /// + public bool IsConnected(ulong guildId) => this.AllShardsConnected; + + /// + public ValueTask ReconnectAsync() + { + this.AllShardsConnected = true; + return ValueTask.CompletedTask; + } + + /// + /// Sends an outbound event to Discord. + /// + public ValueTask SendOutboundEventAsync(byte[] payload, ulong _) => ValueTask.CompletedTask; + + /// + public ValueTask StartAsync(DiscordActivity? activity, DiscordUserStatus? status, DateTimeOffset? idleSince) + { + this.AllShardsConnected = true; + return ValueTask.CompletedTask; + } + + /// + public ValueTask StopAsync() + { + this.AllShardsConnected = false; + return ValueTask.CompletedTask; + } +} diff --git a/DSharpPlus/DiscordConfiguration.cs b/DSharpPlus/DiscordConfiguration.cs index d7cc475366..6615ca1d12 100644 --- a/DSharpPlus/DiscordConfiguration.cs +++ b/DSharpPlus/DiscordConfiguration.cs @@ -1,71 +1,71 @@ -using System; -using DSharpPlus.Net.Udp; - -namespace DSharpPlus; - -/// -/// Represents configuration for . -/// -public sealed class DiscordConfiguration -{ - - /// - /// Sets whether the client should attempt to cache members if exclusively using unprivileged intents. - /// - /// This will only take effect if there are no or - /// intents specified. Otherwise, this will always be overwritten to true. - /// - /// Defaults to true. - /// - public bool AlwaysCacheMembers { internal get; set; } = true; - - /// - /// Sets the default absolute expiration time for cached messages. - /// - public TimeSpan AbsoluteMessageCacheExpiration { internal get; set; } = TimeSpan.FromDays(1); - - /// - /// Sets the default sliding expiration time for cached messages. This is refreshed every time the message is - /// accessed. - /// - public TimeSpan SlidingMessageCacheExpiration { internal get; set; } = TimeSpan.FromMinutes(30); - - /// - /// Sets the factory method used to create instances of UDP clients. - /// Use and equivalents on other implementations to switch out client implementations. - /// Defaults to . - /// - public UdpClientFactoryDelegate UdpClientFactory - { - internal get => this.udpClientFactory; - set => this.udpClientFactory = value ?? throw new InvalidOperationException("You need to supply a valid UDP client factory method."); - } - private UdpClientFactoryDelegate udpClientFactory = DspUdpClient.CreateNew; - - /// - /// Whether to log unknown events or not. Defaults to true. - /// - public bool LogUnknownEvents { internal get; set; } = true; - - /// - /// Whether to log unknown auditlog types and change keys or not. Defaults to true. - /// - public bool LogUnknownAuditlogs { internal get; set; } = true; - - /// - /// Creates a new configuration with default values. - /// - public DiscordConfiguration() - { } - - /// - /// Creates a clone of another discord configuration. - /// - /// Client configuration to clone. - public DiscordConfiguration(DiscordConfiguration other) - { - this.UdpClientFactory = other.UdpClientFactory; - this.LogUnknownEvents = other.LogUnknownEvents; - this.LogUnknownAuditlogs = other.LogUnknownAuditlogs; - } -} +using System; +using DSharpPlus.Net.Udp; + +namespace DSharpPlus; + +/// +/// Represents configuration for . +/// +public sealed class DiscordConfiguration +{ + + /// + /// Sets whether the client should attempt to cache members if exclusively using unprivileged intents. + /// + /// This will only take effect if there are no or + /// intents specified. Otherwise, this will always be overwritten to true. + /// + /// Defaults to true. + /// + public bool AlwaysCacheMembers { internal get; set; } = true; + + /// + /// Sets the default absolute expiration time for cached messages. + /// + public TimeSpan AbsoluteMessageCacheExpiration { internal get; set; } = TimeSpan.FromDays(1); + + /// + /// Sets the default sliding expiration time for cached messages. This is refreshed every time the message is + /// accessed. + /// + public TimeSpan SlidingMessageCacheExpiration { internal get; set; } = TimeSpan.FromMinutes(30); + + /// + /// Sets the factory method used to create instances of UDP clients. + /// Use and equivalents on other implementations to switch out client implementations. + /// Defaults to . + /// + public UdpClientFactoryDelegate UdpClientFactory + { + internal get => this.udpClientFactory; + set => this.udpClientFactory = value ?? throw new InvalidOperationException("You need to supply a valid UDP client factory method."); + } + private UdpClientFactoryDelegate udpClientFactory = DspUdpClient.CreateNew; + + /// + /// Whether to log unknown events or not. Defaults to true. + /// + public bool LogUnknownEvents { internal get; set; } = true; + + /// + /// Whether to log unknown auditlog types and change keys or not. Defaults to true. + /// + public bool LogUnknownAuditlogs { internal get; set; } = true; + + /// + /// Creates a new configuration with default values. + /// + public DiscordConfiguration() + { } + + /// + /// Creates a clone of another discord configuration. + /// + /// Client configuration to clone. + public DiscordConfiguration(DiscordConfiguration other) + { + this.UdpClientFactory = other.UdpClientFactory; + this.LogUnknownEvents = other.LogUnknownEvents; + this.LogUnknownAuditlogs = other.LogUnknownAuditlogs; + } +} diff --git a/DSharpPlus/DiscordIntents.cs b/DSharpPlus/DiscordIntents.cs index 531f1a379c..2fb9d4885c 100644 --- a/DSharpPlus/DiscordIntents.cs +++ b/DSharpPlus/DiscordIntents.cs @@ -1,196 +1,196 @@ -using System; - -namespace DSharpPlus; - -public static class DiscordIntentExtensions -{ - /// - /// Calculates whether these intents have a certain intent. - /// - /// The base intents. - /// The intents to search for. - /// - public static bool HasIntent(this DiscordIntents intents, DiscordIntents search) - => (intents & search) == search; - - /// - /// Adds an intent to these intents. - /// - /// The base intents. - /// The intents to add. - /// - public static DiscordIntents AddIntent(this DiscordIntents intents, DiscordIntents toAdd) - => intents |= toAdd; - - /// - /// Removes an intent from these intents. - /// - /// The base intents. - /// The intents to remove. - /// - public static DiscordIntents RemoveIntent(this DiscordIntents intents, DiscordIntents toRemove) - => intents &= ~toRemove; - - internal static bool HasAllPrivilegedIntents(this DiscordIntents intents) - => intents.HasIntent(DiscordIntents.GuildMembers | DiscordIntents.GuildPresences); -} - -/// -/// Represents gateway intents to be specified for connecting to Discord. -/// -[Flags] -public enum DiscordIntents -{ - /// - /// By default, no Discord Intents are requested from the Discord gateway. - /// - None = 0, - - /// - /// Whether to include general guild events. - /// These include GuildCreated, GuildDeleted, GuildAvailable, GuildDownloadCompleted, - /// GuildRoleCreated, GuildRoleUpdated, GuildRoleDeleted, - /// ChannelCreated, ChannelUpdated, ChannelDeleted, and ChannelPinsUpdated. - /// - Guilds = 1 << 0, - - /// - /// Whether to include guild member events. - /// These include GuildMemberAdded, GuildMemberUpdated, and GuildMemberRemoved. - /// This is a privileged intent, and must be enabled on the bot's developer page. - /// - GuildMembers = 1 << 1, - - /// - /// Whether to include guild ban events. - /// These include GuildBanAdded, GuildBanRemoved and GuildAuditLogCreated. - /// - GuildModeration = 1 << 2, - - /// - /// Whether to include guild emoji events. - /// This includes GuildEmojisUpdated. - /// - GuildEmojisAndStickers = 1 << 3, - - /// - /// Whether to include guild integration events. - /// This includes GuildIntegrationsUpdated. - /// - GuildIntegrations = 1 << 4, - - /// - /// Whether to include guild webhook events. - /// This includes WebhooksUpdated. - /// - GuildWebhooks = 1 << 5, - - /// - /// Whether to include guild invite events. - /// These include InviteCreated and InviteDeleted. - /// - GuildInvites = 1 << 6, - - /// - /// Whether to include guild voice state events. - /// This includes VoiceStateUpdated. - /// - GuildVoiceStates = 1 << 7, - - /// - /// Whether to include guild presence events. - /// This includes PresenceUpdated. - /// This is a privileged intent, and must be enabled on the bot's developer page. - /// - GuildPresences = 1 << 8, - - /// - /// Whether to include guild message events. - /// These include MessageCreated, MessageUpdated, and MessageDeleted. - /// - GuildMessages = 1 << 9, - - /// - /// Whether to include guild reaction events. - /// These include MessageReactionAdded, MessageReactionRemoved, MessageReactionsCleared, - /// and MessageReactionRemovedEmoji. - /// - GuildMessageReactions = 1 << 10, - - /// - /// Whether to include guild typing events. - /// These include TypingStarted. - /// - GuildMessageTyping = 1 << 11, - - /// - /// Whether to include general direct message events. - /// These include ChannelCreated, MessageCreated, MessageUpdated, - /// MessageDeleted, ChannelPinsUpdated. - /// These events only fire for DM channels. - /// - DirectMessages = 1 << 12, - - /// - /// Whether to include direct message reaction events. - /// These include MessageReactionAdded, MessageReactionRemoved, - /// MessageReactionsCleared, and MessageReactionRemovedEmoji. - /// These events only fire for DM channels. - /// - DirectMessageReactions = 1 << 13, - - /// - /// Whether to include direct message typing events. - /// This includes TypingStarted. - /// This event only fires for DM channels. - /// - DirectMessageTyping = 1 << 14, - - /// - /// Whether to include message content. This is a privileged event. - /// Message content includes text, attachments, embeds, components, and reply content. - /// This intent is required for CommandsNext to function correctly. - /// - MessageContents = 1 << 15, - - /// - /// Whether to include scheduled event messages. - /// - ScheduledGuildEvents = 1 << 16, - - /// - /// Whetever to include creation, modification or deletion of an auto-Moderation rule. - /// - AutoModerationEvents = 1 << 20, - - /// - /// Whetever to include when an auto-moderation rule was fired. - /// - AutoModerationExecution = 1 << 21, - - /// - /// Whetever to include add and remove of a poll votes events in guilds. - /// This includes MessagePollVoted - /// - GuildMessagePolls = 1 << 24, - - /// - /// Whetever to include add and remove of a poll votes events in direct messages. - /// This includes MessagePollVoted - /// - DirectMessagePolls = 1 << 25, - - /// - /// Includes all unprivileged intents. - /// These are all intents excluding and . - /// - AllUnprivileged = Guilds | GuildModeration | GuildEmojisAndStickers | GuildIntegrations | GuildWebhooks | GuildInvites | GuildVoiceStates | GuildMessages | - GuildMessageReactions | GuildMessageTyping | DirectMessages | DirectMessageReactions | DirectMessageTyping | ScheduledGuildEvents | - AutoModerationEvents | AutoModerationExecution | GuildMessagePolls | DirectMessagePolls, - - /// - /// Includes all intents. - /// The and intents are privileged, and must be enabled on the bot's developer page. - /// - All = AllUnprivileged | GuildMembers | GuildPresences | MessageContents -} +using System; + +namespace DSharpPlus; + +public static class DiscordIntentExtensions +{ + /// + /// Calculates whether these intents have a certain intent. + /// + /// The base intents. + /// The intents to search for. + /// + public static bool HasIntent(this DiscordIntents intents, DiscordIntents search) + => (intents & search) == search; + + /// + /// Adds an intent to these intents. + /// + /// The base intents. + /// The intents to add. + /// + public static DiscordIntents AddIntent(this DiscordIntents intents, DiscordIntents toAdd) + => intents |= toAdd; + + /// + /// Removes an intent from these intents. + /// + /// The base intents. + /// The intents to remove. + /// + public static DiscordIntents RemoveIntent(this DiscordIntents intents, DiscordIntents toRemove) + => intents &= ~toRemove; + + internal static bool HasAllPrivilegedIntents(this DiscordIntents intents) + => intents.HasIntent(DiscordIntents.GuildMembers | DiscordIntents.GuildPresences); +} + +/// +/// Represents gateway intents to be specified for connecting to Discord. +/// +[Flags] +public enum DiscordIntents +{ + /// + /// By default, no Discord Intents are requested from the Discord gateway. + /// + None = 0, + + /// + /// Whether to include general guild events. + /// These include GuildCreated, GuildDeleted, GuildAvailable, GuildDownloadCompleted, + /// GuildRoleCreated, GuildRoleUpdated, GuildRoleDeleted, + /// ChannelCreated, ChannelUpdated, ChannelDeleted, and ChannelPinsUpdated. + /// + Guilds = 1 << 0, + + /// + /// Whether to include guild member events. + /// These include GuildMemberAdded, GuildMemberUpdated, and GuildMemberRemoved. + /// This is a privileged intent, and must be enabled on the bot's developer page. + /// + GuildMembers = 1 << 1, + + /// + /// Whether to include guild ban events. + /// These include GuildBanAdded, GuildBanRemoved and GuildAuditLogCreated. + /// + GuildModeration = 1 << 2, + + /// + /// Whether to include guild emoji events. + /// This includes GuildEmojisUpdated. + /// + GuildEmojisAndStickers = 1 << 3, + + /// + /// Whether to include guild integration events. + /// This includes GuildIntegrationsUpdated. + /// + GuildIntegrations = 1 << 4, + + /// + /// Whether to include guild webhook events. + /// This includes WebhooksUpdated. + /// + GuildWebhooks = 1 << 5, + + /// + /// Whether to include guild invite events. + /// These include InviteCreated and InviteDeleted. + /// + GuildInvites = 1 << 6, + + /// + /// Whether to include guild voice state events. + /// This includes VoiceStateUpdated. + /// + GuildVoiceStates = 1 << 7, + + /// + /// Whether to include guild presence events. + /// This includes PresenceUpdated. + /// This is a privileged intent, and must be enabled on the bot's developer page. + /// + GuildPresences = 1 << 8, + + /// + /// Whether to include guild message events. + /// These include MessageCreated, MessageUpdated, and MessageDeleted. + /// + GuildMessages = 1 << 9, + + /// + /// Whether to include guild reaction events. + /// These include MessageReactionAdded, MessageReactionRemoved, MessageReactionsCleared, + /// and MessageReactionRemovedEmoji. + /// + GuildMessageReactions = 1 << 10, + + /// + /// Whether to include guild typing events. + /// These include TypingStarted. + /// + GuildMessageTyping = 1 << 11, + + /// + /// Whether to include general direct message events. + /// These include ChannelCreated, MessageCreated, MessageUpdated, + /// MessageDeleted, ChannelPinsUpdated. + /// These events only fire for DM channels. + /// + DirectMessages = 1 << 12, + + /// + /// Whether to include direct message reaction events. + /// These include MessageReactionAdded, MessageReactionRemoved, + /// MessageReactionsCleared, and MessageReactionRemovedEmoji. + /// These events only fire for DM channels. + /// + DirectMessageReactions = 1 << 13, + + /// + /// Whether to include direct message typing events. + /// This includes TypingStarted. + /// This event only fires for DM channels. + /// + DirectMessageTyping = 1 << 14, + + /// + /// Whether to include message content. This is a privileged event. + /// Message content includes text, attachments, embeds, components, and reply content. + /// This intent is required for CommandsNext to function correctly. + /// + MessageContents = 1 << 15, + + /// + /// Whether to include scheduled event messages. + /// + ScheduledGuildEvents = 1 << 16, + + /// + /// Whetever to include creation, modification or deletion of an auto-Moderation rule. + /// + AutoModerationEvents = 1 << 20, + + /// + /// Whetever to include when an auto-moderation rule was fired. + /// + AutoModerationExecution = 1 << 21, + + /// + /// Whetever to include add and remove of a poll votes events in guilds. + /// This includes MessagePollVoted + /// + GuildMessagePolls = 1 << 24, + + /// + /// Whetever to include add and remove of a poll votes events in direct messages. + /// This includes MessagePollVoted + /// + DirectMessagePolls = 1 << 25, + + /// + /// Includes all unprivileged intents. + /// These are all intents excluding and . + /// + AllUnprivileged = Guilds | GuildModeration | GuildEmojisAndStickers | GuildIntegrations | GuildWebhooks | GuildInvites | GuildVoiceStates | GuildMessages | + GuildMessageReactions | GuildMessageTyping | DirectMessages | DirectMessageReactions | DirectMessageTyping | ScheduledGuildEvents | + AutoModerationEvents | AutoModerationExecution | GuildMessagePolls | DirectMessagePolls, + + /// + /// Includes all intents. + /// The and intents are privileged, and must be enabled on the bot's developer page. + /// + All = AllUnprivileged | GuildMembers | GuildPresences | MessageContents +} diff --git a/DSharpPlus/Entities/Application/DiscordApplication.cs b/DSharpPlus/Entities/Application/DiscordApplication.cs index e96d192343..82392120bb 100644 --- a/DSharpPlus/Entities/Application/DiscordApplication.cs +++ b/DSharpPlus/Entities/Application/DiscordApplication.cs @@ -1,640 +1,640 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Globalization; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using DSharpPlus.Net.Abstractions; - -namespace DSharpPlus.Entities; - -/// -/// Represents an OAuth2 application. -/// -public sealed class DiscordApplication : DiscordMessageApplication, IEquatable -{ - /// - /// Gets the application's icon. - /// - public override string? Icon - => !string.IsNullOrWhiteSpace(this.IconHash) - ? $"https://cdn.discordapp.com/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 terms of service URL. - /// - public string? TermsOfServiceUrl { get; internal set; } - - /// - /// Gets the application's privacy policy URL. - /// - public string? PrivacyPolicyUrl { get; internal set; } - - /// - /// Gets the application's allowed RPC origins. - /// - public IReadOnlyList? RpcOrigins { get; internal set; } - - /// - /// Gets the application's flags. - /// - public DiscordApplicationFlags? 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 hash of the application's cover image. - /// - public string? CoverImageHash { get; internal set; } - - /// - /// Gets this application's cover image URL. - /// - public override string? CoverImageUrl - => $"https://cdn.discordapp.com/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; } - - /// - /// Public key used to verify http interactions - /// - public string VerifyKey { get; internal set; } - - /// - /// Partial user object for the bot user associated with the app. - /// - public DiscordUser? Bot { get; internal set; } - - /// - /// Default scopes and permissions for each supported installation context. - /// - public IReadOnlyDictionary? IntegrationTypeConfigurations { get; internal set; } - - /// - /// Guild associated with the app. For example, a developer support server. - /// - public ulong? GuildId { get; internal set; } - - /// - /// Partial object of the associated guild - /// - public DiscordGuild? Guild { get; internal set; } - - /// - /// If this app 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 app 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; } - - /// - /// Approximate count of guilds the app has been added to - /// - public int? ApproximateGuildCount { get; internal set; } - - /// - /// Approximate count of users that have installed the app - /// - public int? ApproximateUserInstallCount { get; internal set; } - - /// - /// Array of redirect URIs for the app - /// - public string[] RedirectUris { get; internal set; } - - /// - /// Interactions endpoint URL for the app - /// - public string? InteractionsEndpointUrl { get; internal set; } - - /// - /// Interactions endpoint URL for the app - /// - public string? RoleConnectionsVerificationEndpointUrl { get; internal set; } - - /// - /// List of tags describing the content and functionality of the app. Max of 5 tags. - /// - public string[]? Tags { get; internal set; } - - /// - /// Settings for the app's default in-app authorization link, if enabled - /// - public DiscordApplicationOAuth2InstallParams? InstallParams { get; internal set; } - - /// - /// Default custom authorization URL for the app, if enabled - /// - public string? CustomInstallUrl { get; internal set; } - - private IReadOnlyList? Assets { get; set; } - - internal Dictionary ApplicationEmojis { get; set; } = new(); - - internal DiscordApplication() { } - - internal DiscordApplication(TransportApplication transportApplication, BaseDiscordClient baseDiscordClient) - { - this.Discord = baseDiscordClient; - this.Id = transportApplication.Id; - this.Name = transportApplication.Name; - this.IconHash = transportApplication.IconHash; - this.Description = transportApplication.Description; - this.IsPublic = transportApplication.IsPublicBot; - this.RequiresCodeGrant = transportApplication.BotRequiresCodeGrant; - this.TermsOfServiceUrl = transportApplication.TermsOfServiceUrl; - this.PrivacyPolicyUrl = transportApplication.PrivacyPolicyUrl; - this.RpcOrigins = transportApplication.RpcOrigins != null - ? new ReadOnlyCollection(transportApplication.RpcOrigins) - : null; - this.Flags = transportApplication.Flags; - this.CoverImageHash = transportApplication.CoverImageHash; - this.VerifyKey = transportApplication.VerifyKey; - - this.Bot = transportApplication.Bot is null - ? null - : new DiscordUser(transportApplication.Bot) - { - Discord = this.Discord - }; - - this.GuildId = transportApplication.GuildId; - this.Guild = transportApplication.Guild; - if (this.Guild is not null) - { - this.Guild.Discord = this.Discord; - } - - this.PrimarySkuId = transportApplication.PrimarySkuId; - this.Slug = transportApplication.Slug; - this.ApproximateGuildCount = transportApplication.ApproximateGuildCount; - this.ApproximateUserInstallCount = transportApplication.ApproximateUserInstallCount; - this.RedirectUris = transportApplication.RedirectUris; - this.InteractionsEndpointUrl = transportApplication.InteractionEndpointUrl; - this.RoleConnectionsVerificationEndpointUrl = transportApplication.RoleConnectionsVerificationUrl; - this.Tags = transportApplication.Tags; - this.InstallParams = transportApplication.InstallParams; - this.IntegrationTypeConfigurations = transportApplication.IntegrationTypeConfigurations; - this.CustomInstallUrl = transportApplication.CustomInstallUrl; - - - // do team and owners - // tbh fuck doing this properly - if (transportApplication.Team == null) - { - // singular owner - DiscordUser owner = new(transportApplication.Owner ?? throw new InvalidOperationException() ) {Discord = this.Discord}; - this.Owners = new ReadOnlyCollection([owner]); - this.Team = null; - } - else - { - // team owner - - this.Team = new DiscordTeam(transportApplication.Team); - - DiscordTeamMember[] members = transportApplication.Team.Members - .Select(x => new DiscordTeamMember(x) { Team = this.Team, User = new DiscordUser(x.User){Discord = this.Discord} }) - .ToArray(); - - DiscordUser[] owners = members - .Where(x => x.MembershipStatus == DiscordTeamMembershipStatus.Accepted) - .Select(x => x.User) - .ToArray(); - - this.Owners = new ReadOnlyCollection(owners); - this.Team.Owner = owners.First(x => x.Id == transportApplication.Team.OwnerId); - this.Team.Members = new ReadOnlyCollection(members); - } - } - - - /// - /// 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(MediaFormat fmt, ushort size = 1024) - { - if (fmt == MediaFormat.Unknown) - { - throw new ArgumentException("You must specify valid image format.", nameof(fmt)); - } - - if (size is < 16 or > 2048) - { - throw new ArgumentOutOfRangeException(nameof(size)); - } - - double log = Math.Log(size, 2); - if (log < 4 || log > 11 || log % 1 != 0) - { - throw new ArgumentOutOfRangeException(nameof(size)); - } - - string formatString = fmt switch - { - MediaFormat.Gif => "gif", - MediaFormat.Jpeg => "jpg", - MediaFormat.Auto or MediaFormat.Png => "png", - MediaFormat.WebP => "webp", - _ => throw new ArgumentOutOfRangeException(nameof(fmt)), - }; - - string ssize = size.ToString(CultureInfo.InvariantCulture); - - if (!string.IsNullOrWhiteSpace(this.CoverImageHash)) - { - string id = this.Id.ToString(CultureInfo.InvariantCulture); - return $"https://cdn.discordapp.com/avatars/{id}/{this.CoverImageHash}.{formatString}?size={ssize}"; - } - else - { - return null; - } - } - - /// - /// Retrieves this application's assets. - /// - /// Whether to always make a REST request and update the cached assets. - /// This application's assets. - public async Task> GetAssetsAsync(bool updateCache = false) - { - if (updateCache || this.Assets == null) - { - this.Assets = await this.Discord.ApiClient.GetApplicationAssetsAsync(this); - } - - return this.Assets; - } - - /// - /// Creates a test entitlement for a user or guild - /// - /// The id of the sku the entitlement belongs to - /// The id of the entity which should recieve this entitlement - /// The type of the entity which should recieve this entitlement - /// The created entitlement - /// - /// This endpoint returns a partial entitlement object. It will not contain subscription_id, starts_at, or ends_at, as it's valid in perpetuity. - /// After creating a test entitlement, you'll need to reload your Discord client. After doing so, you'll see that your server or user now has premium access. - /// - public async ValueTask CreateTestEntitlementAsync - ( - ulong skuId, - ulong ownerId, - DiscordTestEntitlementOwnerType ownerType - ) - => await this.Discord.ApiClient.CreateTestEntitlementAsync(this.Id, skuId, ownerId, ownerType); - - /// - /// Creates a test entitlement for a user or guild - /// - /// The sku the entitlement belongs to - /// The id of the entity which should recieve this entitlement - /// The type of the entity which should recieve this entitlement - /// The created entitlement - /// - /// This endpoint returns a partial entitlement object. It will not contain subscription_id, starts_at, or ends_at, as it's valid in perpetuity. - /// After creating a test entitlement, you'll need to reload your Discord client. After doing so, you'll see that your server or user now has premium access. - /// - public async ValueTask CreateTestEntitlementAsync - ( - DiscordStockKeepingUnit sku, - ulong ownerId, - DiscordTestEntitlementOwnerType ownerType - ) - => await this.Discord.ApiClient.CreateTestEntitlementAsync(this.Id, sku.Id, ownerId, ownerType); - - /// - /// Creates a test entitlement for a user or guild - /// - /// The id of the sku the entitlement belongs to - /// The user which should recieve this entitlement - /// The created entitlement - /// - /// This endpoint returns a partial entitlement object. It will not contain subscription_id, starts_at, or ends_at, as it's valid in perpetuity. - /// After creating a test entitlement, you'll need to reload your Discord client. After doing so, you'll see that your server or user now has premium access. - /// - public async ValueTask CreateTestEntitlementAsync - ( - ulong skuId, - DiscordUser user - ) - => await this.Discord.ApiClient.CreateTestEntitlementAsync(this.Id, skuId, user.Id, DiscordTestEntitlementOwnerType.User); - - /// - /// Creates a test entitlement for a user or guild - /// - /// The id of the sku the entitlement belongs to - /// The guild which should recieve this entitlement - /// The created entitlement - /// - /// This endpoint returns a partial entitlement object. It will not contain subscription_id, starts_at, or ends_at, as it's valid in perpetuity. - /// After creating a test entitlement, you'll need to reload your Discord client. After doing so, you'll see that your server or user now has premium access. - /// - public async ValueTask CreateTestEntitlementAsync - ( - ulong skuId, - DiscordGuild guild - ) - => await this.Discord.ApiClient.CreateTestEntitlementAsync(this.Id, skuId, guild.Id, DiscordTestEntitlementOwnerType.Guild); - - /// - /// Creates a test entitlement for a user or guild - /// - /// The sku the entitlement belongs to - /// The user which should recieve this entitlement - /// The created entitlement - /// - /// This endpoint returns a partial entitlement object. It will not contain subscription_id, starts_at, or ends_at, as it's valid in perpetuity. - /// After creating a test entitlement, you'll need to reload your Discord client. After doing so, you'll see that your server or user now has premium access. - /// - public async ValueTask CreateTestEntitlementAsync - ( - DiscordStockKeepingUnit sku, - DiscordUser user - ) - => await this.Discord.ApiClient.CreateTestEntitlementAsync(this.Id, sku.Id, user.Id, DiscordTestEntitlementOwnerType.User); - - /// - /// Creates a test entitlement for a user or guild - /// - /// The sku the entitlement belongs to - /// The guild which should recieve this entitlement - /// The created entitlement - /// - /// This endpoint returns a partial entitlement object. It will not contain subscription_id, starts_at, or ends_at, as it's valid in perpetuity. - /// After creating a test entitlement, you'll need to reload your Discord client. After doing so, you'll see that your server or user now has premium access. - /// - public async ValueTask CreateTestEntitlementAsync - ( - DiscordStockKeepingUnit sku, - DiscordGuild guild - ) - => await this.Discord.ApiClient.CreateTestEntitlementAsync(this.Id, sku.Id, guild.Id, DiscordTestEntitlementOwnerType.Guild); - - /// - /// For One-Time Purchase consumable SKUs, marks a given entitlement for the user as consumed. - /// - /// The id of the entitlement which will be marked as consumed - public async ValueTask ConsumeEntitlementAsync(ulong entitlementId) - => await this.Discord.ApiClient.ConsumeEntitlementAsync(this.Id, entitlementId); - - /// - /// For One-Time Purchase consumable SKUs, marks a given entitlement for the user as consumed. - /// - /// The entitlement which will be marked as consumed - public async ValueTask ConsumeEntitlementAsync(DiscordEntitlement entitlement) - => await this.Discord.ApiClient.ConsumeEntitlementAsync(this.Id, entitlement.Id); - - /// - /// Deletes a test entitlement - /// - /// The id of the test entitlement which should be deleted - public async ValueTask DeleteTestEntitlementAsync(ulong entitlementId) - => await this.Discord.ApiClient.DeleteTestEntitlementAsync(this.Id, entitlementId); - - /// - /// Deletes a test entitlement - /// - /// The test entitlement which should be deleted - public async ValueTask DeleteTestEntitlementAsync(DiscordEntitlement entitlement) - => await this.Discord.ApiClient.DeleteTestEntitlementAsync(this.Id, entitlement.Id); - - public string GenerateBotOAuth(DiscordPermissions permissions = default) - { - permissions &= DiscordPermissions.All; - // hey look, it's not all annoying and blue :P - return new QueryUriBuilder("https://discord.com/oauth2/authorize") - .AddParameter("client_id", this.Id.ToString(CultureInfo.InvariantCulture)) - .AddParameter("scope", "bot") - .AddParameter("permissions", permissions.ToString()) - .ToString(); - } - - /// - /// Generates a new OAuth2 URI for this application. - /// - /// Redirect URI - the URI Discord will redirect users to as part of the OAuth flow. - /// - /// This URI must be already registered as a valid redirect URI for your application on the developer portal. - /// - /// - /// Permissions for your bot. Only required if the scope is passed. - /// OAuth scopes for your application. - public string GenerateOAuthUri - ( - string? redirectUri = null, - DiscordPermissions permissions = default, - params DiscordOAuthScope[] scopes - ) - { - permissions &= DiscordPermissions.All; - - StringBuilder scopeBuilder = new(); - - foreach (DiscordOAuthScope v in scopes) - { - scopeBuilder.Append(' ').Append(TranslateOAuthScope(v)); - } - - QueryUriBuilder queryBuilder = new QueryUriBuilder("https://discord.com/oauth2/authorize") - .AddParameter("client_id", this.Id.ToString(CultureInfo.InvariantCulture)) - .AddParameter("scope", scopeBuilder.ToString().Trim()); - - if (permissions != DiscordPermissions.None) - { - queryBuilder.AddParameter("permissions", permissions.ToString()); - } - - // response_type=code is always given for /authorize - if (redirectUri != null) - { - queryBuilder.AddParameter("redirect_uri", redirectUri) - .AddParameter("response_type", "code"); - } - - return queryBuilder.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) => 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 right, DiscordApplication left) - { - return (right is not null || left is null) - && (right is null || left is not null) - && ((right is null && left is null) - || right!.Id == left!.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); - - private static string? TranslateOAuthScope(DiscordOAuthScope scope) => scope switch - { - DiscordOAuthScope.Identify => "identify", - DiscordOAuthScope.Email => "email", - DiscordOAuthScope.Connections => "connections", - DiscordOAuthScope.Guilds => "guilds", - DiscordOAuthScope.GuildsJoin => "guilds.join", - DiscordOAuthScope.GuildsMembersRead => "guilds.members.read", - DiscordOAuthScope.GdmJoin => "gdm.join", - DiscordOAuthScope.Rpc => "rpc", - DiscordOAuthScope.RpcNotificationsRead => "rpc.notifications.read", - DiscordOAuthScope.RpcVoiceRead => "rpc.voice.read", - DiscordOAuthScope.RpcVoiceWrite => "rpc.voice.write", - DiscordOAuthScope.RpcActivitiesWrite => "rpc.activities.write", - DiscordOAuthScope.Bot => "bot", - DiscordOAuthScope.WebhookIncoming => "webhook.incoming", - DiscordOAuthScope.MessagesRead => "messages.read", - DiscordOAuthScope.ApplicationsBuildsUpload => "applications.builds.upload", - DiscordOAuthScope.ApplicationsBuildsRead => "applications.builds.read", - DiscordOAuthScope.ApplicationsCommands => "applications.commands", - DiscordOAuthScope.ApplicationsStoreUpdate => "applications.store.update", - DiscordOAuthScope.ApplicationsEntitlements => "applications.entitlements", - DiscordOAuthScope.ActivitiesRead => "activities.read", - DiscordOAuthScope.ActivitiesWrite => "activities.write", - DiscordOAuthScope.RelationshipsRead => "relationships.read", - _ => null - }; - - /// - /// List all stock keeping units belonging to this application - /// - /// - public async ValueTask> ListStockKeepingUnitsAsync() - => await this.Discord.ApiClient.ListStockKeepingUnitsAsync(this.Id); - - /// - /// List all Entitlements belonging to this application. - /// - /// Filters the entitlements by a user. - /// Filters the entitlements by specific SKUs. - /// Filters the entitlements to be before a specific snowflake. Can be used to filter by time. Mutually exclusive with parameter "after" - /// Filters the entitlements to be after a specific snowflake. Can be used to filter by time. Mutually exclusive with parameter "before" - /// Limits how many Entitlements should be returned. One API call per 100 entitlements - /// Filters the entitlements by a specific Guild. - /// Wheter or not to return time limited entitlements which have ended - /// CT to cancel the method before the next api call - /// Returns the list of entitlements fitting to the filters - /// Thrown when both "before" and "after" is set - public async IAsyncEnumerable ListEntitlementsAsync - ( - ulong? userId = null, - IEnumerable? skuIds = null, - ulong? before = null, - ulong? after = null, - int limit = 100, - ulong? guildId = null, - bool? excludeEnded = null, - [EnumeratorCancellation] - CancellationToken cancellationToken = default - ) - { - if (before is not null && after is not null) - { - throw new ArgumentException("before and after are mutually exclusive."); - } - - bool isAscending = before is null; - - while (limit > 0 && !cancellationToken.IsCancellationRequested) - { - int entitlementsThisRequest = Math.Min(100, limit); - limit -= entitlementsThisRequest; - - IReadOnlyList entitlements - = await this.Discord.ApiClient.ListEntitlementsAsync(this.Id, userId, skuIds, before, after, guildId, excludeEnded, limit); - - if (entitlements.Count == 0) - { - yield break; - } - - if (isAscending) - { - foreach (DiscordEntitlement entitlement in entitlements) - { - yield return entitlement; - } - - after = entitlements.Last().Id; - } - else - { - for (int i = entitlements.Count - 1; i >= 0; i--) - { - yield return entitlements[i]; - } - - before = entitlements.First().Id; - } - - if (entitlements.Count != entitlementsThisRequest) - { - yield break; - } - } - } -} +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Globalization; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using DSharpPlus.Net.Abstractions; + +namespace DSharpPlus.Entities; + +/// +/// Represents an OAuth2 application. +/// +public sealed class DiscordApplication : DiscordMessageApplication, IEquatable +{ + /// + /// Gets the application's icon. + /// + public override string? Icon + => !string.IsNullOrWhiteSpace(this.IconHash) + ? $"https://cdn.discordapp.com/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 terms of service URL. + /// + public string? TermsOfServiceUrl { get; internal set; } + + /// + /// Gets the application's privacy policy URL. + /// + public string? PrivacyPolicyUrl { get; internal set; } + + /// + /// Gets the application's allowed RPC origins. + /// + public IReadOnlyList? RpcOrigins { get; internal set; } + + /// + /// Gets the application's flags. + /// + public DiscordApplicationFlags? 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 hash of the application's cover image. + /// + public string? CoverImageHash { get; internal set; } + + /// + /// Gets this application's cover image URL. + /// + public override string? CoverImageUrl + => $"https://cdn.discordapp.com/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; } + + /// + /// Public key used to verify http interactions + /// + public string VerifyKey { get; internal set; } + + /// + /// Partial user object for the bot user associated with the app. + /// + public DiscordUser? Bot { get; internal set; } + + /// + /// Default scopes and permissions for each supported installation context. + /// + public IReadOnlyDictionary? IntegrationTypeConfigurations { get; internal set; } + + /// + /// Guild associated with the app. For example, a developer support server. + /// + public ulong? GuildId { get; internal set; } + + /// + /// Partial object of the associated guild + /// + public DiscordGuild? Guild { get; internal set; } + + /// + /// If this app 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 app 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; } + + /// + /// Approximate count of guilds the app has been added to + /// + public int? ApproximateGuildCount { get; internal set; } + + /// + /// Approximate count of users that have installed the app + /// + public int? ApproximateUserInstallCount { get; internal set; } + + /// + /// Array of redirect URIs for the app + /// + public string[] RedirectUris { get; internal set; } + + /// + /// Interactions endpoint URL for the app + /// + public string? InteractionsEndpointUrl { get; internal set; } + + /// + /// Interactions endpoint URL for the app + /// + public string? RoleConnectionsVerificationEndpointUrl { get; internal set; } + + /// + /// List of tags describing the content and functionality of the app. Max of 5 tags. + /// + public string[]? Tags { get; internal set; } + + /// + /// Settings for the app's default in-app authorization link, if enabled + /// + public DiscordApplicationOAuth2InstallParams? InstallParams { get; internal set; } + + /// + /// Default custom authorization URL for the app, if enabled + /// + public string? CustomInstallUrl { get; internal set; } + + private IReadOnlyList? Assets { get; set; } + + internal Dictionary ApplicationEmojis { get; set; } = new(); + + internal DiscordApplication() { } + + internal DiscordApplication(TransportApplication transportApplication, BaseDiscordClient baseDiscordClient) + { + this.Discord = baseDiscordClient; + this.Id = transportApplication.Id; + this.Name = transportApplication.Name; + this.IconHash = transportApplication.IconHash; + this.Description = transportApplication.Description; + this.IsPublic = transportApplication.IsPublicBot; + this.RequiresCodeGrant = transportApplication.BotRequiresCodeGrant; + this.TermsOfServiceUrl = transportApplication.TermsOfServiceUrl; + this.PrivacyPolicyUrl = transportApplication.PrivacyPolicyUrl; + this.RpcOrigins = transportApplication.RpcOrigins != null + ? new ReadOnlyCollection(transportApplication.RpcOrigins) + : null; + this.Flags = transportApplication.Flags; + this.CoverImageHash = transportApplication.CoverImageHash; + this.VerifyKey = transportApplication.VerifyKey; + + this.Bot = transportApplication.Bot is null + ? null + : new DiscordUser(transportApplication.Bot) + { + Discord = this.Discord + }; + + this.GuildId = transportApplication.GuildId; + this.Guild = transportApplication.Guild; + if (this.Guild is not null) + { + this.Guild.Discord = this.Discord; + } + + this.PrimarySkuId = transportApplication.PrimarySkuId; + this.Slug = transportApplication.Slug; + this.ApproximateGuildCount = transportApplication.ApproximateGuildCount; + this.ApproximateUserInstallCount = transportApplication.ApproximateUserInstallCount; + this.RedirectUris = transportApplication.RedirectUris; + this.InteractionsEndpointUrl = transportApplication.InteractionEndpointUrl; + this.RoleConnectionsVerificationEndpointUrl = transportApplication.RoleConnectionsVerificationUrl; + this.Tags = transportApplication.Tags; + this.InstallParams = transportApplication.InstallParams; + this.IntegrationTypeConfigurations = transportApplication.IntegrationTypeConfigurations; + this.CustomInstallUrl = transportApplication.CustomInstallUrl; + + + // do team and owners + // tbh fuck doing this properly + if (transportApplication.Team == null) + { + // singular owner + DiscordUser owner = new(transportApplication.Owner ?? throw new InvalidOperationException() ) {Discord = this.Discord}; + this.Owners = new ReadOnlyCollection([owner]); + this.Team = null; + } + else + { + // team owner + + this.Team = new DiscordTeam(transportApplication.Team); + + DiscordTeamMember[] members = transportApplication.Team.Members + .Select(x => new DiscordTeamMember(x) { Team = this.Team, User = new DiscordUser(x.User){Discord = this.Discord} }) + .ToArray(); + + DiscordUser[] owners = members + .Where(x => x.MembershipStatus == DiscordTeamMembershipStatus.Accepted) + .Select(x => x.User) + .ToArray(); + + this.Owners = new ReadOnlyCollection(owners); + this.Team.Owner = owners.First(x => x.Id == transportApplication.Team.OwnerId); + this.Team.Members = new ReadOnlyCollection(members); + } + } + + + /// + /// 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(MediaFormat fmt, ushort size = 1024) + { + if (fmt == MediaFormat.Unknown) + { + throw new ArgumentException("You must specify valid image format.", nameof(fmt)); + } + + if (size is < 16 or > 2048) + { + throw new ArgumentOutOfRangeException(nameof(size)); + } + + double log = Math.Log(size, 2); + if (log < 4 || log > 11 || log % 1 != 0) + { + throw new ArgumentOutOfRangeException(nameof(size)); + } + + string formatString = fmt switch + { + MediaFormat.Gif => "gif", + MediaFormat.Jpeg => "jpg", + MediaFormat.Auto or MediaFormat.Png => "png", + MediaFormat.WebP => "webp", + _ => throw new ArgumentOutOfRangeException(nameof(fmt)), + }; + + string ssize = size.ToString(CultureInfo.InvariantCulture); + + if (!string.IsNullOrWhiteSpace(this.CoverImageHash)) + { + string id = this.Id.ToString(CultureInfo.InvariantCulture); + return $"https://cdn.discordapp.com/avatars/{id}/{this.CoverImageHash}.{formatString}?size={ssize}"; + } + else + { + return null; + } + } + + /// + /// Retrieves this application's assets. + /// + /// Whether to always make a REST request and update the cached assets. + /// This application's assets. + public async Task> GetAssetsAsync(bool updateCache = false) + { + if (updateCache || this.Assets == null) + { + this.Assets = await this.Discord.ApiClient.GetApplicationAssetsAsync(this); + } + + return this.Assets; + } + + /// + /// Creates a test entitlement for a user or guild + /// + /// The id of the sku the entitlement belongs to + /// The id of the entity which should recieve this entitlement + /// The type of the entity which should recieve this entitlement + /// The created entitlement + /// + /// This endpoint returns a partial entitlement object. It will not contain subscription_id, starts_at, or ends_at, as it's valid in perpetuity. + /// After creating a test entitlement, you'll need to reload your Discord client. After doing so, you'll see that your server or user now has premium access. + /// + public async ValueTask CreateTestEntitlementAsync + ( + ulong skuId, + ulong ownerId, + DiscordTestEntitlementOwnerType ownerType + ) + => await this.Discord.ApiClient.CreateTestEntitlementAsync(this.Id, skuId, ownerId, ownerType); + + /// + /// Creates a test entitlement for a user or guild + /// + /// The sku the entitlement belongs to + /// The id of the entity which should recieve this entitlement + /// The type of the entity which should recieve this entitlement + /// The created entitlement + /// + /// This endpoint returns a partial entitlement object. It will not contain subscription_id, starts_at, or ends_at, as it's valid in perpetuity. + /// After creating a test entitlement, you'll need to reload your Discord client. After doing so, you'll see that your server or user now has premium access. + /// + public async ValueTask CreateTestEntitlementAsync + ( + DiscordStockKeepingUnit sku, + ulong ownerId, + DiscordTestEntitlementOwnerType ownerType + ) + => await this.Discord.ApiClient.CreateTestEntitlementAsync(this.Id, sku.Id, ownerId, ownerType); + + /// + /// Creates a test entitlement for a user or guild + /// + /// The id of the sku the entitlement belongs to + /// The user which should recieve this entitlement + /// The created entitlement + /// + /// This endpoint returns a partial entitlement object. It will not contain subscription_id, starts_at, or ends_at, as it's valid in perpetuity. + /// After creating a test entitlement, you'll need to reload your Discord client. After doing so, you'll see that your server or user now has premium access. + /// + public async ValueTask CreateTestEntitlementAsync + ( + ulong skuId, + DiscordUser user + ) + => await this.Discord.ApiClient.CreateTestEntitlementAsync(this.Id, skuId, user.Id, DiscordTestEntitlementOwnerType.User); + + /// + /// Creates a test entitlement for a user or guild + /// + /// The id of the sku the entitlement belongs to + /// The guild which should recieve this entitlement + /// The created entitlement + /// + /// This endpoint returns a partial entitlement object. It will not contain subscription_id, starts_at, or ends_at, as it's valid in perpetuity. + /// After creating a test entitlement, you'll need to reload your Discord client. After doing so, you'll see that your server or user now has premium access. + /// + public async ValueTask CreateTestEntitlementAsync + ( + ulong skuId, + DiscordGuild guild + ) + => await this.Discord.ApiClient.CreateTestEntitlementAsync(this.Id, skuId, guild.Id, DiscordTestEntitlementOwnerType.Guild); + + /// + /// Creates a test entitlement for a user or guild + /// + /// The sku the entitlement belongs to + /// The user which should recieve this entitlement + /// The created entitlement + /// + /// This endpoint returns a partial entitlement object. It will not contain subscription_id, starts_at, or ends_at, as it's valid in perpetuity. + /// After creating a test entitlement, you'll need to reload your Discord client. After doing so, you'll see that your server or user now has premium access. + /// + public async ValueTask CreateTestEntitlementAsync + ( + DiscordStockKeepingUnit sku, + DiscordUser user + ) + => await this.Discord.ApiClient.CreateTestEntitlementAsync(this.Id, sku.Id, user.Id, DiscordTestEntitlementOwnerType.User); + + /// + /// Creates a test entitlement for a user or guild + /// + /// The sku the entitlement belongs to + /// The guild which should recieve this entitlement + /// The created entitlement + /// + /// This endpoint returns a partial entitlement object. It will not contain subscription_id, starts_at, or ends_at, as it's valid in perpetuity. + /// After creating a test entitlement, you'll need to reload your Discord client. After doing so, you'll see that your server or user now has premium access. + /// + public async ValueTask CreateTestEntitlementAsync + ( + DiscordStockKeepingUnit sku, + DiscordGuild guild + ) + => await this.Discord.ApiClient.CreateTestEntitlementAsync(this.Id, sku.Id, guild.Id, DiscordTestEntitlementOwnerType.Guild); + + /// + /// For One-Time Purchase consumable SKUs, marks a given entitlement for the user as consumed. + /// + /// The id of the entitlement which will be marked as consumed + public async ValueTask ConsumeEntitlementAsync(ulong entitlementId) + => await this.Discord.ApiClient.ConsumeEntitlementAsync(this.Id, entitlementId); + + /// + /// For One-Time Purchase consumable SKUs, marks a given entitlement for the user as consumed. + /// + /// The entitlement which will be marked as consumed + public async ValueTask ConsumeEntitlementAsync(DiscordEntitlement entitlement) + => await this.Discord.ApiClient.ConsumeEntitlementAsync(this.Id, entitlement.Id); + + /// + /// Deletes a test entitlement + /// + /// The id of the test entitlement which should be deleted + public async ValueTask DeleteTestEntitlementAsync(ulong entitlementId) + => await this.Discord.ApiClient.DeleteTestEntitlementAsync(this.Id, entitlementId); + + /// + /// Deletes a test entitlement + /// + /// The test entitlement which should be deleted + public async ValueTask DeleteTestEntitlementAsync(DiscordEntitlement entitlement) + => await this.Discord.ApiClient.DeleteTestEntitlementAsync(this.Id, entitlement.Id); + + public string GenerateBotOAuth(DiscordPermissions permissions = default) + { + permissions &= DiscordPermissions.All; + // hey look, it's not all annoying and blue :P + return new QueryUriBuilder("https://discord.com/oauth2/authorize") + .AddParameter("client_id", this.Id.ToString(CultureInfo.InvariantCulture)) + .AddParameter("scope", "bot") + .AddParameter("permissions", permissions.ToString()) + .ToString(); + } + + /// + /// Generates a new OAuth2 URI for this application. + /// + /// Redirect URI - the URI Discord will redirect users to as part of the OAuth flow. + /// + /// This URI must be already registered as a valid redirect URI for your application on the developer portal. + /// + /// + /// Permissions for your bot. Only required if the scope is passed. + /// OAuth scopes for your application. + public string GenerateOAuthUri + ( + string? redirectUri = null, + DiscordPermissions permissions = default, + params DiscordOAuthScope[] scopes + ) + { + permissions &= DiscordPermissions.All; + + StringBuilder scopeBuilder = new(); + + foreach (DiscordOAuthScope v in scopes) + { + scopeBuilder.Append(' ').Append(TranslateOAuthScope(v)); + } + + QueryUriBuilder queryBuilder = new QueryUriBuilder("https://discord.com/oauth2/authorize") + .AddParameter("client_id", this.Id.ToString(CultureInfo.InvariantCulture)) + .AddParameter("scope", scopeBuilder.ToString().Trim()); + + if (permissions != DiscordPermissions.None) + { + queryBuilder.AddParameter("permissions", permissions.ToString()); + } + + // response_type=code is always given for /authorize + if (redirectUri != null) + { + queryBuilder.AddParameter("redirect_uri", redirectUri) + .AddParameter("response_type", "code"); + } + + return queryBuilder.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) => 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 right, DiscordApplication left) + { + return (right is not null || left is null) + && (right is null || left is not null) + && ((right is null && left is null) + || right!.Id == left!.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); + + private static string? TranslateOAuthScope(DiscordOAuthScope scope) => scope switch + { + DiscordOAuthScope.Identify => "identify", + DiscordOAuthScope.Email => "email", + DiscordOAuthScope.Connections => "connections", + DiscordOAuthScope.Guilds => "guilds", + DiscordOAuthScope.GuildsJoin => "guilds.join", + DiscordOAuthScope.GuildsMembersRead => "guilds.members.read", + DiscordOAuthScope.GdmJoin => "gdm.join", + DiscordOAuthScope.Rpc => "rpc", + DiscordOAuthScope.RpcNotificationsRead => "rpc.notifications.read", + DiscordOAuthScope.RpcVoiceRead => "rpc.voice.read", + DiscordOAuthScope.RpcVoiceWrite => "rpc.voice.write", + DiscordOAuthScope.RpcActivitiesWrite => "rpc.activities.write", + DiscordOAuthScope.Bot => "bot", + DiscordOAuthScope.WebhookIncoming => "webhook.incoming", + DiscordOAuthScope.MessagesRead => "messages.read", + DiscordOAuthScope.ApplicationsBuildsUpload => "applications.builds.upload", + DiscordOAuthScope.ApplicationsBuildsRead => "applications.builds.read", + DiscordOAuthScope.ApplicationsCommands => "applications.commands", + DiscordOAuthScope.ApplicationsStoreUpdate => "applications.store.update", + DiscordOAuthScope.ApplicationsEntitlements => "applications.entitlements", + DiscordOAuthScope.ActivitiesRead => "activities.read", + DiscordOAuthScope.ActivitiesWrite => "activities.write", + DiscordOAuthScope.RelationshipsRead => "relationships.read", + _ => null + }; + + /// + /// List all stock keeping units belonging to this application + /// + /// + public async ValueTask> ListStockKeepingUnitsAsync() + => await this.Discord.ApiClient.ListStockKeepingUnitsAsync(this.Id); + + /// + /// List all Entitlements belonging to this application. + /// + /// Filters the entitlements by a user. + /// Filters the entitlements by specific SKUs. + /// Filters the entitlements to be before a specific snowflake. Can be used to filter by time. Mutually exclusive with parameter "after" + /// Filters the entitlements to be after a specific snowflake. Can be used to filter by time. Mutually exclusive with parameter "before" + /// Limits how many Entitlements should be returned. One API call per 100 entitlements + /// Filters the entitlements by a specific Guild. + /// Wheter or not to return time limited entitlements which have ended + /// CT to cancel the method before the next api call + /// Returns the list of entitlements fitting to the filters + /// Thrown when both "before" and "after" is set + public async IAsyncEnumerable ListEntitlementsAsync + ( + ulong? userId = null, + IEnumerable? skuIds = null, + ulong? before = null, + ulong? after = null, + int limit = 100, + ulong? guildId = null, + bool? excludeEnded = null, + [EnumeratorCancellation] + CancellationToken cancellationToken = default + ) + { + if (before is not null && after is not null) + { + throw new ArgumentException("before and after are mutually exclusive."); + } + + bool isAscending = before is null; + + while (limit > 0 && !cancellationToken.IsCancellationRequested) + { + int entitlementsThisRequest = Math.Min(100, limit); + limit -= entitlementsThisRequest; + + IReadOnlyList entitlements + = await this.Discord.ApiClient.ListEntitlementsAsync(this.Id, userId, skuIds, before, after, guildId, excludeEnded, limit); + + if (entitlements.Count == 0) + { + yield break; + } + + if (isAscending) + { + foreach (DiscordEntitlement entitlement in entitlements) + { + yield return entitlement; + } + + after = entitlements.Last().Id; + } + else + { + for (int i = entitlements.Count - 1; i >= 0; i--) + { + yield return entitlements[i]; + } + + before = entitlements.First().Id; + } + + if (entitlements.Count != entitlementsThisRequest) + { + yield break; + } + } + } +} diff --git a/DSharpPlus/Entities/Application/DiscordApplicationAsset.cs b/DSharpPlus/Entities/Application/DiscordApplicationAsset.cs index e49ded74f1..b317b6935c 100644 --- a/DSharpPlus/Entities/Application/DiscordApplicationAsset.cs +++ b/DSharpPlus/Entities/Application/DiscordApplicationAsset.cs @@ -1,83 +1,83 @@ -using System; -using System.Globalization; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// 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; } = default!; - - /// - /// Gets the asset's type. - /// - [JsonProperty("type")] - public DiscordApplicationAssetType Type { get; internal set; } - - /// - /// Gets the application this asset belongs to. - /// - public DiscordApplication Application { get; internal set; } = default!; - - /// - /// Gets the Url of this asset. - /// - public override Uri Url - => new($"https://cdn.discordapp.com/app-assets/{this.Application.Id.ToString(CultureInfo.InvariantCulture)}/{this.Id}.png"); - - internal DiscordApplicationAsset() { } - - 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) => 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 right, DiscordApplicationAsset left) => (right is not null || left is null) - && (right is null || left is not null) - && ((right is null && left is null) - || right!.Id == left!.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); -} +using System; +using System.Globalization; +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// 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; } = default!; + + /// + /// Gets the asset's type. + /// + [JsonProperty("type")] + public DiscordApplicationAssetType Type { get; internal set; } + + /// + /// Gets the application this asset belongs to. + /// + public DiscordApplication Application { get; internal set; } = default!; + + /// + /// Gets the Url of this asset. + /// + public override Uri Url + => new($"https://cdn.discordapp.com/app-assets/{this.Application.Id.ToString(CultureInfo.InvariantCulture)}/{this.Id}.png"); + + internal DiscordApplicationAsset() { } + + 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) => 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 right, DiscordApplicationAsset left) => (right is not null || left is null) + && (right is null || left is not null) + && ((right is null && left is null) + || right!.Id == left!.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); +} diff --git a/DSharpPlus/Entities/Application/DiscordApplicationAssetType.cs b/DSharpPlus/Entities/Application/DiscordApplicationAssetType.cs index 019cca347e..fc843be5c7 100644 --- a/DSharpPlus/Entities/Application/DiscordApplicationAssetType.cs +++ b/DSharpPlus/Entities/Application/DiscordApplicationAssetType.cs @@ -1,23 +1,23 @@ -namespace DSharpPlus.Entities; - - -/// -/// Determines the type of the asset attached to the application. -/// -public enum DiscordApplicationAssetType -{ - /// - /// 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 -} +namespace DSharpPlus.Entities; + + +/// +/// Determines the type of the asset attached to the application. +/// +public enum DiscordApplicationAssetType +{ + /// + /// 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/DSharpPlus/Entities/Application/DiscordApplicationFlags.cs b/DSharpPlus/Entities/Application/DiscordApplicationFlags.cs index 711e8cc1c0..c619a01789 100644 --- a/DSharpPlus/Entities/Application/DiscordApplicationFlags.cs +++ b/DSharpPlus/Entities/Application/DiscordApplicationFlags.cs @@ -1,59 +1,59 @@ - -namespace DSharpPlus.Entities; - - -/// -/// Represents flags for a discord application. -/// -public enum DiscordApplicationFlags -{ - /// - /// Indicates if an application uses the Auto Moderation API. - /// - ApplicationAutoModerationRuleCreateBadge = 1 << 6, - - /// - /// Indicates that the application is approved for the intent. - /// - GatewayPresence = 1 << 12, - - /// - /// Indicates that the application is awaiting approval for the intent. - /// - GatewayPresenceLimited = 1 << 13, - - /// - /// Indicates that the application is approved for the intent. - /// - GatewayGuildMembers = 1 << 14, - - /// - /// Indicates that the application is awaiting approval for the intent. - /// - GatewayGuildMembersLimited = 1 << 15, - - /// - /// Indicates that the application is awaiting verification. - /// - VerificationPendingGuildLimit = 1 << 16, - - /// - /// Indicates that the application is a voice channel application. - /// - Embedded = 1 << 17, - - /// - /// The application can track message content. - /// - GatewayMessageContent = 1 << 18, - - /// - /// The application can track message content (limited). - /// - GatewayMessageContentLimited = 1 << 19, - - /// - /// Indicates if an application has registered global application commands. - /// - ApplicationCommandBadge = 1 << 23, -} + +namespace DSharpPlus.Entities; + + +/// +/// Represents flags for a discord application. +/// +public enum DiscordApplicationFlags +{ + /// + /// Indicates if an application uses the Auto Moderation API. + /// + ApplicationAutoModerationRuleCreateBadge = 1 << 6, + + /// + /// Indicates that the application is approved for the intent. + /// + GatewayPresence = 1 << 12, + + /// + /// Indicates that the application is awaiting approval for the intent. + /// + GatewayPresenceLimited = 1 << 13, + + /// + /// Indicates that the application is approved for the intent. + /// + GatewayGuildMembers = 1 << 14, + + /// + /// Indicates that the application is awaiting approval for the intent. + /// + GatewayGuildMembersLimited = 1 << 15, + + /// + /// Indicates that the application is awaiting verification. + /// + VerificationPendingGuildLimit = 1 << 16, + + /// + /// Indicates that the application is a voice channel application. + /// + Embedded = 1 << 17, + + /// + /// The application can track message content. + /// + GatewayMessageContent = 1 << 18, + + /// + /// The application can track message content (limited). + /// + GatewayMessageContentLimited = 1 << 19, + + /// + /// Indicates if an application has registered global application commands. + /// + ApplicationCommandBadge = 1 << 23, +} diff --git a/DSharpPlus/Entities/Application/DiscordApplicationUpdateType.cs b/DSharpPlus/Entities/Application/DiscordApplicationUpdateType.cs index caa04c0308..e20745bb86 100644 --- a/DSharpPlus/Entities/Application/DiscordApplicationUpdateType.cs +++ b/DSharpPlus/Entities/Application/DiscordApplicationUpdateType.cs @@ -1,22 +1,22 @@ -namespace DSharpPlus.Entities; - - -/// -/// Defines the type of entity that was updated. -/// -public enum DiscordApplicationUpdateType -{ - /// - /// A role was updated. - /// - Role = 1, - /// - /// A user was updated. - /// - User = 2, - - /// - /// A channel was updated. - /// - Channel = 3 -} +namespace DSharpPlus.Entities; + + +/// +/// Defines the type of entity that was updated. +/// +public enum DiscordApplicationUpdateType +{ + /// + /// A role was updated. + /// + Role = 1, + /// + /// A user was updated. + /// + User = 2, + + /// + /// A channel was updated. + /// + Channel = 3 +} diff --git a/DSharpPlus/Entities/Application/DiscordAsset.cs b/DSharpPlus/Entities/Application/DiscordAsset.cs index 569a6988ef..e836e022a2 100644 --- a/DSharpPlus/Entities/Application/DiscordAsset.cs +++ b/DSharpPlus/Entities/Application/DiscordAsset.cs @@ -1,16 +1,16 @@ -using System; - -namespace DSharpPlus.Entities; - -public abstract class DiscordAsset -{ - /// - /// Gets the ID of this asset. - /// - public virtual string Id { get; set; } = default!; - - /// - /// Gets the URL of this asset. - /// - public abstract Uri Url { get; } -} +using System; + +namespace DSharpPlus.Entities; + +public abstract class DiscordAsset +{ + /// + /// Gets the ID of this asset. + /// + public virtual string Id { get; set; } = default!; + + /// + /// Gets the URL of this asset. + /// + public abstract Uri Url { get; } +} diff --git a/DSharpPlus/Entities/Application/DiscordOAuthScope.cs b/DSharpPlus/Entities/Application/DiscordOAuthScope.cs index a65adba7ce..f3b8b77811 100644 --- a/DSharpPlus/Entities/Application/DiscordOAuthScope.cs +++ b/DSharpPlus/Entities/Application/DiscordOAuthScope.cs @@ -1,151 +1,151 @@ -namespace DSharpPlus.Entities; - - -/// -/// Represents the possible OAuth scopes for application authorization. -/// -public enum DiscordOAuthScope -{ - /// - /// Allows /users/@me without email. - /// - Identify, - - /// - /// Enables /users/@me to return email. - /// - Email, - - /// - /// Allows /users/@me/connections to return linked third-party accounts. - /// - Connections, - - /// - /// Allows /users/@me/guilds to return basic information about all of a user's guilds. - /// - Guilds, - - /// - /// Allows /guilds/{guild.id}/members/{user.id} to be used for joining users into a guild. - /// - GuildsJoin, - - /// - /// Allows /users/@me/guilds/{guild.id}/members to return a user's member information in a guild. - /// - GuildsMembersRead, - - /// - /// Allows your app to join users into a group DM. - /// - GdmJoin, - - /// - /// For local RPC server access, this allows you to control a user's local Discord client. - /// - /// - /// This scope requires Discord approval. - /// - Rpc, - - /// - /// For local RPC server access, this allows you to receive notifications pushed to the user. - /// - /// - /// This scope requires Discord approval. - /// - RpcNotificationsRead, - - /// - /// For local RPC server access, this allows you to read a user's voice settings and listen for voice events. - /// - /// - /// This scope requires Discord approval. - /// - RpcVoiceRead, - - /// - /// For local RPC server access, this allows you to update a user's voice settings. - /// - /// - /// This scope requires Discord approval. - /// - RpcVoiceWrite, - - /// - /// For local RPC server access, this allows you to update a user's activity. - /// - /// - /// This scope requires Discord approval. - /// - RpcActivitiesWrite, - - /// - /// For OAuth2 bots, this puts the bot in the user's selected guild by default. - /// - Bot, - - /// - /// This generates a webhook that is returned in the OAuth token response for authorization code grants. - /// - WebhookIncoming, - - /// - /// For local RPC server access, this allows you to read messages from all client channels - /// (otherwise restricted to channels/guilds your application creates). - /// - MessagesRead, - - /// - /// Allows your application to upload/update builds for a user's applications. - /// - /// - /// This scope requires Discord approval. - /// - ApplicationsBuildsUpload, - - /// - /// Allows your application to read build data for a user's applications. - /// - ApplicationsBuildsRead, - - /// - /// Allows your application to use application commands in a guild. - /// - ApplicationsCommands, - - /// - /// Allows your application to read and update store data (SKUs, store listings, achievements etc.) for a user's applications. - /// - ApplicationsStoreUpdate, - - /// - /// Allows your application to read entitlements for a user's applications. - /// - ApplicationsEntitlements, - - /// - /// Allows your application to fetch data from a user's "Now Playing/Recently Played" list. - /// - /// - /// This scope requires Discord approval. - /// - ActivitiesRead, - - /// - /// Allows your application to update a user's activity. - /// - /// - /// Outside of the GameSDK activity manager, this scope requires Discord approval. - /// - ActivitiesWrite, - - /// - /// Allows your application to know a user's friends and implicit relationships. - /// - /// - /// This scope requires Discord approval. - /// - RelationshipsRead -} +namespace DSharpPlus.Entities; + + +/// +/// Represents the possible OAuth scopes for application authorization. +/// +public enum DiscordOAuthScope +{ + /// + /// Allows /users/@me without email. + /// + Identify, + + /// + /// Enables /users/@me to return email. + /// + Email, + + /// + /// Allows /users/@me/connections to return linked third-party accounts. + /// + Connections, + + /// + /// Allows /users/@me/guilds to return basic information about all of a user's guilds. + /// + Guilds, + + /// + /// Allows /guilds/{guild.id}/members/{user.id} to be used for joining users into a guild. + /// + GuildsJoin, + + /// + /// Allows /users/@me/guilds/{guild.id}/members to return a user's member information in a guild. + /// + GuildsMembersRead, + + /// + /// Allows your app to join users into a group DM. + /// + GdmJoin, + + /// + /// For local RPC server access, this allows you to control a user's local Discord client. + /// + /// + /// This scope requires Discord approval. + /// + Rpc, + + /// + /// For local RPC server access, this allows you to receive notifications pushed to the user. + /// + /// + /// This scope requires Discord approval. + /// + RpcNotificationsRead, + + /// + /// For local RPC server access, this allows you to read a user's voice settings and listen for voice events. + /// + /// + /// This scope requires Discord approval. + /// + RpcVoiceRead, + + /// + /// For local RPC server access, this allows you to update a user's voice settings. + /// + /// + /// This scope requires Discord approval. + /// + RpcVoiceWrite, + + /// + /// For local RPC server access, this allows you to update a user's activity. + /// + /// + /// This scope requires Discord approval. + /// + RpcActivitiesWrite, + + /// + /// For OAuth2 bots, this puts the bot in the user's selected guild by default. + /// + Bot, + + /// + /// This generates a webhook that is returned in the OAuth token response for authorization code grants. + /// + WebhookIncoming, + + /// + /// For local RPC server access, this allows you to read messages from all client channels + /// (otherwise restricted to channels/guilds your application creates). + /// + MessagesRead, + + /// + /// Allows your application to upload/update builds for a user's applications. + /// + /// + /// This scope requires Discord approval. + /// + ApplicationsBuildsUpload, + + /// + /// Allows your application to read build data for a user's applications. + /// + ApplicationsBuildsRead, + + /// + /// Allows your application to use application commands in a guild. + /// + ApplicationsCommands, + + /// + /// Allows your application to read and update store data (SKUs, store listings, achievements etc.) for a user's applications. + /// + ApplicationsStoreUpdate, + + /// + /// Allows your application to read entitlements for a user's applications. + /// + ApplicationsEntitlements, + + /// + /// Allows your application to fetch data from a user's "Now Playing/Recently Played" list. + /// + /// + /// This scope requires Discord approval. + /// + ActivitiesRead, + + /// + /// Allows your application to update a user's activity. + /// + /// + /// Outside of the GameSDK activity manager, this scope requires Discord approval. + /// + ActivitiesWrite, + + /// + /// Allows your application to know a user's friends and implicit relationships. + /// + /// + /// This scope requires Discord approval. + /// + RelationshipsRead +} diff --git a/DSharpPlus/Entities/Application/DiscordSpotifyAsset.cs b/DSharpPlus/Entities/Application/DiscordSpotifyAsset.cs index c4f7e07918..4179ae6149 100644 --- a/DSharpPlus/Entities/Application/DiscordSpotifyAsset.cs +++ b/DSharpPlus/Entities/Application/DiscordSpotifyAsset.cs @@ -1,23 +1,23 @@ -using System; - -namespace DSharpPlus.Entities; - -public sealed class DiscordSpotifyAsset : DiscordAsset -{ - /// - /// Gets the URL of this asset. - /// - public override Uri Url - => this.url; - - private readonly Uri url; - - public DiscordSpotifyAsset(string pId) - { - this.Id = pId; - string[] ids = this.Id.Split(':'); - string id = ids[1]; - - this.url = new Uri($"https://i.scdn.co/image/{id}"); - } -} +using System; + +namespace DSharpPlus.Entities; + +public sealed class DiscordSpotifyAsset : DiscordAsset +{ + /// + /// Gets the URL of this asset. + /// + public override Uri Url + => this.url; + + private readonly Uri url; + + public DiscordSpotifyAsset(string pId) + { + this.Id = pId; + string[] ids = this.Id.Split(':'); + string id = ids[1]; + + this.url = new Uri($"https://i.scdn.co/image/{id}"); + } +} diff --git a/DSharpPlus/Entities/AuditLogs/AuditLogActionCategory.cs b/DSharpPlus/Entities/AuditLogs/AuditLogActionCategory.cs index 7b0c998bbd..8cb3d73bb5 100644 --- a/DSharpPlus/Entities/AuditLogs/AuditLogActionCategory.cs +++ b/DSharpPlus/Entities/AuditLogs/AuditLogActionCategory.cs @@ -1,51 +1,51 @@ -// This file is part of the DSharpPlus project. -// -// Copyright (c) 2015 Mike Santiago -// Copyright (c) 2016-2025 DSharpPlus Contributors -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -namespace DSharpPlus.Entities.AuditLogs; - - -/// -/// Indicates audit log action category. -/// -public enum DiscordAuditLogActionCategory -{ - /// - /// Indicates that this action resulted in creation or addition of an object. - /// - Create, - - /// - /// Indicates that this action resulted in update of an object. - /// - Update, - - /// - /// Indicates that this action resulted in deletion or removal of an object. - /// - Delete, - - /// - /// Indicates that this action resulted in something else than creation, addition, update, deleteion, or removal of an object. - /// - Other -} +// This file is part of the DSharpPlus project. +// +// Copyright (c) 2015 Mike Santiago +// Copyright (c) 2016-2025 DSharpPlus Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +namespace DSharpPlus.Entities.AuditLogs; + + +/// +/// Indicates audit log action category. +/// +public enum DiscordAuditLogActionCategory +{ + /// + /// Indicates that this action resulted in creation or addition of an object. + /// + Create, + + /// + /// Indicates that this action resulted in update of an object. + /// + Update, + + /// + /// Indicates that this action resulted in deletion or removal of an object. + /// + Delete, + + /// + /// Indicates that this action resulted in something else than creation, addition, update, deleteion, or removal of an object. + /// + Other +} diff --git a/DSharpPlus/Entities/AuditLogs/AuditLogActionType.cs b/DSharpPlus/Entities/AuditLogs/AuditLogActionType.cs index 1c89074622..4f257758da 100644 --- a/DSharpPlus/Entities/AuditLogs/AuditLogActionType.cs +++ b/DSharpPlus/Entities/AuditLogs/AuditLogActionType.cs @@ -1,304 +1,304 @@ -// This file is part of the DSharpPlus project. -// -// Copyright (c) 2015 Mike Santiago -// Copyright (c) 2016-2025 DSharpPlus Contributors -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -namespace DSharpPlus.Entities.AuditLogs; - - -// below is taken from -// https://discord.com/developers/docs/resources/audit-log#audit-log-entry-object-audit-log-events - -/// -/// Represents type of the action that was taken in given audit log event. -/// -public enum DiscordAuditLogActionType -{ - /// - /// Indicates that the guild was updated. - /// - GuildUpdate = 1, - - /// - /// Indicates that the channel was created. - /// - ChannelCreate = 10, - - /// - /// Indicates that the channel was updated. - /// - ChannelUpdate = 11, - - /// - /// Indicates that the channel was deleted. - /// - ChannelDelete = 12, - - /// - /// Indicates that the channel permission overwrite was created. - /// - OverwriteCreate = 13, - - /// - /// Indicates that the channel permission overwrite was updated. - /// - OverwriteUpdate = 14, - - /// - /// Indicates that the channel permission overwrite was deleted. - /// - OverwriteDelete = 15, - - /// - /// Indicates that the user was kicked. - /// - Kick = 20, - - /// - /// Indicates that users were pruned. - /// - Prune = 21, - - /// - /// Indicates that the user was banned. - /// - Ban = 22, - - /// - /// Indicates that the user was unbanned. - /// - Unban = 23, - - /// - /// Indicates that the member was updated. - /// - MemberUpdate = 24, - - /// - /// Indicates that the member's roles were updated. - /// - MemberRoleUpdate = 25, - - /// - /// Indicates that the member has moved to another voice channel. - /// - MemberMove = 26, - - /// - /// Indicates that the member has disconnected from a voice channel. - /// - MemberDisconnect = 27, - - /// - /// Indicates that a bot was added to the guild. - /// - BotAdd = 28, - - /// - /// Indicates that the role was created. - /// - RoleCreate = 30, - - /// - /// Indicates that the role was updated. - /// - RoleUpdate = 31, - - /// - /// Indicates that the role was deleted. - /// - RoleDelete = 32, - - /// - /// Indicates that the invite was created. - /// - InviteCreate = 40, - - /// - /// Indicates that the invite was updated. - /// - InviteUpdate = 41, - - /// - /// Indicates that the invite was deleted. - /// - InviteDelete = 42, - - /// - /// Indicates that the webhook was created. - /// - WebhookCreate = 50, - - /// - /// Indicates that the webhook was updated. - /// - WebhookUpdate = 51, - - /// - /// Indicates that the webhook was deleted. - /// - WebhookDelete = 52, - - /// - /// Indicates that the emoji was created. - /// - EmojiCreate = 60, - - /// - /// Indicates that the emoji was updated. - /// - EmojiUpdate = 61, - - /// - /// Indicates that the emoji was deleted. - /// - EmojiDelete = 62, - - /// - /// Indicates that the message was deleted. - /// - MessageDelete = 72, - - /// - /// Indicates that messages were bulk-deleted. - /// - MessageBulkDelete = 73, - - /// - /// Indicates that a message was pinned. - /// - MessagePin = 74, - - /// - /// Indicates that a message was unpinned. - /// - MessageUnpin = 75, - - /// - /// Indicates that an integration was created. - /// - IntegrationCreate = 80, - - /// - /// Indicates that an integration was updated. - /// - IntegrationUpdate = 81, - - /// - /// Indicates that an integration was deleted. - /// - IntegrationDelete = 82, - - /// - /// Stage instance was created (stage channel becomes live) - /// - StageInstanceCreate = 83, - - /// - /// Stage instance details were updated - /// - StageInstanceUpdate = 84, - - /// - /// Stage instance was deleted (stage channel no longer live) - /// - StageInstanceDelete = 85, - - /// - /// Indicates that an sticker was created. - /// - StickerCreate = 90, - - /// - /// Indicates that an sticker was updated. - /// - StickerUpdate = 91, - - /// - /// Indicates that an sticker was deleted. - /// - StickerDelete = 92, - - /// - /// Indicates that a guild event was created. - /// - GuildScheduledEventCreate = 100, - - /// - /// Indicates that a guild event was updated. - /// - GuildScheduledEventUpdate = 101, - - /// - /// Indicates that a guild event was deleted. - /// - GuildScheduledEventDelete = 102, - - /// - /// Indicates that a thread was created. - /// - ThreadCreate = 110, - - /// - /// Indicates that a thread was updated. - /// - ThreadUpdate = 111, - - /// - /// Indicates that a thread was deleted. - /// - ThreadDelete = 112, - - /// - /// Permissions were updated for a command - /// - ApplicationCommandPermissionUpdate = 121, - - /// - /// Auto Moderation rule was created - /// - AutoModerationRuleCreate = 140, - - /// - /// Auto Moderation rule was updated - /// - AutoModerationRuleUpdate = 141, - - /// - /// Auto Moderation rule was deleted - /// - AutoModerationRuleDelete = 142, - - /// - /// Message was blocked by Auto Moderation - /// - AutoModerationBlockMessage = 143, - - /// - /// Message was flagged by Auto Moderation - /// - AutoModerationFlagToChannel = 144, - - /// - /// Member was timed out by Auto Moderation - /// - AutoModerationUserCommunicationDisabled = 145 -} +// This file is part of the DSharpPlus project. +// +// Copyright (c) 2015 Mike Santiago +// Copyright (c) 2016-2025 DSharpPlus Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +namespace DSharpPlus.Entities.AuditLogs; + + +// below is taken from +// https://discord.com/developers/docs/resources/audit-log#audit-log-entry-object-audit-log-events + +/// +/// Represents type of the action that was taken in given audit log event. +/// +public enum DiscordAuditLogActionType +{ + /// + /// Indicates that the guild was updated. + /// + GuildUpdate = 1, + + /// + /// Indicates that the channel was created. + /// + ChannelCreate = 10, + + /// + /// Indicates that the channel was updated. + /// + ChannelUpdate = 11, + + /// + /// Indicates that the channel was deleted. + /// + ChannelDelete = 12, + + /// + /// Indicates that the channel permission overwrite was created. + /// + OverwriteCreate = 13, + + /// + /// Indicates that the channel permission overwrite was updated. + /// + OverwriteUpdate = 14, + + /// + /// Indicates that the channel permission overwrite was deleted. + /// + OverwriteDelete = 15, + + /// + /// Indicates that the user was kicked. + /// + Kick = 20, + + /// + /// Indicates that users were pruned. + /// + Prune = 21, + + /// + /// Indicates that the user was banned. + /// + Ban = 22, + + /// + /// Indicates that the user was unbanned. + /// + Unban = 23, + + /// + /// Indicates that the member was updated. + /// + MemberUpdate = 24, + + /// + /// Indicates that the member's roles were updated. + /// + MemberRoleUpdate = 25, + + /// + /// Indicates that the member has moved to another voice channel. + /// + MemberMove = 26, + + /// + /// Indicates that the member has disconnected from a voice channel. + /// + MemberDisconnect = 27, + + /// + /// Indicates that a bot was added to the guild. + /// + BotAdd = 28, + + /// + /// Indicates that the role was created. + /// + RoleCreate = 30, + + /// + /// Indicates that the role was updated. + /// + RoleUpdate = 31, + + /// + /// Indicates that the role was deleted. + /// + RoleDelete = 32, + + /// + /// Indicates that the invite was created. + /// + InviteCreate = 40, + + /// + /// Indicates that the invite was updated. + /// + InviteUpdate = 41, + + /// + /// Indicates that the invite was deleted. + /// + InviteDelete = 42, + + /// + /// Indicates that the webhook was created. + /// + WebhookCreate = 50, + + /// + /// Indicates that the webhook was updated. + /// + WebhookUpdate = 51, + + /// + /// Indicates that the webhook was deleted. + /// + WebhookDelete = 52, + + /// + /// Indicates that the emoji was created. + /// + EmojiCreate = 60, + + /// + /// Indicates that the emoji was updated. + /// + EmojiUpdate = 61, + + /// + /// Indicates that the emoji was deleted. + /// + EmojiDelete = 62, + + /// + /// Indicates that the message was deleted. + /// + MessageDelete = 72, + + /// + /// Indicates that messages were bulk-deleted. + /// + MessageBulkDelete = 73, + + /// + /// Indicates that a message was pinned. + /// + MessagePin = 74, + + /// + /// Indicates that a message was unpinned. + /// + MessageUnpin = 75, + + /// + /// Indicates that an integration was created. + /// + IntegrationCreate = 80, + + /// + /// Indicates that an integration was updated. + /// + IntegrationUpdate = 81, + + /// + /// Indicates that an integration was deleted. + /// + IntegrationDelete = 82, + + /// + /// Stage instance was created (stage channel becomes live) + /// + StageInstanceCreate = 83, + + /// + /// Stage instance details were updated + /// + StageInstanceUpdate = 84, + + /// + /// Stage instance was deleted (stage channel no longer live) + /// + StageInstanceDelete = 85, + + /// + /// Indicates that an sticker was created. + /// + StickerCreate = 90, + + /// + /// Indicates that an sticker was updated. + /// + StickerUpdate = 91, + + /// + /// Indicates that an sticker was deleted. + /// + StickerDelete = 92, + + /// + /// Indicates that a guild event was created. + /// + GuildScheduledEventCreate = 100, + + /// + /// Indicates that a guild event was updated. + /// + GuildScheduledEventUpdate = 101, + + /// + /// Indicates that a guild event was deleted. + /// + GuildScheduledEventDelete = 102, + + /// + /// Indicates that a thread was created. + /// + ThreadCreate = 110, + + /// + /// Indicates that a thread was updated. + /// + ThreadUpdate = 111, + + /// + /// Indicates that a thread was deleted. + /// + ThreadDelete = 112, + + /// + /// Permissions were updated for a command + /// + ApplicationCommandPermissionUpdate = 121, + + /// + /// Auto Moderation rule was created + /// + AutoModerationRuleCreate = 140, + + /// + /// Auto Moderation rule was updated + /// + AutoModerationRuleUpdate = 141, + + /// + /// Auto Moderation rule was deleted + /// + AutoModerationRuleDelete = 142, + + /// + /// Message was blocked by Auto Moderation + /// + AutoModerationBlockMessage = 143, + + /// + /// Message was flagged by Auto Moderation + /// + AutoModerationFlagToChannel = 144, + + /// + /// Member was timed out by Auto Moderation + /// + AutoModerationUserCommunicationDisabled = 145 +} diff --git a/DSharpPlus/Entities/AuditLogs/AuditLogParser.cs b/DSharpPlus/Entities/AuditLogs/AuditLogParser.cs index f933e69f2d..975594b53a 100644 --- a/DSharpPlus/Entities/AuditLogs/AuditLogParser.cs +++ b/DSharpPlus/Entities/AuditLogs/AuditLogParser.cs @@ -1,1600 +1,1600 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Globalization; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Threading; -using System.Threading.Tasks; -using DSharpPlus.Net.Abstractions; -using DSharpPlus.Net.Serialization; -using Microsoft.Extensions.Logging; -using Newtonsoft.Json.Linq; - -namespace DSharpPlus.Entities.AuditLogs; - -internal static class AuditLogParser -{ - /// - /// Parses a AuditLog to a list of AuditLogEntries - /// - /// which is the parent of the AuditLog - /// whose entries should be parsed - /// A token to cancel the request - /// A list of . All entries which cant be parsed are dropped - internal static async IAsyncEnumerable ParseAuditLogToEntriesAsync - ( - DiscordGuild guild, - AuditLog auditLog, - [EnumeratorCancellation] - CancellationToken cancellationToken = default - ) - { - BaseDiscordClient client = guild.Discord; - - //Get all User - IEnumerable users = auditLog.Users; - - //Update cache if user is not known - foreach (DiscordUser discordUser in users) - { - discordUser.Discord = client; - if (client.UserCache.ContainsKey(discordUser.Id)) - { - continue; - } - - client.UpdateUserCache(discordUser); - } - - //get unique webhooks, scheduledEvents, threads - IEnumerable uniqueWebhooks = auditLog.Webhooks; - IEnumerable uniqueScheduledEvents = auditLog.Events; - IEnumerable uniqueThreads = auditLog.Threads; - IDictionary webhooks = uniqueWebhooks.ToDictionary(x => x.Id); - - //update event cache and create a dictionary for it - foreach (DiscordScheduledGuildEvent discordEvent in uniqueScheduledEvents) - { - if (guild.scheduledEvents.ContainsKey(discordEvent.Id)) - { - continue; - } - - guild.scheduledEvents[discordEvent.Id] = discordEvent; - } - - IDictionary events = guild.scheduledEvents; - - foreach (DiscordThreadChannel thread in uniqueThreads) - { - if (guild.threads.ContainsKey(thread.Id)) - { - continue; - } - - guild.threads[thread.Id] = thread; - } - - IDictionary threads = guild.threads; - - IEnumerable? discordMembers = users.Select - ( - user => guild.members is not null && guild.members.TryGetValue(user.Id, out DiscordMember? member) - ? member - : new DiscordMember - { - Discord = guild.Discord, - Id = user.Id, - guild_id = guild.Id - }); - - Dictionary members = discordMembers.ToDictionary(xm => xm.Id, xm => xm); - - IOrderedEnumerable? auditLogActions = auditLog.Entries.OrderByDescending(xa => xa.Id); - foreach (AuditLogAction? auditLogAction in auditLogActions) - { - if (cancellationToken.IsCancellationRequested) - { - yield break; - } - - DiscordAuditLogEntry? entry = - await ParseAuditLogEntryAsync(guild, auditLogAction, members, threads, webhooks, events); - - if (entry is null) - { - continue; - } - - yield return entry; - } - } - - /// - /// Tries to parse a AuditLogAction to a DiscordAuditLogEntry - /// - /// which is the parent of the entry - /// which should be parsed - /// A dictionary of which is used to inject the entities instead of passing the id - /// A dictionary of which is used to inject the entities instead of passing the id - /// A dictionary of which is used to inject the entities instead of passing the id - /// A dictionary of which is used to inject the entities instead of passing the id - /// Returns a . Is null if the entry can not be parsed - /// Will use guild cache for optional parameters if those are not present if possible - internal static async Task ParseAuditLogEntryAsync - ( - DiscordGuild guild, - AuditLogAction auditLogAction, - IDictionary? members = null, - IDictionary? threads = null, - IDictionary? webhooks = null, - IDictionary? events = null - ) - { - //initialize members if null - members ??= guild.members; - - //initialize threads if null - threads ??= guild.threads; - - //initialize scheduled events if null - events ??= guild.scheduledEvents; - - webhooks ??= new Dictionary(); - - DiscordAuditLogEntry? entry = null; - switch (auditLogAction.ActionType) - { - case DiscordAuditLogActionType.GuildUpdate: - entry = await ParseGuildUpdateAsync(guild, auditLogAction); - break; - - case DiscordAuditLogActionType.ChannelCreate: - case DiscordAuditLogActionType.ChannelDelete: - case DiscordAuditLogActionType.ChannelUpdate: - entry = ParseChannelEntry(guild, auditLogAction); - break; - - case DiscordAuditLogActionType.OverwriteCreate: - case DiscordAuditLogActionType.OverwriteDelete: - case DiscordAuditLogActionType.OverwriteUpdate: - entry = ParseOverwriteEntry(guild, auditLogAction); - break; - - case DiscordAuditLogActionType.Kick: - entry = new DiscordAuditLogKickEntry - { - Target = members.TryGetValue(auditLogAction.TargetId!.Value, out DiscordMember? kickMember) - ? kickMember - : new DiscordMember - { - Id = auditLogAction.TargetId.Value, - Discord = guild.Discord, - guild_id = guild.Id - } - }; - break; - - case DiscordAuditLogActionType.Prune: - entry = new DiscordAuditLogPruneEntry - { - Days = auditLogAction.Options!.DeleteMemberDays, - Toll = auditLogAction.Options!.MembersRemoved - }; - break; - - case DiscordAuditLogActionType.Ban: - case DiscordAuditLogActionType.Unban: - entry = new DiscordAuditLogBanEntry - { - Target = members.TryGetValue(auditLogAction.TargetId!.Value, out DiscordMember? unbanMember) - ? unbanMember - : new DiscordMember - { - Id = auditLogAction.TargetId.Value, - Discord = guild.Discord, - guild_id = guild.Id - } - }; - break; - - case DiscordAuditLogActionType.MemberUpdate: - case DiscordAuditLogActionType.MemberRoleUpdate: - entry = ParseMemberUpdateEntry(guild, auditLogAction); - break; - - case DiscordAuditLogActionType.RoleCreate: - case DiscordAuditLogActionType.RoleDelete: - case DiscordAuditLogActionType.RoleUpdate: - entry = ParseRoleUpdateEntry(guild, auditLogAction); - break; - - case DiscordAuditLogActionType.InviteCreate: - case DiscordAuditLogActionType.InviteDelete: - case DiscordAuditLogActionType.InviteUpdate: - entry = ParseInviteUpdateEntry(guild, auditLogAction); - break; - - case DiscordAuditLogActionType.WebhookCreate: - case DiscordAuditLogActionType.WebhookDelete: - case DiscordAuditLogActionType.WebhookUpdate: - entry = ParseWebhookUpdateEntry(guild, auditLogAction, webhooks); - break; - - case DiscordAuditLogActionType.EmojiCreate: - case DiscordAuditLogActionType.EmojiDelete: - case DiscordAuditLogActionType.EmojiUpdate: - entry = new DiscordAuditLogEmojiEntry - { - Target = guild.emojis.TryGetValue(auditLogAction.TargetId!.Value, out DiscordEmoji? target) - ? target - : new DiscordEmoji { Id = auditLogAction.TargetId.Value, Discord = guild.Discord } - }; - - DiscordAuditLogEmojiEntry emojiEntry = (DiscordAuditLogEmojiEntry)entry; - foreach (AuditLogActionChange actionChange in auditLogAction.Changes) - { - switch (actionChange.Key.ToLowerInvariant()) - { - case "name": - emojiEntry.NameChange = PropertyChange.From(actionChange); - break; - - default: - if (guild.Discord.Configuration.LogUnknownAuditlogs) - { - guild.Discord.Logger.LogWarning(LoggerEvents.AuditLog, - "Unknown key in emote update: {Key} - Please take a look at GitHub issue #1580", - actionChange.Key); - } - - break; - } - } - - break; - - case DiscordAuditLogActionType.StickerCreate: - case DiscordAuditLogActionType.StickerDelete: - case DiscordAuditLogActionType.StickerUpdate: - entry = ParseStickerUpdateEntry(guild, auditLogAction); - break; - - case DiscordAuditLogActionType.MessageDelete: - case DiscordAuditLogActionType.MessageBulkDelete: - { - entry = new DiscordAuditLogMessageEntry(); - - DiscordAuditLogMessageEntry messageEntry = (DiscordAuditLogMessageEntry)entry; - - if (auditLogAction.Options is not null) - { - messageEntry.Channel = guild.GetChannel(auditLogAction.Options.ChannelId) ?? new DiscordChannel - { - Id = auditLogAction.Options.ChannelId, - Discord = guild.Discord, - GuildId = guild.Id - }; - messageEntry.MessageCount = auditLogAction.Options.Count; - } - - if (messageEntry.Channel is not null) - { - guild.Discord.UserCache.TryGetValue(auditLogAction.UserId.Value, out DiscordUser? user); - messageEntry.Target = user ?? new DiscordUser - { - Id = auditLogAction.UserId.Value, - Discord = guild.Discord - }; - } - - break; - } - - case DiscordAuditLogActionType.MessagePin: - case DiscordAuditLogActionType.MessageUnpin: - { - entry = new DiscordAuditLogMessagePinEntry(); - - DiscordAuditLogMessagePinEntry messagePinEntry = (DiscordAuditLogMessagePinEntry)entry; - - if (guild.Discord is not DiscordClient dc) - { - break; - } - - if (auditLogAction.Options != null) - { - DiscordMessage? message = default; - dc.MessageCache?.TryGet(auditLogAction.Options.MessageId, out message); - - messagePinEntry.Channel = guild.GetChannel(auditLogAction.Options.ChannelId) ?? - new DiscordChannel - { - Id = auditLogAction.Options.ChannelId, - Discord = guild.Discord, - GuildId = guild.Id - }; - messagePinEntry.Message = message ?? new DiscordMessage - { - Id = auditLogAction.Options.MessageId, - Discord = guild.Discord - }; - } - - if (auditLogAction.TargetId.HasValue) - { - dc.UserCache.TryGetValue(auditLogAction.TargetId.Value, out DiscordUser? user); - messagePinEntry.Target = user ?? new DiscordUser - { - Id = auditLogAction.TargetId.Value, - Discord = guild.Discord - }; - } - - break; - } - - case DiscordAuditLogActionType.BotAdd: - { - entry = new DiscordAuditLogBotAddEntry(); - - if (!(guild.Discord is DiscordClient dc && auditLogAction.TargetId.HasValue)) - { - break; - } - - dc.UserCache.TryGetValue(auditLogAction.TargetId.Value, out DiscordUser? bot); - (entry as DiscordAuditLogBotAddEntry)!.TargetBot = bot - ?? new DiscordUser - { - Id = auditLogAction.TargetId.Value, - Discord = guild.Discord - }; - - break; - } - - case DiscordAuditLogActionType.MemberMove: - entry = new DiscordAuditLogMemberMoveEntry(); - - if (auditLogAction.Options == null) - { - break; - } - - DiscordAuditLogMemberMoveEntry? memberMoveEntry = (DiscordAuditLogMemberMoveEntry)entry; - - memberMoveEntry.UserCount = auditLogAction.Options.Count; - memberMoveEntry.Channel = guild.GetChannel(auditLogAction.Options.ChannelId) ?? new DiscordChannel - { - Id = auditLogAction.Options.ChannelId, - Discord = guild.Discord, - GuildId = guild.Id - }; - break; - - case DiscordAuditLogActionType.MemberDisconnect: - entry = new DiscordAuditLogMemberDisconnectEntry { UserCount = auditLogAction.Options?.Count ?? 0 }; - break; - - case DiscordAuditLogActionType.IntegrationCreate: - case DiscordAuditLogActionType.IntegrationDelete: - case DiscordAuditLogActionType.IntegrationUpdate: - entry = ParseIntegrationUpdateEntry(guild, auditLogAction); - break; - - case DiscordAuditLogActionType.GuildScheduledEventCreate: - case DiscordAuditLogActionType.GuildScheduledEventDelete: - case DiscordAuditLogActionType.GuildScheduledEventUpdate: - entry = ParseGuildScheduledEventUpdateEntry(guild, auditLogAction, events); - break; - - case DiscordAuditLogActionType.ThreadCreate: - case DiscordAuditLogActionType.ThreadDelete: - case DiscordAuditLogActionType.ThreadUpdate: - entry = ParseThreadUpdateEntry(guild, auditLogAction, threads); - break; - - case DiscordAuditLogActionType.ApplicationCommandPermissionUpdate: - entry = new DiscordAuditLogApplicationCommandPermissionEntry(); - DiscordAuditLogApplicationCommandPermissionEntry permissionEntry = - (DiscordAuditLogApplicationCommandPermissionEntry)entry; - - if (auditLogAction.Options.ApplicationId == auditLogAction.TargetId) - { - permissionEntry.ApplicationId = (ulong)auditLogAction.TargetId; - permissionEntry.ApplicationCommandId = null; - } - else - { - permissionEntry.ApplicationId = auditLogAction.Options.ApplicationId; - permissionEntry.ApplicationCommandId = auditLogAction.TargetId; - } - - permissionEntry.PermissionChanges = new List>(); - - foreach (AuditLogActionChange change in auditLogAction.Changes) - { - DiscordApplicationCommandPermission? oldValue = ((JObject?)change - .OldValue)? - .ToDiscordObject(); - - DiscordApplicationCommandPermission? newValue = ((JObject)change - .NewValue) - .ToDiscordObject(); - - permissionEntry.PermissionChanges = permissionEntry.PermissionChanges - .Append(PropertyChange.From(oldValue, newValue)); - } - - break; - - case DiscordAuditLogActionType.AutoModerationBlockMessage: - case DiscordAuditLogActionType.AutoModerationFlagToChannel: - case DiscordAuditLogActionType.AutoModerationUserCommunicationDisabled: - entry = new DiscordAuditLogAutoModerationExecutedEntry(); - - DiscordAuditLogAutoModerationExecutedEntry autoModerationEntry = - (DiscordAuditLogAutoModerationExecutedEntry)entry; - - if (auditLogAction.TargetId is not null) - { - autoModerationEntry.TargetUser = - members.TryGetValue(auditLogAction.TargetId.Value, out DiscordMember? targetMember) - ? targetMember - : new DiscordUser - { - Id = auditLogAction.TargetId.Value, - Discord = guild.Discord - }; - } - - autoModerationEntry.ResponsibleRule = auditLogAction.Options!.AutoModerationRuleName; - autoModerationEntry.Channel = guild.GetChannel(auditLogAction.Options.ChannelId); - autoModerationEntry.RuleTriggerType = - (DiscordRuleTriggerType)int.Parse(auditLogAction.Options!.AutoModerationRuleTriggerType); - break; - - case DiscordAuditLogActionType.AutoModerationRuleCreate: - case DiscordAuditLogActionType.AutoModerationRuleUpdate: - case DiscordAuditLogActionType.AutoModerationRuleDelete: - entry = ParseAutoModerationRuleUpdateEntry(guild, auditLogAction); - break; - - default: - if (guild.Discord.Configuration.LogUnknownAuditlogs) - { - guild.Discord.Logger.LogWarning(LoggerEvents.AuditLog, - "Unknown audit log action type: {type} - Please take a look at GitHub issue #1580", - (int)auditLogAction.ActionType); - } - - break; - } - - if (entry is null) - { - return null; - } - - entry.ActionCategory = auditLogAction.ActionType switch - { - DiscordAuditLogActionType.ChannelCreate or DiscordAuditLogActionType.EmojiCreate or DiscordAuditLogActionType.InviteCreate - or DiscordAuditLogActionType.OverwriteCreate or DiscordAuditLogActionType.RoleCreate - or DiscordAuditLogActionType.WebhookCreate or DiscordAuditLogActionType.IntegrationCreate - or DiscordAuditLogActionType.StickerCreate - or DiscordAuditLogActionType.AutoModerationRuleCreate => DiscordAuditLogActionCategory.Create, - - DiscordAuditLogActionType.ChannelDelete or DiscordAuditLogActionType.EmojiDelete or DiscordAuditLogActionType.InviteDelete - or DiscordAuditLogActionType.MessageDelete or DiscordAuditLogActionType.MessageBulkDelete - or DiscordAuditLogActionType.OverwriteDelete or DiscordAuditLogActionType.RoleDelete - or DiscordAuditLogActionType.WebhookDelete or DiscordAuditLogActionType.IntegrationDelete - or DiscordAuditLogActionType.StickerDelete - or DiscordAuditLogActionType.AutoModerationRuleDelete => DiscordAuditLogActionCategory.Delete, - - DiscordAuditLogActionType.ChannelUpdate or DiscordAuditLogActionType.EmojiUpdate or DiscordAuditLogActionType.InviteUpdate - or DiscordAuditLogActionType.MemberRoleUpdate or DiscordAuditLogActionType.MemberUpdate - or DiscordAuditLogActionType.OverwriteUpdate or DiscordAuditLogActionType.RoleUpdate - or DiscordAuditLogActionType.WebhookUpdate or DiscordAuditLogActionType.IntegrationUpdate - or DiscordAuditLogActionType.StickerUpdate - or DiscordAuditLogActionType.AutoModerationRuleUpdate => DiscordAuditLogActionCategory.Update, - _ => DiscordAuditLogActionCategory.Other, - }; - entry.ActionType = auditLogAction.ActionType; - entry.Id = auditLogAction.Id; - entry.Reason = auditLogAction.Reason; - entry.Discord = guild.Discord; - - entry.UserResponsible = members.TryGetValue(auditLogAction.UserId!.Value, out DiscordMember? member) - ? member - : guild.Discord.UserCache.TryGetValue(auditLogAction.UserId!.Value, out DiscordUser? discordUser) - ? discordUser - : new DiscordUser { Id = auditLogAction.UserId!.Value, Discord = guild.Discord }; - - return entry; - } - - /// - /// Parses a to a - /// - /// which is the parent of the entry - /// which should be parsed - private static DiscordAuditLogAutoModerationRuleEntry ParseAutoModerationRuleUpdateEntry(DiscordGuild guild, - AuditLogAction auditLogAction) - { - DiscordAuditLogAutoModerationRuleEntry ruleEntry = new(); - - foreach (AuditLogActionChange change in auditLogAction.Changes) - { - switch (change.Key.ToLowerInvariant()) - { - case "id": - ruleEntry.RuleId = PropertyChange.From(change); - break; - - case "guild_id": - ruleEntry.GuildId = PropertyChange.From(change); - break; - - case "name": - ruleEntry.Name = PropertyChange.From(change); - break; - - case "creator_id": - ruleEntry.CreatorId = PropertyChange.From(change); - break; - - case "event_type": - ruleEntry.EventType = PropertyChange.From(change); - break; - - case "trigger_type": - ruleEntry.TriggerType = PropertyChange.From(change); - break; - - case "trigger_metadata": - ruleEntry.TriggerMetadata = PropertyChange.From(change); - break; - - case "actions": - ruleEntry.Actions = PropertyChange?>.From(change); - break; - - case "enabled": - ruleEntry.Enabled = PropertyChange.From(change); - break; - - case "exempt_roles": - JArray oldRoleIds = (JArray)change.OldValue; - JArray newRoleIds = (JArray)change.NewValue; - - IEnumerable? oldRoles = oldRoleIds? - .Select(x => x.ToObject()) - .Select(x => guild.roles.GetValueOrDefault(x)!); - - IEnumerable? newRoles = newRoleIds? - .Select(x => x.ToObject()) - .Select(x => guild.roles.GetValueOrDefault(x)!); - - ruleEntry.ExemptRoles = - PropertyChange?>.From(oldRoles, newRoles); - break; - - case "exempt_channels": - JArray oldChannelIds = (JArray)change.OldValue; - JArray newChanelIds = (JArray)change.NewValue; - - IEnumerable? oldChannels = oldChannelIds? - .Select(x => x.ToObject()) - .Select(guild.GetChannel); - - IEnumerable? newChannels = newChanelIds? - .Select(x => x.ToObject()) - .Select(guild.GetChannel); - - ruleEntry.ExemptChannels = - PropertyChange?>.From(oldChannels, newChannels); - break; - - case "$add_keyword_filter": - ruleEntry.AddedKeywords = ((JArray)change.NewValue).Select(x => x.ToObject()); - break; - - case "$remove_keyword_filter": - ruleEntry.RemovedKeywords = ((JArray)change.NewValue).Select(x => x.ToObject()); - break; - - case "$add_regex_patterns": - ruleEntry.AddedRegexPatterns = ((JArray)change.NewValue).Select(x => x.ToObject()); - break; - - case "$remove_regex_patterns": - ruleEntry.RemovedRegexPatterns = ((JArray)change.NewValue).Select(x => x.ToObject()); - break; - - case "$add_allow_list": - ruleEntry.AddedAllowList = ((JArray)change.NewValue).Select(x => x.ToObject()); - break; - - case "$remove_allow_list": - ruleEntry.RemovedKeywords = ((JArray)change.NewValue).Select(x => x.ToObject()); - break; - - default: - if (guild.Discord.Configuration.LogUnknownAuditlogs) - { - guild.Discord.Logger.LogWarning(LoggerEvents.AuditLog, - "Unknown key in AutoModRule update: {Key} - Please take a look at GitHub issue #1580", - change.Key); - } - - break; - } - } - - return ruleEntry; - } - - /// - /// Parses a to a - /// - /// which is the parent of the entry - /// which should be parsed - /// Dictionary of to populate entry with thread entities - /// - internal static DiscordAuditLogThreadEventEntry ParseThreadUpdateEntry(DiscordGuild guild, AuditLogAction auditLogAction, - IDictionary threads) - { - DiscordAuditLogThreadEventEntry entry = new() - { - Target = - threads.TryGetValue(auditLogAction.TargetId!.Value, - out DiscordThreadChannel? channel) - ? channel - : new DiscordThreadChannel() { Id = auditLogAction.TargetId.Value, Discord = guild.Discord, GuildId = guild.Id } - }; - - foreach (AuditLogActionChange change in auditLogAction.Changes) - { - switch (change.Key.ToLowerInvariant()) - { - case "name": - entry.Name = PropertyChange.From(change); - break; - - case "type": - entry.Type = PropertyChange.From(change); - break; - - case "archived": - entry.Archived = PropertyChange.From(change); - break; - - case "auto_archive_duration": - entry.AutoArchiveDuration = PropertyChange.From(change); - break; - - case "invitable": - entry.Invitable = PropertyChange.From(change); - break; - - case "locked": - entry.Locked = PropertyChange.From(change); - break; - - case "rate_limit_per_user": - entry.PerUserRateLimit = PropertyChange.From(change); - break; - - case "flags": - entry.Flags = PropertyChange.From(change); - break; - - default: - if (guild.Discord.Configuration.LogUnknownAuditlogs) - { - guild.Discord.Logger.LogWarning(LoggerEvents.AuditLog, - "Unknown key in thread update: {Key} - Please take a look at GitHub issue #1580", - change.Key); - } - - break; - } - } - - return entry; - } - - /// - /// Parses a to a - /// - /// which is the parent of the entry - /// which should be parsed - /// Dictionary of to populate entry with event entities - /// - private static DiscordAuditLogGuildScheduledEventEntry ParseGuildScheduledEventUpdateEntry(DiscordGuild guild, - AuditLogAction auditLogAction, IDictionary events) - { - DiscordAuditLogGuildScheduledEventEntry entry = new() - { - Target = - events.TryGetValue(auditLogAction.TargetId!.Value, out DiscordScheduledGuildEvent? ta) - ? ta - : new DiscordScheduledGuildEvent() { Id = auditLogAction.Id, Discord = guild.Discord }, - }; - - foreach (AuditLogActionChange change in auditLogAction.Changes) - { - switch (change.Key.ToLowerInvariant()) - { - case "name": - entry.Name = PropertyChange.From(change); - break; - case "channel_id": - ulong.TryParse(change.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out ulong newChannelId); - entry.Channel = new PropertyChange - { - Before = - guild.GetChannel(newChannelId) ?? new DiscordChannel - { - Id = change.OldValueUlong, - Discord = guild.Discord, - GuildId = guild.Id - }, - After = guild.GetChannel(newChannelId) ?? new DiscordChannel - { - Id = change.NewValueUlong, - Discord = guild.Discord, - GuildId = guild.Id - } - }; - break; - - case "description": - entry.Description = PropertyChange.From(change); - break; - - case "entity_type": - entry.Type = PropertyChange.From(change); - break; - - case "image_hash": - entry.ImageHash = PropertyChange.From(change); - break; - - case "location": - entry.Location = PropertyChange.From(change); - break; - - case "privacy_level": - entry.PrivacyLevel = PropertyChange.From(change); - break; - - case "status": - entry.Status = PropertyChange.From(change); - break; - - default: - if (guild.Discord.Configuration.LogUnknownAuditlogs) - { - guild.Discord.Logger.LogWarning(LoggerEvents.AuditLog, - "Unknown key in scheduled event update: {Key} - Please take a look at GitHub issue #1580", - change.Key); - } - - break; - } - } - - return entry; - } - - /// - /// Parses a to a - /// - /// which is the parent of the entry - /// which should be parsed - /// - internal static async Task ParseGuildUpdateAsync(DiscordGuild guild, - AuditLogAction auditLogAction) - { - DiscordAuditLogGuildEntry entry = new() { Target = guild }; - - ulong before, after; - foreach (AuditLogActionChange? change in auditLogAction.Changes) - { - switch (change.Key.ToLowerInvariant()) - { - case "name": - entry.NameChange = PropertyChange.From(change); - break; - - case "owner_id": - entry.OwnerChange = new PropertyChange - { - Before = guild.members != null && guild.members.TryGetValue( - change.OldValueUlong, - out DiscordMember? oldMember) - ? oldMember - : await guild.GetMemberAsync(change.OldValueUlong), - After = guild.members != null && guild.members.TryGetValue(change.NewValueUlong, - out DiscordMember? newMember) - ? newMember - : await guild.GetMemberAsync(change.NewValueUlong) - }; - break; - - case "icon_hash": - entry.IconChange = new PropertyChange - { - Before = change.OldValueString != null - ? $"https://cdn.discordapp.com/icons/{guild.Id}/{change.OldValueString}.webp" - : null, - After = change.OldValueString != null - ? $"https://cdn.discordapp.com/icons/{guild.Id}/{change.NewValueString}.webp" - : null - }; - break; - - case "verification_level": - entry.VerificationLevelChange = PropertyChange.From(change); - break; - - case "afk_channel_id": - - ulong.TryParse(change.NewValue as string, NumberStyles.Integer, - CultureInfo.InvariantCulture, out before); - ulong.TryParse(change.OldValue as string, NumberStyles.Integer, - CultureInfo.InvariantCulture, out after); - - entry.AfkChannelChange = new PropertyChange - { - Before = guild.GetChannel(before) ?? new DiscordChannel - { - Id = before, - Discord = guild.Discord, - GuildId = guild.Id - }, - After = guild.GetChannel(after) ?? new DiscordChannel - { - Id = before, - Discord = guild.Discord, - GuildId = guild.Id - } - }; - break; - - case "widget_channel_id": - ulong.TryParse(change.NewValue as string, NumberStyles.Integer, - CultureInfo.InvariantCulture, out before); - ulong.TryParse(change.OldValue as string, NumberStyles.Integer, - CultureInfo.InvariantCulture, out after); - - entry.EmbedChannelChange = new PropertyChange - { - Before = guild.GetChannel(before) ?? new DiscordChannel - { - Id = before, - Discord = guild.Discord, - GuildId = guild.Id - }, - After = guild.GetChannel(after) ?? new DiscordChannel - { - Id = before, - Discord = guild.Discord, - GuildId = guild.Id - } - }; - break; - - case "splash_hash": - entry.SplashChange = new PropertyChange - { - Before = change.OldValueString != null - ? $"https://cdn.discordapp.com/splashes/{guild.Id}/{change.OldValueString}.webp?size=2048" - : null, - After = change.NewValueString != null - ? $"https://cdn.discordapp.com/splashes/{guild.Id}/{change.NewValueString}.webp?size=2048" - : null - }; - break; - - case "default_message_notifications": - entry.NotificationSettingsChange = PropertyChange.From(change); - break; - - case "system_channel_id": - ulong.TryParse(change.NewValue as string, NumberStyles.Integer, - CultureInfo.InvariantCulture, out before); - ulong.TryParse(change.OldValue as string, NumberStyles.Integer, - CultureInfo.InvariantCulture, out after); - - entry.SystemChannelChange = new PropertyChange - { - Before = guild.GetChannel(before) ?? new DiscordChannel - { - Id = before, - Discord = guild.Discord, - GuildId = guild.Id - }, - After = guild.GetChannel(after) ?? new DiscordChannel - { - Id = before, - Discord = guild.Discord, - GuildId = guild.Id - } - }; - break; - - case "explicit_content_filter": - entry.ExplicitContentFilterChange = PropertyChange.From(change); - break; - - case "mfa_level": - entry.MfaLevelChange = PropertyChange.From(change); - break; - - case "region": - entry.RegionChange = PropertyChange.From(change); - break; - - default: - if (guild.Discord.Configuration.LogUnknownAuditlogs) - { - guild.Discord.Logger.LogWarning(LoggerEvents.AuditLog, - "Unknown key in guild update: {Key} - Please take a look at GitHub issue #1580", - change.Key); - } - - break; - } - } - - return entry; - } - - /// - /// Parses a to a - /// - /// which is the parent of the entry - /// which should be parsed - /// - internal static DiscordAuditLogChannelEntry ParseChannelEntry(DiscordGuild guild, AuditLogAction auditLogAction) - { - DiscordAuditLogChannelEntry entry = new() - { - Target = guild.GetChannel(auditLogAction.TargetId!.Value) ?? new DiscordChannel - { - Id = auditLogAction.TargetId.Value, - Discord = guild.Discord, - GuildId = guild.Id - } - }; - - foreach (AuditLogActionChange? change in auditLogAction.Changes) - { - switch (change.Key.ToLowerInvariant()) - { - case "name": - entry.NameChange = PropertyChange.From(change); - break; - - case "type": - entry.TypeChange = PropertyChange.From(change); - break; - - case "permission_overwrites": - - IEnumerable? olds = change.OldValues?.OfType()? - .Select(jObject => jObject.ToDiscordObject())? - .Select(overwrite => - { - overwrite.Discord = guild.Discord; - return overwrite; - }); - - IEnumerable? news = change.NewValues?.OfType()? - .Select(jObject => jObject.ToDiscordObject())? - .Select(overwrite => - { - overwrite.Discord = guild.Discord; - return overwrite; - }); - - entry.OverwriteChange = new PropertyChange> - { - Before = olds != null - ? new ReadOnlyCollection(new List(olds)) - : null, - After = news != null - ? new ReadOnlyCollection(new List(news)) - : null - }; - break; - - case "topic": - entry.TopicChange = new PropertyChange - { - Before = change.OldValueString, - After = change.NewValueString - }; - break; - - case "nsfw": - entry.NsfwChange = PropertyChange.From(change); - break; - - case "bitrate": - entry.BitrateChange = PropertyChange.From(change); - break; - - case "rate_limit_per_user": - entry.PerUserRateLimitChange = PropertyChange.From(change); - break; - - case "user_limit": - entry.UserLimit = PropertyChange.From(change); - break; - - case "flags": - entry.Flags = PropertyChange.From(change); - break; - - case "available_tags": - IEnumerable? newTags = change.NewValues?.OfType()? - .Select(jObject => jObject.ToDiscordObject())? - .Select(forumTag => - { - forumTag.Discord = guild.Discord; - return forumTag; - }); - - IEnumerable? oldTags = change.OldValues?.OfType()? - .Select(jObject => jObject.ToDiscordObject())? - .Select(forumTag => - { - forumTag.Discord = guild.Discord; - return forumTag; - }); - - entry.AvailableTags = PropertyChange>.From(oldTags, newTags); - - break; - - default: - if (guild.Discord.Configuration.LogUnknownAuditlogs) - { - guild.Discord.Logger.LogWarning(LoggerEvents.AuditLog, - "Unknown key in channel update: {Key} - Please take a look at GitHub issue #1580", - change.Key); - } - - break; - } - } - - return entry; - } - - /// - /// Parses a to a - /// - /// which is the parent of the entry - /// which should be parsed - /// - internal static DiscordAuditLogOverwriteEntry ParseOverwriteEntry(DiscordGuild guild, - AuditLogAction auditLogAction) - { - DiscordAuditLogOverwriteEntry entry = new() - { - Target = guild - .GetChannel(auditLogAction.TargetId!.Value) - .PermissionOverwrites - .FirstOrDefault(xo => xo.Id == auditLogAction.Options.Id), - Channel = guild.GetChannel(auditLogAction.TargetId.Value) - }; - - foreach (AuditLogActionChange? change in auditLogAction.Changes) - { - switch (change.Key.ToLowerInvariant()) - { - case "deny": - entry.DeniedPermissions = PropertyChange.From(change); - break; - - case "allow": - entry.AllowedPermissions = PropertyChange.From(change); - break; - - case "type": - entry.Type = PropertyChange.From(change); - break; - - case "id": - entry.TargetIdChange = PropertyChange.From(change); - break; - - default: - if (guild.Discord.Configuration.LogUnknownAuditlogs) - { - guild.Discord.Logger.LogWarning(LoggerEvents.AuditLog, - "Unknown key in overwrite update: {Key} - Please take a look at GitHub issue #1580", - change.Key); - } - - break; - } - } - - return entry; - } - - /// - /// Parses a to a - /// - /// which is the parent of the entry - /// which should be parsed - /// - internal static DiscordAuditLogMemberUpdateEntry ParseMemberUpdateEntry(DiscordGuild guild, - AuditLogAction auditLogAction) - { - DiscordAuditLogMemberUpdateEntry entry = new() - { - Target = guild.members.TryGetValue(auditLogAction.TargetId!.Value, out DiscordMember? roleUpdMember) - ? roleUpdMember - : new DiscordMember { Id = auditLogAction.TargetId.Value, Discord = guild.Discord, guild_id = guild.Id } - }; - - foreach (AuditLogActionChange? change in auditLogAction.Changes) - { - switch (change.Key.ToLowerInvariant()) - { - case "nick": - entry.NicknameChange = PropertyChange.From(change); - break; - - case "deaf": - entry.DeafenChange = PropertyChange.From(change); - break; - - case "mute": - entry.MuteChange = PropertyChange.From(change); - break; - - case "communication_disabled_until": - entry.TimeoutChange = PropertyChange.From(change); - - break; - - case "$add": - entry.AddedRoles = - new ReadOnlyCollection(change.NewValues.Select(xo => (ulong)xo["id"]!) - .Select(gx => guild.roles.GetValueOrDefault(gx)!).ToList()); - break; - - case "$remove": - entry.RemovedRoles = - new ReadOnlyCollection(change.NewValues.Select(xo => (ulong)xo["id"]!) - .Select(x => guild.roles.GetValueOrDefault(x)!).ToList()); - break; - - default: - if (guild.Discord.Configuration.LogUnknownAuditlogs) - { - guild.Discord.Logger.LogWarning(LoggerEvents.AuditLog, - "Unknown key in member update: {Key} - Please take a look at GitHub issue #1580", - change.Key); - } - - break; - } - } - - return entry; - } - - /// - /// Parses a to a - /// - /// which is the parent of the entry - /// which should be parsed - /// - internal static DiscordAuditLogRoleUpdateEntry ParseRoleUpdateEntry(DiscordGuild guild, - AuditLogAction auditLogAction) - { - DiscordAuditLogRoleUpdateEntry entry = new() - { - Target = guild.Roles.GetValueOrDefault(auditLogAction.TargetId!.Value) ?? - new DiscordRole { Id = auditLogAction.TargetId.Value, Discord = guild.Discord } - }; - - foreach (AuditLogActionChange? change in auditLogAction.Changes) - { - switch (change.Key.ToLowerInvariant()) - { - case "name": - entry.NameChange = PropertyChange.From(change); - break; - - case "color": - entry.ColorChange = PropertyChange.From(change); - break; - - case "permissions": - entry.PermissionChange = PropertyChange.From(change); - break; - - case "position": - entry.PositionChange = PropertyChange.From(change); - break; - - case "mentionable": - entry.MentionableChange = PropertyChange.From(change); - break; - - case "hoist": - entry.HoistChange = PropertyChange.From(change); - break; - - default: - if (guild.Discord.Configuration.LogUnknownAuditlogs) - { - guild.Discord.Logger.LogWarning(LoggerEvents.AuditLog, - "Unknown key in role update: {Key} - Please take a look at GitHub issue #1580", - change.Key); - } - - break; - } - } - - return entry; - } - - /// - /// Parses a to a - /// - /// which is the parent of the entry - /// which should be parsed - /// - internal static DiscordAuditLogInviteEntry ParseInviteUpdateEntry(DiscordGuild guild, AuditLogAction auditLogAction) - { - DiscordAuditLogInviteEntry entry = new(); - - DiscordInvite invite = new() - { - Discord = guild.Discord, - Guild = new DiscordInviteGuild - { - Discord = guild.Discord, - Id = guild.Id, - Name = guild.Name, - SplashHash = guild.SplashHash - } - }; - - bool boolBefore, boolAfter; - ulong ulongBefore, ulongAfter; - int intBefore, intAfter; - foreach (AuditLogActionChange? change in auditLogAction.Changes) - { - switch (change.Key.ToLowerInvariant()) - { - case "max_age": - entry.MaxAgeChange = PropertyChange.From(change); - break; - - case "code": - invite.Code = change.OldValueString ?? change.NewValueString; - - entry.CodeChange = PropertyChange.From(change); - break; - - case "temporary": - entry.TemporaryChange = new PropertyChange - { - Before = change.OldValue != null ? (bool?)change.OldValue : null, - After = change.NewValue != null ? (bool?)change.NewValue : null - }; - break; - - case "inviter_id": - boolBefore = ulong.TryParse(change.OldValue as string, NumberStyles.Integer, - CultureInfo.InvariantCulture, - out ulongBefore); - boolAfter = ulong.TryParse(change.NewValue as string, NumberStyles.Integer, - CultureInfo.InvariantCulture, - out ulongAfter); - - entry.InviterChange = new PropertyChange - { - Before = guild.members.TryGetValue(ulongBefore, out DiscordMember? propBeforeMember) - ? propBeforeMember - : new DiscordMember { Id = ulongBefore, Discord = guild.Discord, guild_id = guild.Id }, - After = guild.members.TryGetValue(ulongAfter, out DiscordMember? propAfterMember) - ? propAfterMember - : new DiscordMember { Id = ulongBefore, Discord = guild.Discord, guild_id = guild.Id } - }; - break; - - case "channel_id": - boolBefore = ulong.TryParse(change.OldValue as string, NumberStyles.Integer, - CultureInfo.InvariantCulture, - out ulongBefore); - boolAfter = ulong.TryParse(change.NewValue as string, NumberStyles.Integer, - CultureInfo.InvariantCulture, - out ulongAfter); - - entry.ChannelChange = new PropertyChange - { - Before = boolBefore - ? guild.GetChannel(ulongBefore) ?? - new DiscordChannel { Id = ulongBefore, Discord = guild.Discord, GuildId = guild.Id } - : null, - After = boolAfter - ? guild.GetChannel(ulongAfter) ?? - new DiscordChannel { Id = ulongBefore, Discord = guild.Discord, GuildId = guild.Id } - : null - }; - - DiscordChannel? channel = entry.ChannelChange.Before ?? entry.ChannelChange.After; - DiscordChannelType? channelType = channel?.Type; - invite.Channel = new DiscordInviteChannel - { - Discord = guild.Discord, - Id = boolBefore ? ulongBefore : ulongAfter, - Name = channel?.Name ?? "", - Type = channelType != null ? channelType.Value : DiscordChannelType.Unknown - }; - break; - - case "uses": - boolBefore = int.TryParse(change.OldValue as string, NumberStyles.Integer, - CultureInfo.InvariantCulture, - out intBefore); - boolAfter = int.TryParse(change.OldValue as string, NumberStyles.Integer, - CultureInfo.InvariantCulture, - out intAfter); - - entry.UsesChange = new PropertyChange - { - Before = boolBefore ? intBefore : null, - After = boolAfter ? intAfter : null - }; - break; - - case "max_uses": - boolBefore = int.TryParse(change.OldValue as string, NumberStyles.Integer, - CultureInfo.InvariantCulture, - out intBefore); - boolAfter = int.TryParse(change.OldValue as string, NumberStyles.Integer, - CultureInfo.InvariantCulture, - out intAfter); - - entry.MaxUsesChange = new PropertyChange - { - Before = boolBefore ? intBefore : null, - After = boolAfter ? intAfter : null - }; - break; - - default: - if (guild.Discord.Configuration.LogUnknownAuditlogs) - { - guild.Discord.Logger.LogWarning(LoggerEvents.AuditLog, - "Unknown key in invite update: {Key} - Please take a look at GitHub issue #1580", - change.Key); - } - - break; - } - } - - entry.Target = invite; - return entry; - } - - /// - /// Parses a to a - /// - /// which is the parent of the entry - /// which should be parsed - /// Dictionary of to populate entry with webhook entities - /// - internal static DiscordAuditLogWebhookEntry ParseWebhookUpdateEntry - ( - DiscordGuild guild, - AuditLogAction auditLogAction, - IDictionary webhooks - ) - { - DiscordAuditLogWebhookEntry entry = new() - { - Target = webhooks.TryGetValue(auditLogAction.TargetId!.Value, out DiscordWebhook? webhook) - ? webhook - : new DiscordWebhook { Id = auditLogAction.TargetId.Value, Discord = guild.Discord } - }; - - ulong ulongBefore, ulongAfter; - bool boolBefore, boolAfter; - foreach (AuditLogActionChange change in auditLogAction.Changes) - { - switch (change.Key.ToLowerInvariant()) - { - case "name": - entry.NameChange = PropertyChange.From(change); - break; - - case "channel_id": - boolBefore = ulong.TryParse(change.OldValue as string, NumberStyles.Integer, - CultureInfo.InvariantCulture, out ulongBefore); - boolAfter = ulong.TryParse(change.NewValue as string, NumberStyles.Integer, - CultureInfo.InvariantCulture, out ulongAfter); - - entry.ChannelChange = new PropertyChange - { - Before = - boolBefore - ? guild.GetChannel(ulongBefore) ?? new DiscordChannel - { - Id = ulongBefore, - Discord = guild.Discord, - GuildId = guild.Id - } - : null, - After = boolAfter - ? guild.GetChannel(ulongAfter) ?? - new DiscordChannel { Id = ulongBefore, Discord = guild.Discord, GuildId = guild.Id } - : null - }; - break; - - case "type": - entry.TypeChange = PropertyChange.From(change); - break; - - case "avatar_hash": - entry.AvatarHashChange = PropertyChange.From(change); - break; - - case "application_id" - : //Why the fuck does discord send this as a string if it's supposed to be a snowflake - entry.ApplicationIdChange = PropertyChange.From(change); - break; - - default: - if (guild.Discord.Configuration.LogUnknownAuditlogs) - { - guild.Discord.Logger.LogWarning(LoggerEvents.AuditLog, - "Unknown key in webhook update: {Key} - Please take a look at GitHub issue #1580", - change.Key); - } - - break; - } - } - - return entry; - } - - /// - /// Parses a to a - /// - /// which is the parent of the entry - /// which should be parsed - /// - internal static DiscordAuditLogStickerEntry ParseStickerUpdateEntry(DiscordGuild guild, AuditLogAction auditLogAction) - { - DiscordAuditLogStickerEntry entry = new() - { - Target = guild.stickers.TryGetValue(auditLogAction.TargetId!.Value, out DiscordMessageSticker? sticker) - ? sticker - : new DiscordMessageSticker { Id = auditLogAction.TargetId.Value, Discord = guild.Discord } - }; - - foreach (AuditLogActionChange change in auditLogAction.Changes) - { - switch (change.Key.ToLowerInvariant()) - { - case "name": - entry.NameChange = PropertyChange.From(change); - break; - - case "description": - entry.DescriptionChange = PropertyChange.From(change); - break; - - case "tags": - entry.TagsChange = PropertyChange.From(change); - break; - - case "guild_id": - entry.GuildIdChange = PropertyChange.From(change); - break; - - case "available": - entry.AvailabilityChange = PropertyChange.From(change); - break; - - case "asset": - entry.AssetChange = PropertyChange.From(change); - break; - - case "id": - entry.IdChange = PropertyChange.From(change); - break; - - case "type": - entry.TypeChange = PropertyChange.From(change); - break; - - case "format_type": - entry.FormatChange = PropertyChange.From(change); - break; - - default: - if (guild.Discord.Configuration.LogUnknownAuditlogs) - { - guild.Discord.Logger.LogWarning(LoggerEvents.AuditLog, - "Unknown key in sticker update: {Key} - Please take a look at GitHub issue #1580", - change.Key); - } - - break; - } - } - - return entry; - } - - /// - /// Parses a to a - /// - /// which is the parent of the entry - /// which should be parsed - /// - internal static DiscordAuditLogIntegrationEntry ParseIntegrationUpdateEntry(DiscordGuild guild, AuditLogAction auditLogAction) - { - DiscordAuditLogIntegrationEntry entry = new(); - - foreach (AuditLogActionChange change in auditLogAction.Changes) - { - switch (change.Key.ToLowerInvariant()) - { - case "enable_emoticons": - entry.EnableEmoticons = PropertyChange.From(change); - break; - - case "expire_behavior": - entry.ExpireBehavior = PropertyChange.From(change); - break; - - case "expire_grace_period": - entry.ExpireBehavior = PropertyChange.From(change); - break; - - case "name": - entry.Name = PropertyChange.From(change); - break; - - case "type": - entry.Type = PropertyChange.From(change); - break; - - default: - if (guild.Discord.Configuration.LogUnknownAuditlogs) - { - guild.Discord.Logger.LogWarning(LoggerEvents.AuditLog, - "Unknown key in integration update: {Key} - Please take a look at GitHub issue #1580", - change.Key); - } - - break; - } - } - - return entry; - } -} +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Globalization; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using DSharpPlus.Net.Abstractions; +using DSharpPlus.Net.Serialization; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json.Linq; + +namespace DSharpPlus.Entities.AuditLogs; + +internal static class AuditLogParser +{ + /// + /// Parses a AuditLog to a list of AuditLogEntries + /// + /// which is the parent of the AuditLog + /// whose entries should be parsed + /// A token to cancel the request + /// A list of . All entries which cant be parsed are dropped + internal static async IAsyncEnumerable ParseAuditLogToEntriesAsync + ( + DiscordGuild guild, + AuditLog auditLog, + [EnumeratorCancellation] + CancellationToken cancellationToken = default + ) + { + BaseDiscordClient client = guild.Discord; + + //Get all User + IEnumerable users = auditLog.Users; + + //Update cache if user is not known + foreach (DiscordUser discordUser in users) + { + discordUser.Discord = client; + if (client.UserCache.ContainsKey(discordUser.Id)) + { + continue; + } + + client.UpdateUserCache(discordUser); + } + + //get unique webhooks, scheduledEvents, threads + IEnumerable uniqueWebhooks = auditLog.Webhooks; + IEnumerable uniqueScheduledEvents = auditLog.Events; + IEnumerable uniqueThreads = auditLog.Threads; + IDictionary webhooks = uniqueWebhooks.ToDictionary(x => x.Id); + + //update event cache and create a dictionary for it + foreach (DiscordScheduledGuildEvent discordEvent in uniqueScheduledEvents) + { + if (guild.scheduledEvents.ContainsKey(discordEvent.Id)) + { + continue; + } + + guild.scheduledEvents[discordEvent.Id] = discordEvent; + } + + IDictionary events = guild.scheduledEvents; + + foreach (DiscordThreadChannel thread in uniqueThreads) + { + if (guild.threads.ContainsKey(thread.Id)) + { + continue; + } + + guild.threads[thread.Id] = thread; + } + + IDictionary threads = guild.threads; + + IEnumerable? discordMembers = users.Select + ( + user => guild.members is not null && guild.members.TryGetValue(user.Id, out DiscordMember? member) + ? member + : new DiscordMember + { + Discord = guild.Discord, + Id = user.Id, + guild_id = guild.Id + }); + + Dictionary members = discordMembers.ToDictionary(xm => xm.Id, xm => xm); + + IOrderedEnumerable? auditLogActions = auditLog.Entries.OrderByDescending(xa => xa.Id); + foreach (AuditLogAction? auditLogAction in auditLogActions) + { + if (cancellationToken.IsCancellationRequested) + { + yield break; + } + + DiscordAuditLogEntry? entry = + await ParseAuditLogEntryAsync(guild, auditLogAction, members, threads, webhooks, events); + + if (entry is null) + { + continue; + } + + yield return entry; + } + } + + /// + /// Tries to parse a AuditLogAction to a DiscordAuditLogEntry + /// + /// which is the parent of the entry + /// which should be parsed + /// A dictionary of which is used to inject the entities instead of passing the id + /// A dictionary of which is used to inject the entities instead of passing the id + /// A dictionary of which is used to inject the entities instead of passing the id + /// A dictionary of which is used to inject the entities instead of passing the id + /// Returns a . Is null if the entry can not be parsed + /// Will use guild cache for optional parameters if those are not present if possible + internal static async Task ParseAuditLogEntryAsync + ( + DiscordGuild guild, + AuditLogAction auditLogAction, + IDictionary? members = null, + IDictionary? threads = null, + IDictionary? webhooks = null, + IDictionary? events = null + ) + { + //initialize members if null + members ??= guild.members; + + //initialize threads if null + threads ??= guild.threads; + + //initialize scheduled events if null + events ??= guild.scheduledEvents; + + webhooks ??= new Dictionary(); + + DiscordAuditLogEntry? entry = null; + switch (auditLogAction.ActionType) + { + case DiscordAuditLogActionType.GuildUpdate: + entry = await ParseGuildUpdateAsync(guild, auditLogAction); + break; + + case DiscordAuditLogActionType.ChannelCreate: + case DiscordAuditLogActionType.ChannelDelete: + case DiscordAuditLogActionType.ChannelUpdate: + entry = ParseChannelEntry(guild, auditLogAction); + break; + + case DiscordAuditLogActionType.OverwriteCreate: + case DiscordAuditLogActionType.OverwriteDelete: + case DiscordAuditLogActionType.OverwriteUpdate: + entry = ParseOverwriteEntry(guild, auditLogAction); + break; + + case DiscordAuditLogActionType.Kick: + entry = new DiscordAuditLogKickEntry + { + Target = members.TryGetValue(auditLogAction.TargetId!.Value, out DiscordMember? kickMember) + ? kickMember + : new DiscordMember + { + Id = auditLogAction.TargetId.Value, + Discord = guild.Discord, + guild_id = guild.Id + } + }; + break; + + case DiscordAuditLogActionType.Prune: + entry = new DiscordAuditLogPruneEntry + { + Days = auditLogAction.Options!.DeleteMemberDays, + Toll = auditLogAction.Options!.MembersRemoved + }; + break; + + case DiscordAuditLogActionType.Ban: + case DiscordAuditLogActionType.Unban: + entry = new DiscordAuditLogBanEntry + { + Target = members.TryGetValue(auditLogAction.TargetId!.Value, out DiscordMember? unbanMember) + ? unbanMember + : new DiscordMember + { + Id = auditLogAction.TargetId.Value, + Discord = guild.Discord, + guild_id = guild.Id + } + }; + break; + + case DiscordAuditLogActionType.MemberUpdate: + case DiscordAuditLogActionType.MemberRoleUpdate: + entry = ParseMemberUpdateEntry(guild, auditLogAction); + break; + + case DiscordAuditLogActionType.RoleCreate: + case DiscordAuditLogActionType.RoleDelete: + case DiscordAuditLogActionType.RoleUpdate: + entry = ParseRoleUpdateEntry(guild, auditLogAction); + break; + + case DiscordAuditLogActionType.InviteCreate: + case DiscordAuditLogActionType.InviteDelete: + case DiscordAuditLogActionType.InviteUpdate: + entry = ParseInviteUpdateEntry(guild, auditLogAction); + break; + + case DiscordAuditLogActionType.WebhookCreate: + case DiscordAuditLogActionType.WebhookDelete: + case DiscordAuditLogActionType.WebhookUpdate: + entry = ParseWebhookUpdateEntry(guild, auditLogAction, webhooks); + break; + + case DiscordAuditLogActionType.EmojiCreate: + case DiscordAuditLogActionType.EmojiDelete: + case DiscordAuditLogActionType.EmojiUpdate: + entry = new DiscordAuditLogEmojiEntry + { + Target = guild.emojis.TryGetValue(auditLogAction.TargetId!.Value, out DiscordEmoji? target) + ? target + : new DiscordEmoji { Id = auditLogAction.TargetId.Value, Discord = guild.Discord } + }; + + DiscordAuditLogEmojiEntry emojiEntry = (DiscordAuditLogEmojiEntry)entry; + foreach (AuditLogActionChange actionChange in auditLogAction.Changes) + { + switch (actionChange.Key.ToLowerInvariant()) + { + case "name": + emojiEntry.NameChange = PropertyChange.From(actionChange); + break; + + default: + if (guild.Discord.Configuration.LogUnknownAuditlogs) + { + guild.Discord.Logger.LogWarning(LoggerEvents.AuditLog, + "Unknown key in emote update: {Key} - Please take a look at GitHub issue #1580", + actionChange.Key); + } + + break; + } + } + + break; + + case DiscordAuditLogActionType.StickerCreate: + case DiscordAuditLogActionType.StickerDelete: + case DiscordAuditLogActionType.StickerUpdate: + entry = ParseStickerUpdateEntry(guild, auditLogAction); + break; + + case DiscordAuditLogActionType.MessageDelete: + case DiscordAuditLogActionType.MessageBulkDelete: + { + entry = new DiscordAuditLogMessageEntry(); + + DiscordAuditLogMessageEntry messageEntry = (DiscordAuditLogMessageEntry)entry; + + if (auditLogAction.Options is not null) + { + messageEntry.Channel = guild.GetChannel(auditLogAction.Options.ChannelId) ?? new DiscordChannel + { + Id = auditLogAction.Options.ChannelId, + Discord = guild.Discord, + GuildId = guild.Id + }; + messageEntry.MessageCount = auditLogAction.Options.Count; + } + + if (messageEntry.Channel is not null) + { + guild.Discord.UserCache.TryGetValue(auditLogAction.UserId.Value, out DiscordUser? user); + messageEntry.Target = user ?? new DiscordUser + { + Id = auditLogAction.UserId.Value, + Discord = guild.Discord + }; + } + + break; + } + + case DiscordAuditLogActionType.MessagePin: + case DiscordAuditLogActionType.MessageUnpin: + { + entry = new DiscordAuditLogMessagePinEntry(); + + DiscordAuditLogMessagePinEntry messagePinEntry = (DiscordAuditLogMessagePinEntry)entry; + + if (guild.Discord is not DiscordClient dc) + { + break; + } + + if (auditLogAction.Options != null) + { + DiscordMessage? message = default; + dc.MessageCache?.TryGet(auditLogAction.Options.MessageId, out message); + + messagePinEntry.Channel = guild.GetChannel(auditLogAction.Options.ChannelId) ?? + new DiscordChannel + { + Id = auditLogAction.Options.ChannelId, + Discord = guild.Discord, + GuildId = guild.Id + }; + messagePinEntry.Message = message ?? new DiscordMessage + { + Id = auditLogAction.Options.MessageId, + Discord = guild.Discord + }; + } + + if (auditLogAction.TargetId.HasValue) + { + dc.UserCache.TryGetValue(auditLogAction.TargetId.Value, out DiscordUser? user); + messagePinEntry.Target = user ?? new DiscordUser + { + Id = auditLogAction.TargetId.Value, + Discord = guild.Discord + }; + } + + break; + } + + case DiscordAuditLogActionType.BotAdd: + { + entry = new DiscordAuditLogBotAddEntry(); + + if (!(guild.Discord is DiscordClient dc && auditLogAction.TargetId.HasValue)) + { + break; + } + + dc.UserCache.TryGetValue(auditLogAction.TargetId.Value, out DiscordUser? bot); + (entry as DiscordAuditLogBotAddEntry)!.TargetBot = bot + ?? new DiscordUser + { + Id = auditLogAction.TargetId.Value, + Discord = guild.Discord + }; + + break; + } + + case DiscordAuditLogActionType.MemberMove: + entry = new DiscordAuditLogMemberMoveEntry(); + + if (auditLogAction.Options == null) + { + break; + } + + DiscordAuditLogMemberMoveEntry? memberMoveEntry = (DiscordAuditLogMemberMoveEntry)entry; + + memberMoveEntry.UserCount = auditLogAction.Options.Count; + memberMoveEntry.Channel = guild.GetChannel(auditLogAction.Options.ChannelId) ?? new DiscordChannel + { + Id = auditLogAction.Options.ChannelId, + Discord = guild.Discord, + GuildId = guild.Id + }; + break; + + case DiscordAuditLogActionType.MemberDisconnect: + entry = new DiscordAuditLogMemberDisconnectEntry { UserCount = auditLogAction.Options?.Count ?? 0 }; + break; + + case DiscordAuditLogActionType.IntegrationCreate: + case DiscordAuditLogActionType.IntegrationDelete: + case DiscordAuditLogActionType.IntegrationUpdate: + entry = ParseIntegrationUpdateEntry(guild, auditLogAction); + break; + + case DiscordAuditLogActionType.GuildScheduledEventCreate: + case DiscordAuditLogActionType.GuildScheduledEventDelete: + case DiscordAuditLogActionType.GuildScheduledEventUpdate: + entry = ParseGuildScheduledEventUpdateEntry(guild, auditLogAction, events); + break; + + case DiscordAuditLogActionType.ThreadCreate: + case DiscordAuditLogActionType.ThreadDelete: + case DiscordAuditLogActionType.ThreadUpdate: + entry = ParseThreadUpdateEntry(guild, auditLogAction, threads); + break; + + case DiscordAuditLogActionType.ApplicationCommandPermissionUpdate: + entry = new DiscordAuditLogApplicationCommandPermissionEntry(); + DiscordAuditLogApplicationCommandPermissionEntry permissionEntry = + (DiscordAuditLogApplicationCommandPermissionEntry)entry; + + if (auditLogAction.Options.ApplicationId == auditLogAction.TargetId) + { + permissionEntry.ApplicationId = (ulong)auditLogAction.TargetId; + permissionEntry.ApplicationCommandId = null; + } + else + { + permissionEntry.ApplicationId = auditLogAction.Options.ApplicationId; + permissionEntry.ApplicationCommandId = auditLogAction.TargetId; + } + + permissionEntry.PermissionChanges = new List>(); + + foreach (AuditLogActionChange change in auditLogAction.Changes) + { + DiscordApplicationCommandPermission? oldValue = ((JObject?)change + .OldValue)? + .ToDiscordObject(); + + DiscordApplicationCommandPermission? newValue = ((JObject)change + .NewValue) + .ToDiscordObject(); + + permissionEntry.PermissionChanges = permissionEntry.PermissionChanges + .Append(PropertyChange.From(oldValue, newValue)); + } + + break; + + case DiscordAuditLogActionType.AutoModerationBlockMessage: + case DiscordAuditLogActionType.AutoModerationFlagToChannel: + case DiscordAuditLogActionType.AutoModerationUserCommunicationDisabled: + entry = new DiscordAuditLogAutoModerationExecutedEntry(); + + DiscordAuditLogAutoModerationExecutedEntry autoModerationEntry = + (DiscordAuditLogAutoModerationExecutedEntry)entry; + + if (auditLogAction.TargetId is not null) + { + autoModerationEntry.TargetUser = + members.TryGetValue(auditLogAction.TargetId.Value, out DiscordMember? targetMember) + ? targetMember + : new DiscordUser + { + Id = auditLogAction.TargetId.Value, + Discord = guild.Discord + }; + } + + autoModerationEntry.ResponsibleRule = auditLogAction.Options!.AutoModerationRuleName; + autoModerationEntry.Channel = guild.GetChannel(auditLogAction.Options.ChannelId); + autoModerationEntry.RuleTriggerType = + (DiscordRuleTriggerType)int.Parse(auditLogAction.Options!.AutoModerationRuleTriggerType); + break; + + case DiscordAuditLogActionType.AutoModerationRuleCreate: + case DiscordAuditLogActionType.AutoModerationRuleUpdate: + case DiscordAuditLogActionType.AutoModerationRuleDelete: + entry = ParseAutoModerationRuleUpdateEntry(guild, auditLogAction); + break; + + default: + if (guild.Discord.Configuration.LogUnknownAuditlogs) + { + guild.Discord.Logger.LogWarning(LoggerEvents.AuditLog, + "Unknown audit log action type: {type} - Please take a look at GitHub issue #1580", + (int)auditLogAction.ActionType); + } + + break; + } + + if (entry is null) + { + return null; + } + + entry.ActionCategory = auditLogAction.ActionType switch + { + DiscordAuditLogActionType.ChannelCreate or DiscordAuditLogActionType.EmojiCreate or DiscordAuditLogActionType.InviteCreate + or DiscordAuditLogActionType.OverwriteCreate or DiscordAuditLogActionType.RoleCreate + or DiscordAuditLogActionType.WebhookCreate or DiscordAuditLogActionType.IntegrationCreate + or DiscordAuditLogActionType.StickerCreate + or DiscordAuditLogActionType.AutoModerationRuleCreate => DiscordAuditLogActionCategory.Create, + + DiscordAuditLogActionType.ChannelDelete or DiscordAuditLogActionType.EmojiDelete or DiscordAuditLogActionType.InviteDelete + or DiscordAuditLogActionType.MessageDelete or DiscordAuditLogActionType.MessageBulkDelete + or DiscordAuditLogActionType.OverwriteDelete or DiscordAuditLogActionType.RoleDelete + or DiscordAuditLogActionType.WebhookDelete or DiscordAuditLogActionType.IntegrationDelete + or DiscordAuditLogActionType.StickerDelete + or DiscordAuditLogActionType.AutoModerationRuleDelete => DiscordAuditLogActionCategory.Delete, + + DiscordAuditLogActionType.ChannelUpdate or DiscordAuditLogActionType.EmojiUpdate or DiscordAuditLogActionType.InviteUpdate + or DiscordAuditLogActionType.MemberRoleUpdate or DiscordAuditLogActionType.MemberUpdate + or DiscordAuditLogActionType.OverwriteUpdate or DiscordAuditLogActionType.RoleUpdate + or DiscordAuditLogActionType.WebhookUpdate or DiscordAuditLogActionType.IntegrationUpdate + or DiscordAuditLogActionType.StickerUpdate + or DiscordAuditLogActionType.AutoModerationRuleUpdate => DiscordAuditLogActionCategory.Update, + _ => DiscordAuditLogActionCategory.Other, + }; + entry.ActionType = auditLogAction.ActionType; + entry.Id = auditLogAction.Id; + entry.Reason = auditLogAction.Reason; + entry.Discord = guild.Discord; + + entry.UserResponsible = members.TryGetValue(auditLogAction.UserId!.Value, out DiscordMember? member) + ? member + : guild.Discord.UserCache.TryGetValue(auditLogAction.UserId!.Value, out DiscordUser? discordUser) + ? discordUser + : new DiscordUser { Id = auditLogAction.UserId!.Value, Discord = guild.Discord }; + + return entry; + } + + /// + /// Parses a to a + /// + /// which is the parent of the entry + /// which should be parsed + private static DiscordAuditLogAutoModerationRuleEntry ParseAutoModerationRuleUpdateEntry(DiscordGuild guild, + AuditLogAction auditLogAction) + { + DiscordAuditLogAutoModerationRuleEntry ruleEntry = new(); + + foreach (AuditLogActionChange change in auditLogAction.Changes) + { + switch (change.Key.ToLowerInvariant()) + { + case "id": + ruleEntry.RuleId = PropertyChange.From(change); + break; + + case "guild_id": + ruleEntry.GuildId = PropertyChange.From(change); + break; + + case "name": + ruleEntry.Name = PropertyChange.From(change); + break; + + case "creator_id": + ruleEntry.CreatorId = PropertyChange.From(change); + break; + + case "event_type": + ruleEntry.EventType = PropertyChange.From(change); + break; + + case "trigger_type": + ruleEntry.TriggerType = PropertyChange.From(change); + break; + + case "trigger_metadata": + ruleEntry.TriggerMetadata = PropertyChange.From(change); + break; + + case "actions": + ruleEntry.Actions = PropertyChange?>.From(change); + break; + + case "enabled": + ruleEntry.Enabled = PropertyChange.From(change); + break; + + case "exempt_roles": + JArray oldRoleIds = (JArray)change.OldValue; + JArray newRoleIds = (JArray)change.NewValue; + + IEnumerable? oldRoles = oldRoleIds? + .Select(x => x.ToObject()) + .Select(x => guild.roles.GetValueOrDefault(x)!); + + IEnumerable? newRoles = newRoleIds? + .Select(x => x.ToObject()) + .Select(x => guild.roles.GetValueOrDefault(x)!); + + ruleEntry.ExemptRoles = + PropertyChange?>.From(oldRoles, newRoles); + break; + + case "exempt_channels": + JArray oldChannelIds = (JArray)change.OldValue; + JArray newChanelIds = (JArray)change.NewValue; + + IEnumerable? oldChannels = oldChannelIds? + .Select(x => x.ToObject()) + .Select(guild.GetChannel); + + IEnumerable? newChannels = newChanelIds? + .Select(x => x.ToObject()) + .Select(guild.GetChannel); + + ruleEntry.ExemptChannels = + PropertyChange?>.From(oldChannels, newChannels); + break; + + case "$add_keyword_filter": + ruleEntry.AddedKeywords = ((JArray)change.NewValue).Select(x => x.ToObject()); + break; + + case "$remove_keyword_filter": + ruleEntry.RemovedKeywords = ((JArray)change.NewValue).Select(x => x.ToObject()); + break; + + case "$add_regex_patterns": + ruleEntry.AddedRegexPatterns = ((JArray)change.NewValue).Select(x => x.ToObject()); + break; + + case "$remove_regex_patterns": + ruleEntry.RemovedRegexPatterns = ((JArray)change.NewValue).Select(x => x.ToObject()); + break; + + case "$add_allow_list": + ruleEntry.AddedAllowList = ((JArray)change.NewValue).Select(x => x.ToObject()); + break; + + case "$remove_allow_list": + ruleEntry.RemovedKeywords = ((JArray)change.NewValue).Select(x => x.ToObject()); + break; + + default: + if (guild.Discord.Configuration.LogUnknownAuditlogs) + { + guild.Discord.Logger.LogWarning(LoggerEvents.AuditLog, + "Unknown key in AutoModRule update: {Key} - Please take a look at GitHub issue #1580", + change.Key); + } + + break; + } + } + + return ruleEntry; + } + + /// + /// Parses a to a + /// + /// which is the parent of the entry + /// which should be parsed + /// Dictionary of to populate entry with thread entities + /// + internal static DiscordAuditLogThreadEventEntry ParseThreadUpdateEntry(DiscordGuild guild, AuditLogAction auditLogAction, + IDictionary threads) + { + DiscordAuditLogThreadEventEntry entry = new() + { + Target = + threads.TryGetValue(auditLogAction.TargetId!.Value, + out DiscordThreadChannel? channel) + ? channel + : new DiscordThreadChannel() { Id = auditLogAction.TargetId.Value, Discord = guild.Discord, GuildId = guild.Id } + }; + + foreach (AuditLogActionChange change in auditLogAction.Changes) + { + switch (change.Key.ToLowerInvariant()) + { + case "name": + entry.Name = PropertyChange.From(change); + break; + + case "type": + entry.Type = PropertyChange.From(change); + break; + + case "archived": + entry.Archived = PropertyChange.From(change); + break; + + case "auto_archive_duration": + entry.AutoArchiveDuration = PropertyChange.From(change); + break; + + case "invitable": + entry.Invitable = PropertyChange.From(change); + break; + + case "locked": + entry.Locked = PropertyChange.From(change); + break; + + case "rate_limit_per_user": + entry.PerUserRateLimit = PropertyChange.From(change); + break; + + case "flags": + entry.Flags = PropertyChange.From(change); + break; + + default: + if (guild.Discord.Configuration.LogUnknownAuditlogs) + { + guild.Discord.Logger.LogWarning(LoggerEvents.AuditLog, + "Unknown key in thread update: {Key} - Please take a look at GitHub issue #1580", + change.Key); + } + + break; + } + } + + return entry; + } + + /// + /// Parses a to a + /// + /// which is the parent of the entry + /// which should be parsed + /// Dictionary of to populate entry with event entities + /// + private static DiscordAuditLogGuildScheduledEventEntry ParseGuildScheduledEventUpdateEntry(DiscordGuild guild, + AuditLogAction auditLogAction, IDictionary events) + { + DiscordAuditLogGuildScheduledEventEntry entry = new() + { + Target = + events.TryGetValue(auditLogAction.TargetId!.Value, out DiscordScheduledGuildEvent? ta) + ? ta + : new DiscordScheduledGuildEvent() { Id = auditLogAction.Id, Discord = guild.Discord }, + }; + + foreach (AuditLogActionChange change in auditLogAction.Changes) + { + switch (change.Key.ToLowerInvariant()) + { + case "name": + entry.Name = PropertyChange.From(change); + break; + case "channel_id": + ulong.TryParse(change.NewValue as string, NumberStyles.Integer, CultureInfo.InvariantCulture, out ulong newChannelId); + entry.Channel = new PropertyChange + { + Before = + guild.GetChannel(newChannelId) ?? new DiscordChannel + { + Id = change.OldValueUlong, + Discord = guild.Discord, + GuildId = guild.Id + }, + After = guild.GetChannel(newChannelId) ?? new DiscordChannel + { + Id = change.NewValueUlong, + Discord = guild.Discord, + GuildId = guild.Id + } + }; + break; + + case "description": + entry.Description = PropertyChange.From(change); + break; + + case "entity_type": + entry.Type = PropertyChange.From(change); + break; + + case "image_hash": + entry.ImageHash = PropertyChange.From(change); + break; + + case "location": + entry.Location = PropertyChange.From(change); + break; + + case "privacy_level": + entry.PrivacyLevel = PropertyChange.From(change); + break; + + case "status": + entry.Status = PropertyChange.From(change); + break; + + default: + if (guild.Discord.Configuration.LogUnknownAuditlogs) + { + guild.Discord.Logger.LogWarning(LoggerEvents.AuditLog, + "Unknown key in scheduled event update: {Key} - Please take a look at GitHub issue #1580", + change.Key); + } + + break; + } + } + + return entry; + } + + /// + /// Parses a to a + /// + /// which is the parent of the entry + /// which should be parsed + /// + internal static async Task ParseGuildUpdateAsync(DiscordGuild guild, + AuditLogAction auditLogAction) + { + DiscordAuditLogGuildEntry entry = new() { Target = guild }; + + ulong before, after; + foreach (AuditLogActionChange? change in auditLogAction.Changes) + { + switch (change.Key.ToLowerInvariant()) + { + case "name": + entry.NameChange = PropertyChange.From(change); + break; + + case "owner_id": + entry.OwnerChange = new PropertyChange + { + Before = guild.members != null && guild.members.TryGetValue( + change.OldValueUlong, + out DiscordMember? oldMember) + ? oldMember + : await guild.GetMemberAsync(change.OldValueUlong), + After = guild.members != null && guild.members.TryGetValue(change.NewValueUlong, + out DiscordMember? newMember) + ? newMember + : await guild.GetMemberAsync(change.NewValueUlong) + }; + break; + + case "icon_hash": + entry.IconChange = new PropertyChange + { + Before = change.OldValueString != null + ? $"https://cdn.discordapp.com/icons/{guild.Id}/{change.OldValueString}.webp" + : null, + After = change.OldValueString != null + ? $"https://cdn.discordapp.com/icons/{guild.Id}/{change.NewValueString}.webp" + : null + }; + break; + + case "verification_level": + entry.VerificationLevelChange = PropertyChange.From(change); + break; + + case "afk_channel_id": + + ulong.TryParse(change.NewValue as string, NumberStyles.Integer, + CultureInfo.InvariantCulture, out before); + ulong.TryParse(change.OldValue as string, NumberStyles.Integer, + CultureInfo.InvariantCulture, out after); + + entry.AfkChannelChange = new PropertyChange + { + Before = guild.GetChannel(before) ?? new DiscordChannel + { + Id = before, + Discord = guild.Discord, + GuildId = guild.Id + }, + After = guild.GetChannel(after) ?? new DiscordChannel + { + Id = before, + Discord = guild.Discord, + GuildId = guild.Id + } + }; + break; + + case "widget_channel_id": + ulong.TryParse(change.NewValue as string, NumberStyles.Integer, + CultureInfo.InvariantCulture, out before); + ulong.TryParse(change.OldValue as string, NumberStyles.Integer, + CultureInfo.InvariantCulture, out after); + + entry.EmbedChannelChange = new PropertyChange + { + Before = guild.GetChannel(before) ?? new DiscordChannel + { + Id = before, + Discord = guild.Discord, + GuildId = guild.Id + }, + After = guild.GetChannel(after) ?? new DiscordChannel + { + Id = before, + Discord = guild.Discord, + GuildId = guild.Id + } + }; + break; + + case "splash_hash": + entry.SplashChange = new PropertyChange + { + Before = change.OldValueString != null + ? $"https://cdn.discordapp.com/splashes/{guild.Id}/{change.OldValueString}.webp?size=2048" + : null, + After = change.NewValueString != null + ? $"https://cdn.discordapp.com/splashes/{guild.Id}/{change.NewValueString}.webp?size=2048" + : null + }; + break; + + case "default_message_notifications": + entry.NotificationSettingsChange = PropertyChange.From(change); + break; + + case "system_channel_id": + ulong.TryParse(change.NewValue as string, NumberStyles.Integer, + CultureInfo.InvariantCulture, out before); + ulong.TryParse(change.OldValue as string, NumberStyles.Integer, + CultureInfo.InvariantCulture, out after); + + entry.SystemChannelChange = new PropertyChange + { + Before = guild.GetChannel(before) ?? new DiscordChannel + { + Id = before, + Discord = guild.Discord, + GuildId = guild.Id + }, + After = guild.GetChannel(after) ?? new DiscordChannel + { + Id = before, + Discord = guild.Discord, + GuildId = guild.Id + } + }; + break; + + case "explicit_content_filter": + entry.ExplicitContentFilterChange = PropertyChange.From(change); + break; + + case "mfa_level": + entry.MfaLevelChange = PropertyChange.From(change); + break; + + case "region": + entry.RegionChange = PropertyChange.From(change); + break; + + default: + if (guild.Discord.Configuration.LogUnknownAuditlogs) + { + guild.Discord.Logger.LogWarning(LoggerEvents.AuditLog, + "Unknown key in guild update: {Key} - Please take a look at GitHub issue #1580", + change.Key); + } + + break; + } + } + + return entry; + } + + /// + /// Parses a to a + /// + /// which is the parent of the entry + /// which should be parsed + /// + internal static DiscordAuditLogChannelEntry ParseChannelEntry(DiscordGuild guild, AuditLogAction auditLogAction) + { + DiscordAuditLogChannelEntry entry = new() + { + Target = guild.GetChannel(auditLogAction.TargetId!.Value) ?? new DiscordChannel + { + Id = auditLogAction.TargetId.Value, + Discord = guild.Discord, + GuildId = guild.Id + } + }; + + foreach (AuditLogActionChange? change in auditLogAction.Changes) + { + switch (change.Key.ToLowerInvariant()) + { + case "name": + entry.NameChange = PropertyChange.From(change); + break; + + case "type": + entry.TypeChange = PropertyChange.From(change); + break; + + case "permission_overwrites": + + IEnumerable? olds = change.OldValues?.OfType()? + .Select(jObject => jObject.ToDiscordObject())? + .Select(overwrite => + { + overwrite.Discord = guild.Discord; + return overwrite; + }); + + IEnumerable? news = change.NewValues?.OfType()? + .Select(jObject => jObject.ToDiscordObject())? + .Select(overwrite => + { + overwrite.Discord = guild.Discord; + return overwrite; + }); + + entry.OverwriteChange = new PropertyChange> + { + Before = olds != null + ? new ReadOnlyCollection(new List(olds)) + : null, + After = news != null + ? new ReadOnlyCollection(new List(news)) + : null + }; + break; + + case "topic": + entry.TopicChange = new PropertyChange + { + Before = change.OldValueString, + After = change.NewValueString + }; + break; + + case "nsfw": + entry.NsfwChange = PropertyChange.From(change); + break; + + case "bitrate": + entry.BitrateChange = PropertyChange.From(change); + break; + + case "rate_limit_per_user": + entry.PerUserRateLimitChange = PropertyChange.From(change); + break; + + case "user_limit": + entry.UserLimit = PropertyChange.From(change); + break; + + case "flags": + entry.Flags = PropertyChange.From(change); + break; + + case "available_tags": + IEnumerable? newTags = change.NewValues?.OfType()? + .Select(jObject => jObject.ToDiscordObject())? + .Select(forumTag => + { + forumTag.Discord = guild.Discord; + return forumTag; + }); + + IEnumerable? oldTags = change.OldValues?.OfType()? + .Select(jObject => jObject.ToDiscordObject())? + .Select(forumTag => + { + forumTag.Discord = guild.Discord; + return forumTag; + }); + + entry.AvailableTags = PropertyChange>.From(oldTags, newTags); + + break; + + default: + if (guild.Discord.Configuration.LogUnknownAuditlogs) + { + guild.Discord.Logger.LogWarning(LoggerEvents.AuditLog, + "Unknown key in channel update: {Key} - Please take a look at GitHub issue #1580", + change.Key); + } + + break; + } + } + + return entry; + } + + /// + /// Parses a to a + /// + /// which is the parent of the entry + /// which should be parsed + /// + internal static DiscordAuditLogOverwriteEntry ParseOverwriteEntry(DiscordGuild guild, + AuditLogAction auditLogAction) + { + DiscordAuditLogOverwriteEntry entry = new() + { + Target = guild + .GetChannel(auditLogAction.TargetId!.Value) + .PermissionOverwrites + .FirstOrDefault(xo => xo.Id == auditLogAction.Options.Id), + Channel = guild.GetChannel(auditLogAction.TargetId.Value) + }; + + foreach (AuditLogActionChange? change in auditLogAction.Changes) + { + switch (change.Key.ToLowerInvariant()) + { + case "deny": + entry.DeniedPermissions = PropertyChange.From(change); + break; + + case "allow": + entry.AllowedPermissions = PropertyChange.From(change); + break; + + case "type": + entry.Type = PropertyChange.From(change); + break; + + case "id": + entry.TargetIdChange = PropertyChange.From(change); + break; + + default: + if (guild.Discord.Configuration.LogUnknownAuditlogs) + { + guild.Discord.Logger.LogWarning(LoggerEvents.AuditLog, + "Unknown key in overwrite update: {Key} - Please take a look at GitHub issue #1580", + change.Key); + } + + break; + } + } + + return entry; + } + + /// + /// Parses a to a + /// + /// which is the parent of the entry + /// which should be parsed + /// + internal static DiscordAuditLogMemberUpdateEntry ParseMemberUpdateEntry(DiscordGuild guild, + AuditLogAction auditLogAction) + { + DiscordAuditLogMemberUpdateEntry entry = new() + { + Target = guild.members.TryGetValue(auditLogAction.TargetId!.Value, out DiscordMember? roleUpdMember) + ? roleUpdMember + : new DiscordMember { Id = auditLogAction.TargetId.Value, Discord = guild.Discord, guild_id = guild.Id } + }; + + foreach (AuditLogActionChange? change in auditLogAction.Changes) + { + switch (change.Key.ToLowerInvariant()) + { + case "nick": + entry.NicknameChange = PropertyChange.From(change); + break; + + case "deaf": + entry.DeafenChange = PropertyChange.From(change); + break; + + case "mute": + entry.MuteChange = PropertyChange.From(change); + break; + + case "communication_disabled_until": + entry.TimeoutChange = PropertyChange.From(change); + + break; + + case "$add": + entry.AddedRoles = + new ReadOnlyCollection(change.NewValues.Select(xo => (ulong)xo["id"]!) + .Select(gx => guild.roles.GetValueOrDefault(gx)!).ToList()); + break; + + case "$remove": + entry.RemovedRoles = + new ReadOnlyCollection(change.NewValues.Select(xo => (ulong)xo["id"]!) + .Select(x => guild.roles.GetValueOrDefault(x)!).ToList()); + break; + + default: + if (guild.Discord.Configuration.LogUnknownAuditlogs) + { + guild.Discord.Logger.LogWarning(LoggerEvents.AuditLog, + "Unknown key in member update: {Key} - Please take a look at GitHub issue #1580", + change.Key); + } + + break; + } + } + + return entry; + } + + /// + /// Parses a to a + /// + /// which is the parent of the entry + /// which should be parsed + /// + internal static DiscordAuditLogRoleUpdateEntry ParseRoleUpdateEntry(DiscordGuild guild, + AuditLogAction auditLogAction) + { + DiscordAuditLogRoleUpdateEntry entry = new() + { + Target = guild.Roles.GetValueOrDefault(auditLogAction.TargetId!.Value) ?? + new DiscordRole { Id = auditLogAction.TargetId.Value, Discord = guild.Discord } + }; + + foreach (AuditLogActionChange? change in auditLogAction.Changes) + { + switch (change.Key.ToLowerInvariant()) + { + case "name": + entry.NameChange = PropertyChange.From(change); + break; + + case "color": + entry.ColorChange = PropertyChange.From(change); + break; + + case "permissions": + entry.PermissionChange = PropertyChange.From(change); + break; + + case "position": + entry.PositionChange = PropertyChange.From(change); + break; + + case "mentionable": + entry.MentionableChange = PropertyChange.From(change); + break; + + case "hoist": + entry.HoistChange = PropertyChange.From(change); + break; + + default: + if (guild.Discord.Configuration.LogUnknownAuditlogs) + { + guild.Discord.Logger.LogWarning(LoggerEvents.AuditLog, + "Unknown key in role update: {Key} - Please take a look at GitHub issue #1580", + change.Key); + } + + break; + } + } + + return entry; + } + + /// + /// Parses a to a + /// + /// which is the parent of the entry + /// which should be parsed + /// + internal static DiscordAuditLogInviteEntry ParseInviteUpdateEntry(DiscordGuild guild, AuditLogAction auditLogAction) + { + DiscordAuditLogInviteEntry entry = new(); + + DiscordInvite invite = new() + { + Discord = guild.Discord, + Guild = new DiscordInviteGuild + { + Discord = guild.Discord, + Id = guild.Id, + Name = guild.Name, + SplashHash = guild.SplashHash + } + }; + + bool boolBefore, boolAfter; + ulong ulongBefore, ulongAfter; + int intBefore, intAfter; + foreach (AuditLogActionChange? change in auditLogAction.Changes) + { + switch (change.Key.ToLowerInvariant()) + { + case "max_age": + entry.MaxAgeChange = PropertyChange.From(change); + break; + + case "code": + invite.Code = change.OldValueString ?? change.NewValueString; + + entry.CodeChange = PropertyChange.From(change); + break; + + case "temporary": + entry.TemporaryChange = new PropertyChange + { + Before = change.OldValue != null ? (bool?)change.OldValue : null, + After = change.NewValue != null ? (bool?)change.NewValue : null + }; + break; + + case "inviter_id": + boolBefore = ulong.TryParse(change.OldValue as string, NumberStyles.Integer, + CultureInfo.InvariantCulture, + out ulongBefore); + boolAfter = ulong.TryParse(change.NewValue as string, NumberStyles.Integer, + CultureInfo.InvariantCulture, + out ulongAfter); + + entry.InviterChange = new PropertyChange + { + Before = guild.members.TryGetValue(ulongBefore, out DiscordMember? propBeforeMember) + ? propBeforeMember + : new DiscordMember { Id = ulongBefore, Discord = guild.Discord, guild_id = guild.Id }, + After = guild.members.TryGetValue(ulongAfter, out DiscordMember? propAfterMember) + ? propAfterMember + : new DiscordMember { Id = ulongBefore, Discord = guild.Discord, guild_id = guild.Id } + }; + break; + + case "channel_id": + boolBefore = ulong.TryParse(change.OldValue as string, NumberStyles.Integer, + CultureInfo.InvariantCulture, + out ulongBefore); + boolAfter = ulong.TryParse(change.NewValue as string, NumberStyles.Integer, + CultureInfo.InvariantCulture, + out ulongAfter); + + entry.ChannelChange = new PropertyChange + { + Before = boolBefore + ? guild.GetChannel(ulongBefore) ?? + new DiscordChannel { Id = ulongBefore, Discord = guild.Discord, GuildId = guild.Id } + : null, + After = boolAfter + ? guild.GetChannel(ulongAfter) ?? + new DiscordChannel { Id = ulongBefore, Discord = guild.Discord, GuildId = guild.Id } + : null + }; + + DiscordChannel? channel = entry.ChannelChange.Before ?? entry.ChannelChange.After; + DiscordChannelType? channelType = channel?.Type; + invite.Channel = new DiscordInviteChannel + { + Discord = guild.Discord, + Id = boolBefore ? ulongBefore : ulongAfter, + Name = channel?.Name ?? "", + Type = channelType != null ? channelType.Value : DiscordChannelType.Unknown + }; + break; + + case "uses": + boolBefore = int.TryParse(change.OldValue as string, NumberStyles.Integer, + CultureInfo.InvariantCulture, + out intBefore); + boolAfter = int.TryParse(change.OldValue as string, NumberStyles.Integer, + CultureInfo.InvariantCulture, + out intAfter); + + entry.UsesChange = new PropertyChange + { + Before = boolBefore ? intBefore : null, + After = boolAfter ? intAfter : null + }; + break; + + case "max_uses": + boolBefore = int.TryParse(change.OldValue as string, NumberStyles.Integer, + CultureInfo.InvariantCulture, + out intBefore); + boolAfter = int.TryParse(change.OldValue as string, NumberStyles.Integer, + CultureInfo.InvariantCulture, + out intAfter); + + entry.MaxUsesChange = new PropertyChange + { + Before = boolBefore ? intBefore : null, + After = boolAfter ? intAfter : null + }; + break; + + default: + if (guild.Discord.Configuration.LogUnknownAuditlogs) + { + guild.Discord.Logger.LogWarning(LoggerEvents.AuditLog, + "Unknown key in invite update: {Key} - Please take a look at GitHub issue #1580", + change.Key); + } + + break; + } + } + + entry.Target = invite; + return entry; + } + + /// + /// Parses a to a + /// + /// which is the parent of the entry + /// which should be parsed + /// Dictionary of to populate entry with webhook entities + /// + internal static DiscordAuditLogWebhookEntry ParseWebhookUpdateEntry + ( + DiscordGuild guild, + AuditLogAction auditLogAction, + IDictionary webhooks + ) + { + DiscordAuditLogWebhookEntry entry = new() + { + Target = webhooks.TryGetValue(auditLogAction.TargetId!.Value, out DiscordWebhook? webhook) + ? webhook + : new DiscordWebhook { Id = auditLogAction.TargetId.Value, Discord = guild.Discord } + }; + + ulong ulongBefore, ulongAfter; + bool boolBefore, boolAfter; + foreach (AuditLogActionChange change in auditLogAction.Changes) + { + switch (change.Key.ToLowerInvariant()) + { + case "name": + entry.NameChange = PropertyChange.From(change); + break; + + case "channel_id": + boolBefore = ulong.TryParse(change.OldValue as string, NumberStyles.Integer, + CultureInfo.InvariantCulture, out ulongBefore); + boolAfter = ulong.TryParse(change.NewValue as string, NumberStyles.Integer, + CultureInfo.InvariantCulture, out ulongAfter); + + entry.ChannelChange = new PropertyChange + { + Before = + boolBefore + ? guild.GetChannel(ulongBefore) ?? new DiscordChannel + { + Id = ulongBefore, + Discord = guild.Discord, + GuildId = guild.Id + } + : null, + After = boolAfter + ? guild.GetChannel(ulongAfter) ?? + new DiscordChannel { Id = ulongBefore, Discord = guild.Discord, GuildId = guild.Id } + : null + }; + break; + + case "type": + entry.TypeChange = PropertyChange.From(change); + break; + + case "avatar_hash": + entry.AvatarHashChange = PropertyChange.From(change); + break; + + case "application_id" + : //Why the fuck does discord send this as a string if it's supposed to be a snowflake + entry.ApplicationIdChange = PropertyChange.From(change); + break; + + default: + if (guild.Discord.Configuration.LogUnknownAuditlogs) + { + guild.Discord.Logger.LogWarning(LoggerEvents.AuditLog, + "Unknown key in webhook update: {Key} - Please take a look at GitHub issue #1580", + change.Key); + } + + break; + } + } + + return entry; + } + + /// + /// Parses a to a + /// + /// which is the parent of the entry + /// which should be parsed + /// + internal static DiscordAuditLogStickerEntry ParseStickerUpdateEntry(DiscordGuild guild, AuditLogAction auditLogAction) + { + DiscordAuditLogStickerEntry entry = new() + { + Target = guild.stickers.TryGetValue(auditLogAction.TargetId!.Value, out DiscordMessageSticker? sticker) + ? sticker + : new DiscordMessageSticker { Id = auditLogAction.TargetId.Value, Discord = guild.Discord } + }; + + foreach (AuditLogActionChange change in auditLogAction.Changes) + { + switch (change.Key.ToLowerInvariant()) + { + case "name": + entry.NameChange = PropertyChange.From(change); + break; + + case "description": + entry.DescriptionChange = PropertyChange.From(change); + break; + + case "tags": + entry.TagsChange = PropertyChange.From(change); + break; + + case "guild_id": + entry.GuildIdChange = PropertyChange.From(change); + break; + + case "available": + entry.AvailabilityChange = PropertyChange.From(change); + break; + + case "asset": + entry.AssetChange = PropertyChange.From(change); + break; + + case "id": + entry.IdChange = PropertyChange.From(change); + break; + + case "type": + entry.TypeChange = PropertyChange.From(change); + break; + + case "format_type": + entry.FormatChange = PropertyChange.From(change); + break; + + default: + if (guild.Discord.Configuration.LogUnknownAuditlogs) + { + guild.Discord.Logger.LogWarning(LoggerEvents.AuditLog, + "Unknown key in sticker update: {Key} - Please take a look at GitHub issue #1580", + change.Key); + } + + break; + } + } + + return entry; + } + + /// + /// Parses a to a + /// + /// which is the parent of the entry + /// which should be parsed + /// + internal static DiscordAuditLogIntegrationEntry ParseIntegrationUpdateEntry(DiscordGuild guild, AuditLogAction auditLogAction) + { + DiscordAuditLogIntegrationEntry entry = new(); + + foreach (AuditLogActionChange change in auditLogAction.Changes) + { + switch (change.Key.ToLowerInvariant()) + { + case "enable_emoticons": + entry.EnableEmoticons = PropertyChange.From(change); + break; + + case "expire_behavior": + entry.ExpireBehavior = PropertyChange.From(change); + break; + + case "expire_grace_period": + entry.ExpireBehavior = PropertyChange.From(change); + break; + + case "name": + entry.Name = PropertyChange.From(change); + break; + + case "type": + entry.Type = PropertyChange.From(change); + break; + + default: + if (guild.Discord.Configuration.LogUnknownAuditlogs) + { + guild.Discord.Logger.LogWarning(LoggerEvents.AuditLog, + "Unknown key in integration update: {Key} - Please take a look at GitHub issue #1580", + change.Key); + } + + break; + } + } + + return entry; + } +} diff --git a/DSharpPlus/Entities/AuditLogs/DiscordAuditLogEntry.cs b/DSharpPlus/Entities/AuditLogs/DiscordAuditLogEntry.cs index f2f927e07c..0545057e4f 100644 --- a/DSharpPlus/Entities/AuditLogs/DiscordAuditLogEntry.cs +++ b/DSharpPlus/Entities/AuditLogs/DiscordAuditLogEntry.cs @@ -1,51 +1,51 @@ -// This file is part of the DSharpPlus project. -// -// Copyright (c) 2015 Mike Santiago -// Copyright (c) 2016-2025 DSharpPlus Contributors -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -namespace DSharpPlus.Entities.AuditLogs; - - -/// -/// Represents an audit log entry. -/// -public abstract class DiscordAuditLogEntry : SnowflakeObject -{ - /// - /// Gets the entry's action type. - /// - public DiscordAuditLogActionType ActionType { get; internal set; } - - /// - /// Gets the user responsible for the action. - /// - public DiscordUser? UserResponsible { get; internal set; } - - /// - /// Gets the reason defined in the action. - /// - public string? Reason { get; internal set; } - - /// - /// Gets the category under which the action falls. - /// - public DiscordAuditLogActionCategory ActionCategory { get; internal set; } -} +// This file is part of the DSharpPlus project. +// +// Copyright (c) 2015 Mike Santiago +// Copyright (c) 2016-2025 DSharpPlus Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +namespace DSharpPlus.Entities.AuditLogs; + + +/// +/// Represents an audit log entry. +/// +public abstract class DiscordAuditLogEntry : SnowflakeObject +{ + /// + /// Gets the entry's action type. + /// + public DiscordAuditLogActionType ActionType { get; internal set; } + + /// + /// Gets the user responsible for the action. + /// + public DiscordUser? UserResponsible { get; internal set; } + + /// + /// Gets the reason defined in the action. + /// + public string? Reason { get; internal set; } + + /// + /// Gets the category under which the action falls. + /// + public DiscordAuditLogActionCategory ActionCategory { get; internal set; } +} diff --git a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogApplicationCommandPermissionEntry.cs b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogApplicationCommandPermissionEntry.cs index 1eedd1abaa..2b1e4377ec 100644 --- a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogApplicationCommandPermissionEntry.cs +++ b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogApplicationCommandPermissionEntry.cs @@ -1,44 +1,44 @@ -// This file is part of the DSharpPlus project. -// -// Copyright (c) 2015 Mike Santiago -// Copyright (c) 2016-2025 DSharpPlus Contributors -// -// 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; - -namespace DSharpPlus.Entities.AuditLogs; - -public sealed class DiscordAuditLogApplicationCommandPermissionEntry : DiscordAuditLogEntry -{ - /// - /// Id of the application command that was changed - /// - public ulong? ApplicationCommandId { get; internal set; } - - /// - /// Id of the application that owns the command - /// - public ulong ApplicationId { get; internal set; } - - /// - /// Permissions changed - /// - public IEnumerable> PermissionChanges { get; internal set; } = default!; -} +// This file is part of the DSharpPlus project. +// +// Copyright (c) 2015 Mike Santiago +// Copyright (c) 2016-2025 DSharpPlus Contributors +// +// 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; + +namespace DSharpPlus.Entities.AuditLogs; + +public sealed class DiscordAuditLogApplicationCommandPermissionEntry : DiscordAuditLogEntry +{ + /// + /// Id of the application command that was changed + /// + public ulong? ApplicationCommandId { get; internal set; } + + /// + /// Id of the application that owns the command + /// + public ulong ApplicationId { get; internal set; } + + /// + /// Permissions changed + /// + public IEnumerable> PermissionChanges { get; internal set; } = default!; +} diff --git a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogAutoModerationExecutedEntry.cs b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogAutoModerationExecutedEntry.cs index d26d243d3b..bbde2485b1 100644 --- a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogAutoModerationExecutedEntry.cs +++ b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogAutoModerationExecutedEntry.cs @@ -1,53 +1,53 @@ -// This file is part of the DSharpPlus project. -// -// Copyright (c) 2015 Mike Santiago -// Copyright (c) 2016-2025 DSharpPlus Contributors -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -namespace DSharpPlus.Entities.AuditLogs; - - -public sealed class DiscordAuditLogAutoModerationExecutedEntry : DiscordAuditLogEntry -{ - /// - /// Name of the rule that was executed - /// - public string ResponsibleRule { get; internal set; } = default!; - - /// - /// User that was affected by the rule - /// - public DiscordUser TargetUser { get; internal set; } = default!; - - /// - /// Type of the trigger that was executed - /// - public DiscordRuleTriggerType RuleTriggerType { get; internal set; } - - /// - /// Channel where the rule was executed - /// - public DiscordChannel? Channel { get; internal set; } - - /// - /// Id of the channel where the rule was executed - /// - public ulong ChannelId { get; internal set; } -} +// This file is part of the DSharpPlus project. +// +// Copyright (c) 2015 Mike Santiago +// Copyright (c) 2016-2025 DSharpPlus Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +namespace DSharpPlus.Entities.AuditLogs; + + +public sealed class DiscordAuditLogAutoModerationExecutedEntry : DiscordAuditLogEntry +{ + /// + /// Name of the rule that was executed + /// + public string ResponsibleRule { get; internal set; } = default!; + + /// + /// User that was affected by the rule + /// + public DiscordUser TargetUser { get; internal set; } = default!; + + /// + /// Type of the trigger that was executed + /// + public DiscordRuleTriggerType RuleTriggerType { get; internal set; } + + /// + /// Channel where the rule was executed + /// + public DiscordChannel? Channel { get; internal set; } + + /// + /// Id of the channel where the rule was executed + /// + public ulong ChannelId { get; internal set; } +} diff --git a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogAutoModerationRuleEntry.cs b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogAutoModerationRuleEntry.cs index f9b880d846..f5cfafe91e 100644 --- a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogAutoModerationRuleEntry.cs +++ b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogAutoModerationRuleEntry.cs @@ -1,115 +1,115 @@ -// This file is part of the DSharpPlus project. -// -// Copyright (c) 2015 Mike Santiago -// Copyright (c) 2016-2025 DSharpPlus Contributors -// -// 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; - -namespace DSharpPlus.Entities.AuditLogs; - -public sealed class DiscordAuditLogAutoModerationRuleEntry : DiscordAuditLogEntry -{ - - /// - /// Id of the rule - /// - public PropertyChange RuleId { get; internal set; } - - /// - /// Id of the guild where the rule was changed - /// - public PropertyChange GuildId { get; internal set; } - - /// - /// Name of the rule - /// - public PropertyChange Name { get; internal set; } - - /// - /// Id of the user that created the rule - /// - public PropertyChange CreatorId { get; internal set; } - - /// - /// Indicates in what event context a rule should be checked. - /// - public PropertyChange EventType { get; internal set; } - - /// - /// Characterizes the type of content which can trigger the rule. - /// - public PropertyChange TriggerType { get; internal set; } - - /// - /// Additional data used to determine whether a rule should be triggered. - /// - public PropertyChange TriggerMetadata { get; internal set; } - - /// - /// Actions which will execute when the rule is triggered. - /// - public PropertyChange?> Actions { get; internal set; } - - /// - /// Whether the rule is enabled or not. - /// - public PropertyChange Enabled { get; internal set; } - - /// - /// Roles that should not be affected by the rule - /// - public PropertyChange?> ExemptRoles { get; internal set; } - - /// - /// Channels that should not be affected by the rule - /// - public PropertyChange?> ExemptChannels { get; internal set; } - - /// - /// List of trigger keywords that were added to the rule - /// - public IEnumerable? AddedKeywords { get; internal set; } - - /// - /// List of trigger keywords that were removed from the rule - /// - public IEnumerable? RemovedKeywords { get; internal set; } - - /// - /// List of trigger regex patterns that were added to the rule - /// - public IEnumerable? AddedRegexPatterns { get; internal set; } - - /// - /// List of trigger regex patterns that were removed from the rule - /// - public IEnumerable? RemovedRegexPatterns { get; internal set; } - - /// - /// List of strings that were added to the allow list - /// - public IEnumerable? AddedAllowList { get; internal set; } - - /// - /// List of strings that were removed from the allow list - /// - public IEnumerable? RemovedAllowList { get; internal set; } -} +// This file is part of the DSharpPlus project. +// +// Copyright (c) 2015 Mike Santiago +// Copyright (c) 2016-2025 DSharpPlus Contributors +// +// 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; + +namespace DSharpPlus.Entities.AuditLogs; + +public sealed class DiscordAuditLogAutoModerationRuleEntry : DiscordAuditLogEntry +{ + + /// + /// Id of the rule + /// + public PropertyChange RuleId { get; internal set; } + + /// + /// Id of the guild where the rule was changed + /// + public PropertyChange GuildId { get; internal set; } + + /// + /// Name of the rule + /// + public PropertyChange Name { get; internal set; } + + /// + /// Id of the user that created the rule + /// + public PropertyChange CreatorId { get; internal set; } + + /// + /// Indicates in what event context a rule should be checked. + /// + public PropertyChange EventType { get; internal set; } + + /// + /// Characterizes the type of content which can trigger the rule. + /// + public PropertyChange TriggerType { get; internal set; } + + /// + /// Additional data used to determine whether a rule should be triggered. + /// + public PropertyChange TriggerMetadata { get; internal set; } + + /// + /// Actions which will execute when the rule is triggered. + /// + public PropertyChange?> Actions { get; internal set; } + + /// + /// Whether the rule is enabled or not. + /// + public PropertyChange Enabled { get; internal set; } + + /// + /// Roles that should not be affected by the rule + /// + public PropertyChange?> ExemptRoles { get; internal set; } + + /// + /// Channels that should not be affected by the rule + /// + public PropertyChange?> ExemptChannels { get; internal set; } + + /// + /// List of trigger keywords that were added to the rule + /// + public IEnumerable? AddedKeywords { get; internal set; } + + /// + /// List of trigger keywords that were removed from the rule + /// + public IEnumerable? RemovedKeywords { get; internal set; } + + /// + /// List of trigger regex patterns that were added to the rule + /// + public IEnumerable? AddedRegexPatterns { get; internal set; } + + /// + /// List of trigger regex patterns that were removed from the rule + /// + public IEnumerable? RemovedRegexPatterns { get; internal set; } + + /// + /// List of strings that were added to the allow list + /// + public IEnumerable? AddedAllowList { get; internal set; } + + /// + /// List of strings that were removed from the allow list + /// + public IEnumerable? RemovedAllowList { get; internal set; } +} diff --git a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogBanEntry.cs b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogBanEntry.cs index 7f04a05e78..6fb97beba2 100644 --- a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogBanEntry.cs +++ b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogBanEntry.cs @@ -1,35 +1,35 @@ -// This file is part of the DSharpPlus project. -// -// Copyright (c) 2015 Mike Santiago -// Copyright (c) 2016-2025 DSharpPlus Contributors -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -namespace DSharpPlus.Entities.AuditLogs; - - -public sealed class DiscordAuditLogBanEntry : DiscordAuditLogEntry -{ - /// - /// Gets the banned member. - /// - public DiscordMember Target { get; internal set; } = default!; - - internal DiscordAuditLogBanEntry() { } -} +// This file is part of the DSharpPlus project. +// +// Copyright (c) 2015 Mike Santiago +// Copyright (c) 2016-2025 DSharpPlus Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +namespace DSharpPlus.Entities.AuditLogs; + + +public sealed class DiscordAuditLogBanEntry : DiscordAuditLogEntry +{ + /// + /// Gets the banned member. + /// + public DiscordMember Target { get; internal set; } = default!; + + internal DiscordAuditLogBanEntry() { } +} diff --git a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogBotAddEntry.cs b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogBotAddEntry.cs index 2c27b8a04e..55d09bdc8b 100644 --- a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogBotAddEntry.cs +++ b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogBotAddEntry.cs @@ -1,33 +1,33 @@ -// This file is part of the DSharpPlus project. -// -// Copyright (c) 2015 Mike Santiago -// Copyright (c) 2016-2025 DSharpPlus Contributors -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -namespace DSharpPlus.Entities.AuditLogs; - - -public sealed class DiscordAuditLogBotAddEntry : DiscordAuditLogEntry -{ - /// - /// Gets the bot that has been added to the guild. - /// - public DiscordUser TargetBot { get; internal set; } = default!; -} +// This file is part of the DSharpPlus project. +// +// Copyright (c) 2015 Mike Santiago +// Copyright (c) 2016-2025 DSharpPlus Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +namespace DSharpPlus.Entities.AuditLogs; + + +public sealed class DiscordAuditLogBotAddEntry : DiscordAuditLogEntry +{ + /// + /// Gets the bot that has been added to the guild. + /// + public DiscordUser TargetBot { get; internal set; } = default!; +} diff --git a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogChannelEntry.cs b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogChannelEntry.cs index a082fabb02..2583ac7072 100644 --- a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogChannelEntry.cs +++ b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogChannelEntry.cs @@ -1,77 +1,77 @@ -// This file is part of the DSharpPlus project. -// -// Copyright (c) 2015 Mike Santiago -// Copyright (c) 2016-2025 DSharpPlus Contributors -// -// 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; - -namespace DSharpPlus.Entities.AuditLogs; - -public sealed class DiscordAuditLogChannelEntry : DiscordAuditLogEntry -{ - /// - /// Gets the affected channel. - /// - public DiscordChannel Target { get; internal set; } = default!; - - /// - /// Gets the description of channel's name change. - /// - public PropertyChange NameChange { get; internal set; } - - /// - /// Gets the description of channel's type change. - /// - public PropertyChange TypeChange { get; internal set; } - - /// - /// Gets the description of channel's nsfw flag change. - /// - public PropertyChange NsfwChange { get; internal set; } - - /// - /// Gets the description of channel's bitrate change. - /// - public PropertyChange BitrateChange { get; internal set; } - - /// - /// Gets the description of channel permission overwrites' change. - /// - public PropertyChange> OverwriteChange { get; internal set; } - - /// - /// Gets the description of channel's topic change. - /// - public PropertyChange TopicChange { get; internal set; } - - /// - /// Gets the description of channel's slow mode timeout change. - /// - public PropertyChange PerUserRateLimitChange { get; internal set; } - - public PropertyChange UserLimit { get; internal set; } - - public PropertyChange Flags { get; internal set; } - - public PropertyChange> AvailableTags { get; internal set; } - - internal DiscordAuditLogChannelEntry() { } -} +// This file is part of the DSharpPlus project. +// +// Copyright (c) 2015 Mike Santiago +// Copyright (c) 2016-2025 DSharpPlus Contributors +// +// 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; + +namespace DSharpPlus.Entities.AuditLogs; + +public sealed class DiscordAuditLogChannelEntry : DiscordAuditLogEntry +{ + /// + /// Gets the affected channel. + /// + public DiscordChannel Target { get; internal set; } = default!; + + /// + /// Gets the description of channel's name change. + /// + public PropertyChange NameChange { get; internal set; } + + /// + /// Gets the description of channel's type change. + /// + public PropertyChange TypeChange { get; internal set; } + + /// + /// Gets the description of channel's nsfw flag change. + /// + public PropertyChange NsfwChange { get; internal set; } + + /// + /// Gets the description of channel's bitrate change. + /// + public PropertyChange BitrateChange { get; internal set; } + + /// + /// Gets the description of channel permission overwrites' change. + /// + public PropertyChange> OverwriteChange { get; internal set; } + + /// + /// Gets the description of channel's topic change. + /// + public PropertyChange TopicChange { get; internal set; } + + /// + /// Gets the description of channel's slow mode timeout change. + /// + public PropertyChange PerUserRateLimitChange { get; internal set; } + + public PropertyChange UserLimit { get; internal set; } + + public PropertyChange Flags { get; internal set; } + + public PropertyChange> AvailableTags { get; internal set; } + + internal DiscordAuditLogChannelEntry() { } +} diff --git a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogEmojiEntry.cs b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogEmojiEntry.cs index ed9db37361..c7eac29772 100644 --- a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogEmojiEntry.cs +++ b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogEmojiEntry.cs @@ -1,40 +1,40 @@ -// This file is part of the DSharpPlus project. -// -// Copyright (c) 2015 Mike Santiago -// Copyright (c) 2016-2025 DSharpPlus Contributors -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -namespace DSharpPlus.Entities.AuditLogs; - - -public sealed class DiscordAuditLogEmojiEntry : DiscordAuditLogEntry -{ - /// - /// Gets the affected emoji. - /// - public DiscordEmoji Target { get; internal set; } = default!; - - /// - /// Gets the description of emoji's name change. - /// - public PropertyChange NameChange { get; internal set; } - - internal DiscordAuditLogEmojiEntry() { } -} +// This file is part of the DSharpPlus project. +// +// Copyright (c) 2015 Mike Santiago +// Copyright (c) 2016-2025 DSharpPlus Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +namespace DSharpPlus.Entities.AuditLogs; + + +public sealed class DiscordAuditLogEmojiEntry : DiscordAuditLogEntry +{ + /// + /// Gets the affected emoji. + /// + public DiscordEmoji Target { get; internal set; } = default!; + + /// + /// Gets the description of emoji's name change. + /// + public PropertyChange NameChange { get; internal set; } + + internal DiscordAuditLogEmojiEntry() { } +} diff --git a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogGuildEntry.cs b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogGuildEntry.cs index 71ba021acf..4eedfec1a9 100644 --- a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogGuildEntry.cs +++ b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogGuildEntry.cs @@ -1,95 +1,95 @@ -// This file is part of the DSharpPlus project. -// -// Copyright (c) 2015 Mike Santiago -// Copyright (c) 2016-2025 DSharpPlus Contributors -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -namespace DSharpPlus.Entities.AuditLogs; - - -public sealed class DiscordAuditLogGuildEntry : DiscordAuditLogEntry -{ - /// - /// Gets the affected guild. - /// - public DiscordGuild Target { get; internal set; } = default!; - - /// - /// Gets the description of guild name's change. - /// - public PropertyChange NameChange { get; internal set; } - - /// - /// Gets the description of owner's change. - /// - public PropertyChange OwnerChange { get; internal set; } - - /// - /// Gets the description of icon's change. - /// - public PropertyChange IconChange { get; internal set; } - - /// - /// Gets the description of verification level's change. - /// - public PropertyChange VerificationLevelChange { get; internal set; } - - /// - /// Gets the description of afk channel's change. - /// - public PropertyChange AfkChannelChange { get; internal set; } - - /// - /// Gets the description of widget channel's change. - /// - public PropertyChange EmbedChannelChange { get; internal set; } - - /// - /// Gets the description of notification settings' change. - /// - public PropertyChange NotificationSettingsChange { get; internal set; } - - /// - /// Gets the description of system message channel's change. - /// - public PropertyChange SystemChannelChange { get; internal set; } - - /// - /// Gets the description of explicit content filter settings' change. - /// - public PropertyChange ExplicitContentFilterChange { get; internal set; } - - /// - /// Gets the description of guild's mfa level change. - /// - public PropertyChange MfaLevelChange { get; internal set; } - - /// - /// Gets the description of invite splash's change. - /// - public PropertyChange SplashChange { get; internal set; } - - /// - /// Gets the description of the guild's region change. - /// - public PropertyChange RegionChange { get; internal set; } - - internal DiscordAuditLogGuildEntry() { } -} +// This file is part of the DSharpPlus project. +// +// Copyright (c) 2015 Mike Santiago +// Copyright (c) 2016-2025 DSharpPlus Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +namespace DSharpPlus.Entities.AuditLogs; + + +public sealed class DiscordAuditLogGuildEntry : DiscordAuditLogEntry +{ + /// + /// Gets the affected guild. + /// + public DiscordGuild Target { get; internal set; } = default!; + + /// + /// Gets the description of guild name's change. + /// + public PropertyChange NameChange { get; internal set; } + + /// + /// Gets the description of owner's change. + /// + public PropertyChange OwnerChange { get; internal set; } + + /// + /// Gets the description of icon's change. + /// + public PropertyChange IconChange { get; internal set; } + + /// + /// Gets the description of verification level's change. + /// + public PropertyChange VerificationLevelChange { get; internal set; } + + /// + /// Gets the description of afk channel's change. + /// + public PropertyChange AfkChannelChange { get; internal set; } + + /// + /// Gets the description of widget channel's change. + /// + public PropertyChange EmbedChannelChange { get; internal set; } + + /// + /// Gets the description of notification settings' change. + /// + public PropertyChange NotificationSettingsChange { get; internal set; } + + /// + /// Gets the description of system message channel's change. + /// + public PropertyChange SystemChannelChange { get; internal set; } + + /// + /// Gets the description of explicit content filter settings' change. + /// + public PropertyChange ExplicitContentFilterChange { get; internal set; } + + /// + /// Gets the description of guild's mfa level change. + /// + public PropertyChange MfaLevelChange { get; internal set; } + + /// + /// Gets the description of invite splash's change. + /// + public PropertyChange SplashChange { get; internal set; } + + /// + /// Gets the description of the guild's region change. + /// + public PropertyChange RegionChange { get; internal set; } + + internal DiscordAuditLogGuildEntry() { } +} diff --git a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogGuildScheduledEventEntry.cs b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogGuildScheduledEventEntry.cs index e4d770c225..e32c0fe7c1 100644 --- a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogGuildScheduledEventEntry.cs +++ b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogGuildScheduledEventEntry.cs @@ -1,75 +1,75 @@ -// This file is part of the DSharpPlus project. -// -// Copyright (c) 2015 Mike Santiago -// Copyright (c) 2016-2025 DSharpPlus Contributors -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -namespace DSharpPlus.Entities.AuditLogs; - - -public sealed class DiscordAuditLogGuildScheduledEventEntry : DiscordAuditLogEntry -{ - /// - /// Gets a change in the event's name - /// - public PropertyChange Name { get; internal set; } - - /// - /// Gets the target event. Note that this will only have the ID specified if it is not cached. - /// - public DiscordScheduledGuildEvent Target { get; internal set; } = default!; - - /// - /// Gets the channel the event was changed to. - /// - public PropertyChange Channel { get; internal set; } - - /// - /// Gets the description change of the event. - /// - public PropertyChange Description { get; internal set; } - - /// - /// Gets the change of type for the event. - /// - public PropertyChange Type { get; internal set; } - - /// - /// Gets the change in image hash. - /// - public PropertyChange ImageHash { get; internal set; } - - /// - /// Gets the change in event location, if it's an external event. - /// - public PropertyChange Location { get; internal set; } - - /// - /// Gets change in privacy level. - /// - public PropertyChange PrivacyLevel { get; internal set; } - - /// - /// Gets the change in status. - /// - public PropertyChange Status { get; internal set; } - - public DiscordAuditLogGuildScheduledEventEntry() { } -} +// This file is part of the DSharpPlus project. +// +// Copyright (c) 2015 Mike Santiago +// Copyright (c) 2016-2025 DSharpPlus Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +namespace DSharpPlus.Entities.AuditLogs; + + +public sealed class DiscordAuditLogGuildScheduledEventEntry : DiscordAuditLogEntry +{ + /// + /// Gets a change in the event's name + /// + public PropertyChange Name { get; internal set; } + + /// + /// Gets the target event. Note that this will only have the ID specified if it is not cached. + /// + public DiscordScheduledGuildEvent Target { get; internal set; } = default!; + + /// + /// Gets the channel the event was changed to. + /// + public PropertyChange Channel { get; internal set; } + + /// + /// Gets the description change of the event. + /// + public PropertyChange Description { get; internal set; } + + /// + /// Gets the change of type for the event. + /// + public PropertyChange Type { get; internal set; } + + /// + /// Gets the change in image hash. + /// + public PropertyChange ImageHash { get; internal set; } + + /// + /// Gets the change in event location, if it's an external event. + /// + public PropertyChange Location { get; internal set; } + + /// + /// Gets change in privacy level. + /// + public PropertyChange PrivacyLevel { get; internal set; } + + /// + /// Gets the change in status. + /// + public PropertyChange Status { get; internal set; } + + public DiscordAuditLogGuildScheduledEventEntry() { } +} diff --git a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogIntegrationEntry.cs b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogIntegrationEntry.cs index 8e6f899eca..75e5b94451 100644 --- a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogIntegrationEntry.cs +++ b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogIntegrationEntry.cs @@ -1,53 +1,53 @@ -// This file is part of the DSharpPlus project. -// -// Copyright (c) 2015 Mike Santiago -// Copyright (c) 2016-2025 DSharpPlus Contributors -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -namespace DSharpPlus.Entities.AuditLogs; - - -public sealed class DiscordAuditLogIntegrationEntry : DiscordAuditLogEntry -{ - /// - /// Gets the description of emoticons' change. - /// - public PropertyChange EnableEmoticons { get; internal set; } - - /// - /// Gets the description of expire grace period's change. - /// - public PropertyChange ExpireGracePeriod { get; internal set; } - - /// - /// Gets the description of expire behavior change. - /// - public PropertyChange ExpireBehavior { get; internal set; } - - /// - /// Gets the type of the integration. - /// - public PropertyChange Type { get; internal set; } - - /// - /// Gets the name of the integration. - /// - public PropertyChange Name { get; internal set; } -} +// This file is part of the DSharpPlus project. +// +// Copyright (c) 2015 Mike Santiago +// Copyright (c) 2016-2025 DSharpPlus Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +namespace DSharpPlus.Entities.AuditLogs; + + +public sealed class DiscordAuditLogIntegrationEntry : DiscordAuditLogEntry +{ + /// + /// Gets the description of emoticons' change. + /// + public PropertyChange EnableEmoticons { get; internal set; } + + /// + /// Gets the description of expire grace period's change. + /// + public PropertyChange ExpireGracePeriod { get; internal set; } + + /// + /// Gets the description of expire behavior change. + /// + public PropertyChange ExpireBehavior { get; internal set; } + + /// + /// Gets the type of the integration. + /// + public PropertyChange Type { get; internal set; } + + /// + /// Gets the name of the integration. + /// + public PropertyChange Name { get; internal set; } +} diff --git a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogInviteEntry.cs b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogInviteEntry.cs index 18dd7caac9..39d8173591 100644 --- a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogInviteEntry.cs +++ b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogInviteEntry.cs @@ -1,70 +1,70 @@ -// This file is part of the DSharpPlus project. -// -// Copyright (c) 2015 Mike Santiago -// Copyright (c) 2016-2025 DSharpPlus Contributors -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -namespace DSharpPlus.Entities.AuditLogs; - - -public sealed class DiscordAuditLogInviteEntry : DiscordAuditLogEntry -{ - /// - /// Gets the affected invite. - /// - public DiscordInvite Target { get; internal set; } = default!; - - /// - /// Gets the description of invite's max age change. - /// - public PropertyChange MaxAgeChange { get; internal set; } - - /// - /// Gets the description of invite's code change. - /// - public PropertyChange CodeChange { get; internal set; } - - /// - /// Gets the description of invite's temporariness change. - /// - public PropertyChange TemporaryChange { get; internal set; } - - /// - /// Gets the description of invite's inviting member change. - /// - public PropertyChange InviterChange { get; internal set; } - - /// - /// Gets the description of invite's target channel change. - /// - public PropertyChange ChannelChange { get; internal set; } - - /// - /// Gets the description of invite's use count change. - /// - public PropertyChange UsesChange { get; internal set; } - - /// - /// Gets the description of invite's max use count change. - /// - public PropertyChange MaxUsesChange { get; internal set; } - - internal DiscordAuditLogInviteEntry() { } -} +// This file is part of the DSharpPlus project. +// +// Copyright (c) 2015 Mike Santiago +// Copyright (c) 2016-2025 DSharpPlus Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +namespace DSharpPlus.Entities.AuditLogs; + + +public sealed class DiscordAuditLogInviteEntry : DiscordAuditLogEntry +{ + /// + /// Gets the affected invite. + /// + public DiscordInvite Target { get; internal set; } = default!; + + /// + /// Gets the description of invite's max age change. + /// + public PropertyChange MaxAgeChange { get; internal set; } + + /// + /// Gets the description of invite's code change. + /// + public PropertyChange CodeChange { get; internal set; } + + /// + /// Gets the description of invite's temporariness change. + /// + public PropertyChange TemporaryChange { get; internal set; } + + /// + /// Gets the description of invite's inviting member change. + /// + public PropertyChange InviterChange { get; internal set; } + + /// + /// Gets the description of invite's target channel change. + /// + public PropertyChange ChannelChange { get; internal set; } + + /// + /// Gets the description of invite's use count change. + /// + public PropertyChange UsesChange { get; internal set; } + + /// + /// Gets the description of invite's max use count change. + /// + public PropertyChange MaxUsesChange { get; internal set; } + + internal DiscordAuditLogInviteEntry() { } +} diff --git a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogKickEntry.cs b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogKickEntry.cs index 49b527f416..94d5f37d88 100644 --- a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogKickEntry.cs +++ b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogKickEntry.cs @@ -1,35 +1,35 @@ -// This file is part of the DSharpPlus project. -// -// Copyright (c) 2015 Mike Santiago -// Copyright (c) 2016-2025 DSharpPlus Contributors -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -namespace DSharpPlus.Entities.AuditLogs; - - -public sealed class DiscordAuditLogKickEntry : DiscordAuditLogEntry -{ - /// - /// Gets the kicked member. - /// - public DiscordMember Target { get; internal set; } = default!; - - internal DiscordAuditLogKickEntry() { } -} +// This file is part of the DSharpPlus project. +// +// Copyright (c) 2015 Mike Santiago +// Copyright (c) 2016-2025 DSharpPlus Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +namespace DSharpPlus.Entities.AuditLogs; + + +public sealed class DiscordAuditLogKickEntry : DiscordAuditLogEntry +{ + /// + /// Gets the kicked member. + /// + public DiscordMember Target { get; internal set; } = default!; + + internal DiscordAuditLogKickEntry() { } +} diff --git a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogMemberDisconnectEntry.cs b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogMemberDisconnectEntry.cs index 52c101f984..51b4810818 100644 --- a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogMemberDisconnectEntry.cs +++ b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogMemberDisconnectEntry.cs @@ -1,33 +1,33 @@ -// This file is part of the DSharpPlus project. -// -// Copyright (c) 2015 Mike Santiago -// Copyright (c) 2016-2025 DSharpPlus Contributors -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -namespace DSharpPlus.Entities.AuditLogs; - - -public sealed class DiscordAuditLogMemberDisconnectEntry : DiscordAuditLogEntry -{ - /// - /// Gets the amount of users that were disconnected from the voice channel. - /// - public int UserCount { get; internal set; } -} +// This file is part of the DSharpPlus project. +// +// Copyright (c) 2015 Mike Santiago +// Copyright (c) 2016-2025 DSharpPlus Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +namespace DSharpPlus.Entities.AuditLogs; + + +public sealed class DiscordAuditLogMemberDisconnectEntry : DiscordAuditLogEntry +{ + /// + /// Gets the amount of users that were disconnected from the voice channel. + /// + public int UserCount { get; internal set; } +} diff --git a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogMemberMoveEntry.cs b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogMemberMoveEntry.cs index d38ad774a1..4b924f4eaf 100644 --- a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogMemberMoveEntry.cs +++ b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogMemberMoveEntry.cs @@ -1,38 +1,38 @@ -// This file is part of the DSharpPlus project. -// -// Copyright (c) 2015 Mike Santiago -// Copyright (c) 2016-2025 DSharpPlus Contributors -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -namespace DSharpPlus.Entities.AuditLogs; - - -public sealed class DiscordAuditLogMemberMoveEntry : DiscordAuditLogEntry -{ - /// - /// Gets the channel the members were moved in. - /// - public DiscordChannel Channel { get; internal set; } = default!; - - /// - /// Gets the amount of users that were moved out from the voice channel. - /// - public int UserCount { get; internal set; } -} +// This file is part of the DSharpPlus project. +// +// Copyright (c) 2015 Mike Santiago +// Copyright (c) 2016-2025 DSharpPlus Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +namespace DSharpPlus.Entities.AuditLogs; + + +public sealed class DiscordAuditLogMemberMoveEntry : DiscordAuditLogEntry +{ + /// + /// Gets the channel the members were moved in. + /// + public DiscordChannel Channel { get; internal set; } = default!; + + /// + /// Gets the amount of users that were moved out from the voice channel. + /// + public int UserCount { get; internal set; } +} diff --git a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogMemberUpdateEntry.cs b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogMemberUpdateEntry.cs index 14a3237155..ab591fc79d 100644 --- a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogMemberUpdateEntry.cs +++ b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogMemberUpdateEntry.cs @@ -1,67 +1,67 @@ -// This file is part of the DSharpPlus project. -// -// Copyright (c) 2015 Mike Santiago -// Copyright (c) 2016-2025 DSharpPlus Contributors -// -// 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 DSharpPlus.Entities.AuditLogs; - -public sealed class DiscordAuditLogMemberUpdateEntry : DiscordAuditLogEntry -{ - /// - /// Gets the affected member. - /// - public DiscordMember Target { get; internal set; } = default!; - - /// - /// Gets the description of member's nickname change. - /// - public PropertyChange NicknameChange { get; internal set; } - - /// - /// Gets the roles that were removed from the member. - /// - public IReadOnlyList? RemovedRoles { get; internal set; } - - /// - /// Gets the roles that were added to the member. - /// - public IReadOnlyList? AddedRoles { get; internal set; } - - /// - /// Gets the description of member's mute status change. - /// - public PropertyChange MuteChange { get; internal set; } - - /// - /// Gets the description of member's deaf status change. - /// - public PropertyChange DeafenChange { get; internal set; } - - /// - /// Gets the change in a user's timeout status - /// - public PropertyChange TimeoutChange { get; internal set; } - - internal DiscordAuditLogMemberUpdateEntry() { } -} +// This file is part of the DSharpPlus project. +// +// Copyright (c) 2015 Mike Santiago +// Copyright (c) 2016-2025 DSharpPlus Contributors +// +// 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 DSharpPlus.Entities.AuditLogs; + +public sealed class DiscordAuditLogMemberUpdateEntry : DiscordAuditLogEntry +{ + /// + /// Gets the affected member. + /// + public DiscordMember Target { get; internal set; } = default!; + + /// + /// Gets the description of member's nickname change. + /// + public PropertyChange NicknameChange { get; internal set; } + + /// + /// Gets the roles that were removed from the member. + /// + public IReadOnlyList? RemovedRoles { get; internal set; } + + /// + /// Gets the roles that were added to the member. + /// + public IReadOnlyList? AddedRoles { get; internal set; } + + /// + /// Gets the description of member's mute status change. + /// + public PropertyChange MuteChange { get; internal set; } + + /// + /// Gets the description of member's deaf status change. + /// + public PropertyChange DeafenChange { get; internal set; } + + /// + /// Gets the change in a user's timeout status + /// + public PropertyChange TimeoutChange { get; internal set; } + + internal DiscordAuditLogMemberUpdateEntry() { } +} diff --git a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogMessageEntry.cs b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogMessageEntry.cs index 996afb14d6..09976c470c 100644 --- a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogMessageEntry.cs +++ b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogMessageEntry.cs @@ -1,50 +1,50 @@ -// This file is part of the DSharpPlus project. -// -// Copyright (c) 2015 Mike Santiago -// Copyright (c) 2016-2025 DSharpPlus Contributors -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -namespace DSharpPlus.Entities.AuditLogs; - - -public sealed class DiscordAuditLogMessageEntry : DiscordAuditLogEntry -{ - /// - /// Gets the affected User. - /// - public DiscordUser Target { get; internal set; } = default!; - - /// - /// Gets the affected Member. This is null if the action was performed on a user that is not in the member cache. - /// - public DiscordMember? Member => this.Channel.Guild.Members.TryGetValue(this.Target.Id, out DiscordMember? member) ? member : null; - - /// - /// Gets the channel in which the action occurred. - /// - public DiscordChannel Channel { get; internal set; } = default!; - - /// - /// Gets the number of messages that were affected. - /// - public int? MessageCount { get; internal set; } - - internal DiscordAuditLogMessageEntry() { } -} +// This file is part of the DSharpPlus project. +// +// Copyright (c) 2015 Mike Santiago +// Copyright (c) 2016-2025 DSharpPlus Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +namespace DSharpPlus.Entities.AuditLogs; + + +public sealed class DiscordAuditLogMessageEntry : DiscordAuditLogEntry +{ + /// + /// Gets the affected User. + /// + public DiscordUser Target { get; internal set; } = default!; + + /// + /// Gets the affected Member. This is null if the action was performed on a user that is not in the member cache. + /// + public DiscordMember? Member => this.Channel.Guild.Members.TryGetValue(this.Target.Id, out DiscordMember? member) ? member : null; + + /// + /// Gets the channel in which the action occurred. + /// + public DiscordChannel Channel { get; internal set; } = default!; + + /// + /// Gets the number of messages that were affected. + /// + public int? MessageCount { get; internal set; } + + internal DiscordAuditLogMessageEntry() { } +} diff --git a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogMessagePinEntry.cs b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogMessagePinEntry.cs index a38bf7cc5f..d873fabbb0 100644 --- a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogMessagePinEntry.cs +++ b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogMessagePinEntry.cs @@ -1,45 +1,45 @@ -// This file is part of the DSharpPlus project. -// -// Copyright (c) 2015 Mike Santiago -// Copyright (c) 2016-2025 DSharpPlus Contributors -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -namespace DSharpPlus.Entities.AuditLogs; - - -public sealed class DiscordAuditLogMessagePinEntry : DiscordAuditLogEntry -{ - /// - /// Gets the affected message's user. - /// - public DiscordUser Target { get; internal set; } = default!; - - /// - /// Gets the channel the message is in. - /// - public DiscordChannel Channel { get; internal set; } = default!; - - /// - /// Gets the message the pin action was for. - /// - public DiscordMessage Message { get; internal set; } = default!; - - internal DiscordAuditLogMessagePinEntry() { } -} +// This file is part of the DSharpPlus project. +// +// Copyright (c) 2015 Mike Santiago +// Copyright (c) 2016-2025 DSharpPlus Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +namespace DSharpPlus.Entities.AuditLogs; + + +public sealed class DiscordAuditLogMessagePinEntry : DiscordAuditLogEntry +{ + /// + /// Gets the affected message's user. + /// + public DiscordUser Target { get; internal set; } = default!; + + /// + /// Gets the channel the message is in. + /// + public DiscordChannel Channel { get; internal set; } = default!; + + /// + /// Gets the message the pin action was for. + /// + public DiscordMessage Message { get; internal set; } = default!; + + internal DiscordAuditLogMessagePinEntry() { } +} diff --git a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogOverwriteEntry.cs b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogOverwriteEntry.cs index 077cc1e863..01457e54c0 100644 --- a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogOverwriteEntry.cs +++ b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogOverwriteEntry.cs @@ -1,60 +1,60 @@ -// This file is part of the DSharpPlus project. -// -// Copyright (c) 2015 Mike Santiago -// Copyright (c) 2016-2025 DSharpPlus Contributors -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -namespace DSharpPlus.Entities.AuditLogs; - - -public sealed class DiscordAuditLogOverwriteEntry : DiscordAuditLogEntry -{ - /// - /// Gets the affected overwrite. Null if the overwrite was deleted or not in cache. - /// - public DiscordOverwrite? Target { get; internal set; } = default!; - - /// - /// Gets the channel for which the overwrite was changed. - /// - public DiscordChannel Channel { get; internal set; } = default!; - - /// - /// Gets the description of overwrite's allow value change. - /// - public PropertyChange AllowedPermissions { get; internal set; } - - /// - /// Gets the description of overwrite's deny value change. - /// - public PropertyChange DeniedPermissions { get; internal set; } - - /// - /// Gets the description of overwrite's type change. - /// - public PropertyChange Type { get; internal set; } - - /// - /// Gets the description of overwrite's target id change. - /// - public PropertyChange TargetIdChange { get; internal set; } - - internal DiscordAuditLogOverwriteEntry() { } -} +// This file is part of the DSharpPlus project. +// +// Copyright (c) 2015 Mike Santiago +// Copyright (c) 2016-2025 DSharpPlus Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +namespace DSharpPlus.Entities.AuditLogs; + + +public sealed class DiscordAuditLogOverwriteEntry : DiscordAuditLogEntry +{ + /// + /// Gets the affected overwrite. Null if the overwrite was deleted or not in cache. + /// + public DiscordOverwrite? Target { get; internal set; } = default!; + + /// + /// Gets the channel for which the overwrite was changed. + /// + public DiscordChannel Channel { get; internal set; } = default!; + + /// + /// Gets the description of overwrite's allow value change. + /// + public PropertyChange AllowedPermissions { get; internal set; } + + /// + /// Gets the description of overwrite's deny value change. + /// + public PropertyChange DeniedPermissions { get; internal set; } + + /// + /// Gets the description of overwrite's type change. + /// + public PropertyChange Type { get; internal set; } + + /// + /// Gets the description of overwrite's target id change. + /// + public PropertyChange TargetIdChange { get; internal set; } + + internal DiscordAuditLogOverwriteEntry() { } +} diff --git a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogPruneEntry.cs b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogPruneEntry.cs index 144549991f..8e465f7a3b 100644 --- a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogPruneEntry.cs +++ b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogPruneEntry.cs @@ -1,41 +1,41 @@ -// This file is part of the DSharpPlus project. -// -// Copyright (c) 2015 Mike Santiago -// Copyright (c) 2016-2025 DSharpPlus Contributors -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -namespace DSharpPlus.Entities.AuditLogs; - - -public sealed class DiscordAuditLogPruneEntry : DiscordAuditLogEntry -{ - /// - /// Gets the number inactivity days after which members were pruned. - /// - public int Days { get; internal set; } - - /// - /// Gets the number of members pruned. - /// - public int Toll { get; internal set; } - - internal DiscordAuditLogPruneEntry() { } -} - +// This file is part of the DSharpPlus project. +// +// Copyright (c) 2015 Mike Santiago +// Copyright (c) 2016-2025 DSharpPlus Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +namespace DSharpPlus.Entities.AuditLogs; + + +public sealed class DiscordAuditLogPruneEntry : DiscordAuditLogEntry +{ + /// + /// Gets the number inactivity days after which members were pruned. + /// + public int Days { get; internal set; } + + /// + /// Gets the number of members pruned. + /// + public int Toll { get; internal set; } + + internal DiscordAuditLogPruneEntry() { } +} + diff --git a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogRoleUpdateEntry.cs b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogRoleUpdateEntry.cs index ba17f94eae..456ea44c95 100644 --- a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogRoleUpdateEntry.cs +++ b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogRoleUpdateEntry.cs @@ -1,42 +1,42 @@ -namespace DSharpPlus.Entities.AuditLogs; - - -public sealed class DiscordAuditLogRoleUpdateEntry : DiscordAuditLogEntry -{ - /// - /// Gets the affected role. - /// - public DiscordRole Target { get; internal set; } = default!; - - /// - /// Gets the description of role's name change. - /// - public PropertyChange NameChange { get; internal set; } - - /// - /// Gets the description of role's color change. - /// - public PropertyChange ColorChange { get; internal set; } - - /// - /// Gets the description of role's permission set change. - /// - public PropertyChange PermissionChange { get; internal set; } - - /// - /// Gets the description of the role's position change. - /// - public PropertyChange PositionChange { get; internal set; } - - /// - /// Gets the description of the role's mentionability change. - /// - public PropertyChange MentionableChange { get; internal set; } - - /// - /// Gets the description of the role's hoist status change. - /// - public PropertyChange HoistChange { get; internal set; } - - internal DiscordAuditLogRoleUpdateEntry() { } -} +namespace DSharpPlus.Entities.AuditLogs; + + +public sealed class DiscordAuditLogRoleUpdateEntry : DiscordAuditLogEntry +{ + /// + /// Gets the affected role. + /// + public DiscordRole Target { get; internal set; } = default!; + + /// + /// Gets the description of role's name change. + /// + public PropertyChange NameChange { get; internal set; } + + /// + /// Gets the description of role's color change. + /// + public PropertyChange ColorChange { get; internal set; } + + /// + /// Gets the description of role's permission set change. + /// + public PropertyChange PermissionChange { get; internal set; } + + /// + /// Gets the description of the role's position change. + /// + public PropertyChange PositionChange { get; internal set; } + + /// + /// Gets the description of the role's mentionability change. + /// + public PropertyChange MentionableChange { get; internal set; } + + /// + /// Gets the description of the role's hoist status change. + /// + public PropertyChange HoistChange { get; internal set; } + + internal DiscordAuditLogRoleUpdateEntry() { } +} diff --git a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogStickerEntry.cs b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogStickerEntry.cs index 49fb13bdd9..740430a444 100644 --- a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogStickerEntry.cs +++ b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogStickerEntry.cs @@ -1,80 +1,80 @@ -// This file is part of the DSharpPlus project. -// -// Copyright (c) 2015 Mike Santiago -// Copyright (c) 2016-2025 DSharpPlus Contributors -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -namespace DSharpPlus.Entities.AuditLogs; - - -public sealed class DiscordAuditLogStickerEntry : DiscordAuditLogEntry -{ - /// - /// Gets the affected sticker. - /// - public DiscordMessageSticker Target { get; internal set; } = default!; - - /// - /// Gets the description of sticker's name change. - /// - public PropertyChange NameChange { get; internal set; } - - /// - /// Gets the description of sticker's description change. - /// - public PropertyChange DescriptionChange { get; internal set; } - - /// - /// Gets the description of sticker's tags change. - /// - public PropertyChange TagsChange { get; internal set; } - - /// - /// Gets the description of sticker's tags change. - /// - public PropertyChange AssetChange { get; internal set; } - - /// - /// Gets the description of sticker's guild id change. - /// - public PropertyChange GuildIdChange { get; internal set; } - - /// - /// Gets the description of sticker's availability change. - /// - public PropertyChange AvailabilityChange { get; internal set; } - - /// - /// Gets the description of sticker's id change. - /// - public PropertyChange IdChange { get; internal set; } - - /// - /// Gets the description of sticker's type change. - /// - public PropertyChange TypeChange { get; internal set; } - - /// - /// Gets the description of sticker's format change. - /// - public PropertyChange FormatChange { get; internal set; } - - internal DiscordAuditLogStickerEntry() { } -} +// This file is part of the DSharpPlus project. +// +// Copyright (c) 2015 Mike Santiago +// Copyright (c) 2016-2025 DSharpPlus Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +namespace DSharpPlus.Entities.AuditLogs; + + +public sealed class DiscordAuditLogStickerEntry : DiscordAuditLogEntry +{ + /// + /// Gets the affected sticker. + /// + public DiscordMessageSticker Target { get; internal set; } = default!; + + /// + /// Gets the description of sticker's name change. + /// + public PropertyChange NameChange { get; internal set; } + + /// + /// Gets the description of sticker's description change. + /// + public PropertyChange DescriptionChange { get; internal set; } + + /// + /// Gets the description of sticker's tags change. + /// + public PropertyChange TagsChange { get; internal set; } + + /// + /// Gets the description of sticker's tags change. + /// + public PropertyChange AssetChange { get; internal set; } + + /// + /// Gets the description of sticker's guild id change. + /// + public PropertyChange GuildIdChange { get; internal set; } + + /// + /// Gets the description of sticker's availability change. + /// + public PropertyChange AvailabilityChange { get; internal set; } + + /// + /// Gets the description of sticker's id change. + /// + public PropertyChange IdChange { get; internal set; } + + /// + /// Gets the description of sticker's type change. + /// + public PropertyChange TypeChange { get; internal set; } + + /// + /// Gets the description of sticker's format change. + /// + public PropertyChange FormatChange { get; internal set; } + + internal DiscordAuditLogStickerEntry() { } +} diff --git a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogThreadEventEntry.cs b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogThreadEventEntry.cs index ebbd7c4d37..147cd806c7 100644 --- a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogThreadEventEntry.cs +++ b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogThreadEventEntry.cs @@ -1,75 +1,75 @@ -// This file is part of the DSharpPlus project. -// -// Copyright (c) 2015 Mike Santiago -// Copyright (c) 2016-2025 DSharpPlus Contributors -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -namespace DSharpPlus.Entities.AuditLogs; - - -public sealed class DiscordAuditLogThreadEventEntry : DiscordAuditLogEntry -{ - /// - /// Gets the target thread. - /// - public DiscordThreadChannel Target { get; internal set; } = default!; - - /// - /// Gets a change in the thread's name. - /// - public PropertyChange Name { get; internal set; } - - /// - /// Gets a change in channel type. - /// - public PropertyChange Type { get; internal set; } - - /// - /// Gets a change in the thread's archived status. - /// - public PropertyChange Archived { get; internal set; } - - /// - /// Gets a change in the thread's auto archive duration. - /// - public PropertyChange AutoArchiveDuration { get; internal set; } - - /// - /// Gets a change in the threads invitibility - /// - public PropertyChange Invitable { get; internal set; } - - /// - /// Gets a change in the thread's locked status - /// - public PropertyChange Locked { get; internal set; } - - /// - /// Gets a change in the thread's slowmode setting - /// - public PropertyChange PerUserRateLimit { get; internal set; } - - /// - /// Gets a change in channel flags - /// - public PropertyChange Flags { get; internal set; } - - internal DiscordAuditLogThreadEventEntry() { } -} +// This file is part of the DSharpPlus project. +// +// Copyright (c) 2015 Mike Santiago +// Copyright (c) 2016-2025 DSharpPlus Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +namespace DSharpPlus.Entities.AuditLogs; + + +public sealed class DiscordAuditLogThreadEventEntry : DiscordAuditLogEntry +{ + /// + /// Gets the target thread. + /// + public DiscordThreadChannel Target { get; internal set; } = default!; + + /// + /// Gets a change in the thread's name. + /// + public PropertyChange Name { get; internal set; } + + /// + /// Gets a change in channel type. + /// + public PropertyChange Type { get; internal set; } + + /// + /// Gets a change in the thread's archived status. + /// + public PropertyChange Archived { get; internal set; } + + /// + /// Gets a change in the thread's auto archive duration. + /// + public PropertyChange AutoArchiveDuration { get; internal set; } + + /// + /// Gets a change in the threads invitibility + /// + public PropertyChange Invitable { get; internal set; } + + /// + /// Gets a change in the thread's locked status + /// + public PropertyChange Locked { get; internal set; } + + /// + /// Gets a change in the thread's slowmode setting + /// + public PropertyChange PerUserRateLimit { get; internal set; } + + /// + /// Gets a change in channel flags + /// + public PropertyChange Flags { get; internal set; } + + internal DiscordAuditLogThreadEventEntry() { } +} diff --git a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogWebhookEntry.cs b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogWebhookEntry.cs index fa3e3228c4..86124b6fde 100644 --- a/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogWebhookEntry.cs +++ b/DSharpPlus/Entities/AuditLogs/Entries/DiscordAuditLogWebhookEntry.cs @@ -1,60 +1,60 @@ -// This file is part of the DSharpPlus project. -// -// Copyright (c) 2015 Mike Santiago -// Copyright (c) 2016-2025 DSharpPlus Contributors -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -namespace DSharpPlus.Entities.AuditLogs; - - -public sealed class DiscordAuditLogWebhookEntry : DiscordAuditLogEntry -{ - /// - /// Gets the affected webhook. - /// - public DiscordWebhook Target { get; internal set; } = default!; - - /// - /// Gets the description of webhook's name change. - /// - public PropertyChange NameChange { get; internal set; } - - /// - /// Gets the description of webhook's target channel change. - /// - public PropertyChange ChannelChange { get; internal set; } - - /// - /// Gets the description of webhook's type change. - /// - public PropertyChange TypeChange { get; internal set; } - - /// - /// Gets the description of webhook's avatar change. - /// - public PropertyChange AvatarHashChange { get; internal set; } - - /// - /// Gets the change in application ID. - /// - public PropertyChange ApplicationIdChange { get; internal set; } - - internal DiscordAuditLogWebhookEntry() { } -} +// This file is part of the DSharpPlus project. +// +// Copyright (c) 2015 Mike Santiago +// Copyright (c) 2016-2025 DSharpPlus Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +namespace DSharpPlus.Entities.AuditLogs; + + +public sealed class DiscordAuditLogWebhookEntry : DiscordAuditLogEntry +{ + /// + /// Gets the affected webhook. + /// + public DiscordWebhook Target { get; internal set; } = default!; + + /// + /// Gets the description of webhook's name change. + /// + public PropertyChange NameChange { get; internal set; } + + /// + /// Gets the description of webhook's target channel change. + /// + public PropertyChange ChannelChange { get; internal set; } + + /// + /// Gets the description of webhook's type change. + /// + public PropertyChange TypeChange { get; internal set; } + + /// + /// Gets the description of webhook's avatar change. + /// + public PropertyChange AvatarHashChange { get; internal set; } + + /// + /// Gets the change in application ID. + /// + public PropertyChange ApplicationIdChange { get; internal set; } + + internal DiscordAuditLogWebhookEntry() { } +} diff --git a/DSharpPlus/Entities/AuditLogs/PropertyChange.cs b/DSharpPlus/Entities/AuditLogs/PropertyChange.cs index b67ec5fdca..6c07cb5952 100644 --- a/DSharpPlus/Entities/AuditLogs/PropertyChange.cs +++ b/DSharpPlus/Entities/AuditLogs/PropertyChange.cs @@ -1,59 +1,59 @@ -// This file is part of the DSharpPlus project. -// -// Copyright (c) 2015 Mike Santiago -// Copyright (c) 2016-2025 DSharpPlus Contributors -// -// 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 DSharpPlus.Net.Abstractions; - -namespace DSharpPlus.Entities.AuditLogs; - -/// -/// Represents a description of how a property changed. -/// -/// Type of the changed property. -public readonly record struct PropertyChange -{ - /// - /// The property's value before it was changed. - /// - public T? Before { get; internal init; } - - /// - /// The property's value after it was changed. - /// - public T? After { get; internal init; } - - /// - /// Create a new from the given before and after values. - /// - public static PropertyChange From(object? before, object? after) => - new() - { - Before = before is not null and T ? (T)before : default, - After = after is not null and T ? (T)after : default - }; - - /// - /// Create a new from the given change using before and after values. - /// - internal static PropertyChange From(AuditLogActionChange change) => - From(change.OldValue, change.NewValue); -} +// This file is part of the DSharpPlus project. +// +// Copyright (c) 2015 Mike Santiago +// Copyright (c) 2016-2025 DSharpPlus Contributors +// +// 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 DSharpPlus.Net.Abstractions; + +namespace DSharpPlus.Entities.AuditLogs; + +/// +/// Represents a description of how a property changed. +/// +/// Type of the changed property. +public readonly record struct PropertyChange +{ + /// + /// The property's value before it was changed. + /// + public T? Before { get; internal init; } + + /// + /// The property's value after it was changed. + /// + public T? After { get; internal init; } + + /// + /// Create a new from the given before and after values. + /// + public static PropertyChange From(object? before, object? after) => + new() + { + Before = before is not null and T ? (T)before : default, + After = after is not null and T ? (T)after : default + }; + + /// + /// Create a new from the given change using before and after values. + /// + internal static PropertyChange From(AuditLogActionChange change) => + From(change.OldValue, change.NewValue); +} diff --git a/DSharpPlus/Entities/AutoModeration/Action/DiscordAutoModerationAction.cs b/DSharpPlus/Entities/AutoModeration/Action/DiscordAutoModerationAction.cs index 36fe6cca4c..f4108fb691 100644 --- a/DSharpPlus/Entities/AutoModeration/Action/DiscordAutoModerationAction.cs +++ b/DSharpPlus/Entities/AutoModeration/Action/DiscordAutoModerationAction.cs @@ -1,21 +1,21 @@ -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents a Discord auto moderation action. -/// -public class DiscordAutoModerationAction -{ - /// - /// Gets the rule action type. - /// - [JsonProperty("type")] - public DiscordRuleActionType Type { get; internal set; } - - /// - /// Gets additional metadata needed during execution for this specific action type. - /// - [JsonProperty("metadata", NullValueHandling = NullValueHandling.Ignore)] - public DiscordRuleActionMetadata? Metadata { get; internal set; } -} +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Represents a Discord auto moderation action. +/// +public class DiscordAutoModerationAction +{ + /// + /// Gets the rule action type. + /// + [JsonProperty("type")] + public DiscordRuleActionType Type { get; internal set; } + + /// + /// Gets additional metadata needed during execution for this specific action type. + /// + [JsonProperty("metadata", NullValueHandling = NullValueHandling.Ignore)] + public DiscordRuleActionMetadata? Metadata { get; internal set; } +} diff --git a/DSharpPlus/Entities/AutoModeration/Action/DiscordAutoModerationActionBuilder.cs b/DSharpPlus/Entities/AutoModeration/Action/DiscordAutoModerationActionBuilder.cs index 8c64c2a55e..fad01a96b1 100644 --- a/DSharpPlus/Entities/AutoModeration/Action/DiscordAutoModerationActionBuilder.cs +++ b/DSharpPlus/Entities/AutoModeration/Action/DiscordAutoModerationActionBuilder.cs @@ -1,55 +1,55 @@ -namespace DSharpPlus.Entities; - - -/// -/// Constructs auto-moderation actions. -/// -public class DiscordAutoModerationActionBuilder -{ - /// - /// Sets the rule action type. - /// - public DiscordRuleActionType Type { get; internal set; } - - /// - /// Sets additional metadata needed during execution for this specific action type. - /// - public DiscordRuleActionMetadata? Metadata { get; internal set; } - - /// - /// Sets the rule action type. - /// - /// The rule action type. - /// This builder. - public DiscordAutoModerationActionBuilder WithRuleActionType(DiscordRuleActionType type) - { - this.Type = type; - - return this; - } - - /// - /// Sets the action metadata. - /// - /// The action metadata. - /// This builder. - public DiscordAutoModerationActionBuilder WithActionMetadata(DiscordRuleActionMetadata metadata) - { - this.Metadata = metadata; - - return this; - } - - /// - /// Constructs a new trigger rule action. - /// - /// The built rule. - public DiscordAutoModerationAction Build() - { - return new() - { - Type = this.Type, - Metadata = this.Metadata - }; - } -} +namespace DSharpPlus.Entities; + + +/// +/// Constructs auto-moderation actions. +/// +public class DiscordAutoModerationActionBuilder +{ + /// + /// Sets the rule action type. + /// + public DiscordRuleActionType Type { get; internal set; } + + /// + /// Sets additional metadata needed during execution for this specific action type. + /// + public DiscordRuleActionMetadata? Metadata { get; internal set; } + + /// + /// Sets the rule action type. + /// + /// The rule action type. + /// This builder. + public DiscordAutoModerationActionBuilder WithRuleActionType(DiscordRuleActionType type) + { + this.Type = type; + + return this; + } + + /// + /// Sets the action metadata. + /// + /// The action metadata. + /// This builder. + public DiscordAutoModerationActionBuilder WithActionMetadata(DiscordRuleActionMetadata metadata) + { + this.Metadata = metadata; + + return this; + } + + /// + /// Constructs a new trigger rule action. + /// + /// The built rule. + public DiscordAutoModerationAction Build() + { + return new() + { + Type = this.Type, + Metadata = this.Metadata + }; + } +} diff --git a/DSharpPlus/Entities/AutoModeration/Action/DiscordAutoModerationActionExecution.cs b/DSharpPlus/Entities/AutoModeration/Action/DiscordAutoModerationActionExecution.cs index a7a08acfd4..b76ba1cd1b 100644 --- a/DSharpPlus/Entities/AutoModeration/Action/DiscordAutoModerationActionExecution.cs +++ b/DSharpPlus/Entities/AutoModeration/Action/DiscordAutoModerationActionExecution.cs @@ -1,77 +1,77 @@ -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents a Discord rule executed action. -/// -public class DiscordAutoModerationActionExecution -{ - /// - /// Gets the id of the guild in which action was executed. - /// - [JsonProperty("guild_id")] - public ulong GuildId { get; internal set; } - - /// - /// Gets the action which was executed. - /// - [JsonProperty("action")] - public DiscordAutoModerationAction? Action { get; internal set; } - - /// - /// Gets the id of the rule which was triggered. - /// - [JsonProperty("rule_id")] - public ulong RuleId { get; internal set; } - - /// - /// Gets the rule trigger type. - /// - [JsonProperty("rule_trigger_type")] - public DiscordRuleTriggerType TriggerType { get; internal set; } - - /// - /// Gets the id of the user which triggered the rule. - /// - [JsonProperty("user_id")] - public ulong UserId { get; internal set; } - - /// - /// Gets the id of the channel in which user triggered the rule. - /// - [JsonProperty("channel_id", NullValueHandling = NullValueHandling.Ignore)] - public ulong? ChannelId { get; internal set; } - - /// - /// Gets the id of any user message which content belongs to. - /// - [JsonProperty("message_id", NullValueHandling = NullValueHandling.Ignore)] - public ulong? MessageId { get; internal set; } - - /// - /// Gets the id of the message sent by the alert system. - /// - [JsonProperty("alert_system_message_id", NullValueHandling = NullValueHandling.Ignore)] - public ulong? AlertSystemMessageId { get; internal set; } - - /// - /// Gets the content of the message. - /// - /// is required to not get an empty value. - [JsonProperty("content", NullValueHandling = NullValueHandling.Ignore)] - public string? Content { get; internal set; } - - /// - /// Gets the keywords (word or phrase) configured in the rule that triggered it. - /// - [JsonProperty("matched_keyword")] - public string? MatchedKeyword { get; internal set; } - - /// - /// Gets the substring in content that triggered the rule. - /// - /// is required to not get an empty value. - [JsonProperty("matched_content", NullValueHandling = NullValueHandling.Ignore)] - public string? MatchedContent { get; internal set; } -} +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Represents a Discord rule executed action. +/// +public class DiscordAutoModerationActionExecution +{ + /// + /// Gets the id of the guild in which action was executed. + /// + [JsonProperty("guild_id")] + public ulong GuildId { get; internal set; } + + /// + /// Gets the action which was executed. + /// + [JsonProperty("action")] + public DiscordAutoModerationAction? Action { get; internal set; } + + /// + /// Gets the id of the rule which was triggered. + /// + [JsonProperty("rule_id")] + public ulong RuleId { get; internal set; } + + /// + /// Gets the rule trigger type. + /// + [JsonProperty("rule_trigger_type")] + public DiscordRuleTriggerType TriggerType { get; internal set; } + + /// + /// Gets the id of the user which triggered the rule. + /// + [JsonProperty("user_id")] + public ulong UserId { get; internal set; } + + /// + /// Gets the id of the channel in which user triggered the rule. + /// + [JsonProperty("channel_id", NullValueHandling = NullValueHandling.Ignore)] + public ulong? ChannelId { get; internal set; } + + /// + /// Gets the id of any user message which content belongs to. + /// + [JsonProperty("message_id", NullValueHandling = NullValueHandling.Ignore)] + public ulong? MessageId { get; internal set; } + + /// + /// Gets the id of the message sent by the alert system. + /// + [JsonProperty("alert_system_message_id", NullValueHandling = NullValueHandling.Ignore)] + public ulong? AlertSystemMessageId { get; internal set; } + + /// + /// Gets the content of the message. + /// + /// is required to not get an empty value. + [JsonProperty("content", NullValueHandling = NullValueHandling.Ignore)] + public string? Content { get; internal set; } + + /// + /// Gets the keywords (word or phrase) configured in the rule that triggered it. + /// + [JsonProperty("matched_keyword")] + public string? MatchedKeyword { get; internal set; } + + /// + /// Gets the substring in content that triggered the rule. + /// + /// is required to not get an empty value. + [JsonProperty("matched_content", NullValueHandling = NullValueHandling.Ignore)] + public string? MatchedContent { get; internal set; } +} diff --git a/DSharpPlus/Entities/AutoModeration/Action/DiscordRuleActionMetadata.cs b/DSharpPlus/Entities/AutoModeration/Action/DiscordRuleActionMetadata.cs index 8896a2db6f..968bc1fde0 100644 --- a/DSharpPlus/Entities/AutoModeration/Action/DiscordRuleActionMetadata.cs +++ b/DSharpPlus/Entities/AutoModeration/Action/DiscordRuleActionMetadata.cs @@ -1,32 +1,32 @@ -using System; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents a Discord rule action metadata. -/// -public class DiscordRuleActionMetadata -{ - /// - /// Gets the channel which the blocked content should be logged. - /// - [JsonProperty("channel_id")] - public ulong ChannelId { get; internal set; } - - /// - /// Gets the timeout duration in seconds. - /// - [JsonIgnore] - public TimeSpan TimeoutSeconds => TimeSpan.FromSeconds(this.DurationSeconds); - - /// Gets the timeout duration in seconds. - /// - /// Gets the message that will be shown on the user screen whenever their message is blocked. - /// - [JsonProperty("custom_message", NullValueHandling = NullValueHandling.Ignore)] - public string? CustomMessage { get; internal set; } - - [JsonProperty("duration_seconds")] - internal uint DurationSeconds { get; set; } -} +using System; +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Represents a Discord rule action metadata. +/// +public class DiscordRuleActionMetadata +{ + /// + /// Gets the channel which the blocked content should be logged. + /// + [JsonProperty("channel_id")] + public ulong ChannelId { get; internal set; } + + /// + /// Gets the timeout duration in seconds. + /// + [JsonIgnore] + public TimeSpan TimeoutSeconds => TimeSpan.FromSeconds(this.DurationSeconds); + + /// Gets the timeout duration in seconds. + /// + /// Gets the message that will be shown on the user screen whenever their message is blocked. + /// + [JsonProperty("custom_message", NullValueHandling = NullValueHandling.Ignore)] + public string? CustomMessage { get; internal set; } + + [JsonProperty("duration_seconds")] + internal uint DurationSeconds { get; set; } +} diff --git a/DSharpPlus/Entities/AutoModeration/Action/DiscordRuleActionMetadataBuilder.cs b/DSharpPlus/Entities/AutoModeration/Action/DiscordRuleActionMetadataBuilder.cs index f161626488..290f852691 100644 --- a/DSharpPlus/Entities/AutoModeration/Action/DiscordRuleActionMetadataBuilder.cs +++ b/DSharpPlus/Entities/AutoModeration/Action/DiscordRuleActionMetadataBuilder.cs @@ -1,80 +1,80 @@ -using System; - -namespace DSharpPlus.Entities.AutoModeration.Action; - -/// -/// Constructs auto-moderation rule action metadata. -/// -public class DiscordRuleActionMetadataBuilder -{ - /// - /// Sets the channel which the blocked content should be logged. - /// - public ulong ChannelId { get; internal set; } - - /// - /// Sets the timeout duration in seconds. - /// - public uint DurationSeconds { get; internal set; } - - /// - /// Gets the message that will be shown on the user screen whenever the message is blocked. - /// - public string? CustomMessage { get; internal set; } - - /// - /// Add the channel id in which the blocked content will be logged. - /// - /// The channel id. - /// - public DiscordRuleActionMetadataBuilder WithLogChannelId(ulong channelId) - { - this.ChannelId = channelId; - - return this; - } - - /// - /// Add the timeout duration in seconds that will be applied on the member which triggered the rule. - /// - /// Timeout duration. - /// This builder. - public DiscordRuleActionMetadataBuilder WithTimeoutDuration(uint timeoutDurationInSeconds) - { - this.DurationSeconds = timeoutDurationInSeconds; - - return this; - } - - /// - /// Add the custom message which will be shown when the rule will be triggered. - /// - /// Message to show. - /// This builder. - /// - public DiscordRuleActionMetadataBuilder WithCustomMessage(string message) - { - if (string.IsNullOrEmpty(message)) - { - throw new ArgumentException("Message can't be null or empty."); - } - - this.CustomMessage = message; - - return this; - } - - /// - /// Build the rule action. - /// - /// The built rule action. - public DiscordRuleActionMetadata Build() - { - return new() - { - ChannelId = this.ChannelId, - DurationSeconds = this.DurationSeconds, - CustomMessage = this.CustomMessage, - }; - } -} +using System; + +namespace DSharpPlus.Entities.AutoModeration.Action; + +/// +/// Constructs auto-moderation rule action metadata. +/// +public class DiscordRuleActionMetadataBuilder +{ + /// + /// Sets the channel which the blocked content should be logged. + /// + public ulong ChannelId { get; internal set; } + + /// + /// Sets the timeout duration in seconds. + /// + public uint DurationSeconds { get; internal set; } + + /// + /// Gets the message that will be shown on the user screen whenever the message is blocked. + /// + public string? CustomMessage { get; internal set; } + + /// + /// Add the channel id in which the blocked content will be logged. + /// + /// The channel id. + /// + public DiscordRuleActionMetadataBuilder WithLogChannelId(ulong channelId) + { + this.ChannelId = channelId; + + return this; + } + + /// + /// Add the timeout duration in seconds that will be applied on the member which triggered the rule. + /// + /// Timeout duration. + /// This builder. + public DiscordRuleActionMetadataBuilder WithTimeoutDuration(uint timeoutDurationInSeconds) + { + this.DurationSeconds = timeoutDurationInSeconds; + + return this; + } + + /// + /// Add the custom message which will be shown when the rule will be triggered. + /// + /// Message to show. + /// This builder. + /// + public DiscordRuleActionMetadataBuilder WithCustomMessage(string message) + { + if (string.IsNullOrEmpty(message)) + { + throw new ArgumentException("Message can't be null or empty."); + } + + this.CustomMessage = message; + + return this; + } + + /// + /// Build the rule action. + /// + /// The built rule action. + public DiscordRuleActionMetadata Build() + { + return new() + { + ChannelId = this.ChannelId, + DurationSeconds = this.DurationSeconds, + CustomMessage = this.CustomMessage, + }; + } +} diff --git a/DSharpPlus/Entities/AutoModeration/Action/DiscordRuleActionType.cs b/DSharpPlus/Entities/AutoModeration/Action/DiscordRuleActionType.cs index 7a0a87ef36..c2e1ffcb86 100644 --- a/DSharpPlus/Entities/AutoModeration/Action/DiscordRuleActionType.cs +++ b/DSharpPlus/Entities/AutoModeration/Action/DiscordRuleActionType.cs @@ -1,24 +1,24 @@ -namespace DSharpPlus.Entities; - - -/// -/// Contains all actions that can be taken after a rule activation. -/// -public enum DiscordRuleActionType -{ - /// - /// Blocks a member's message and prevents it from being posted. - /// A custom message can be specified and shown to members whenever their message is blocked. - /// - BlockMessage = 1, - - /// - /// Logs the user content to a specified channel. - /// - SendAlertMessage = 2, - - /// - /// Timeout user for a specified duration. - /// - Timeout = 3 -} +namespace DSharpPlus.Entities; + + +/// +/// Contains all actions that can be taken after a rule activation. +/// +public enum DiscordRuleActionType +{ + /// + /// Blocks a member's message and prevents it from being posted. + /// A custom message can be specified and shown to members whenever their message is blocked. + /// + BlockMessage = 1, + + /// + /// Logs the user content to a specified channel. + /// + SendAlertMessage = 2, + + /// + /// Timeout user for a specified duration. + /// + Timeout = 3 +} diff --git a/DSharpPlus/Entities/AutoModeration/DiscordAutoModerationRule.cs b/DSharpPlus/Entities/AutoModeration/DiscordAutoModerationRule.cs index 46b408fcc0..04641818cd 100644 --- a/DSharpPlus/Entities/AutoModeration/DiscordAutoModerationRule.cs +++ b/DSharpPlus/Entities/AutoModeration/DiscordAutoModerationRule.cs @@ -1,118 +1,118 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using DSharpPlus.Net.Models; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents a Discord auto-moderation rule. -/// -public class DiscordAutoModerationRule : SnowflakeObject -{ - [JsonProperty("guild_id")] - internal ulong GuildId { get; set; } - - /// - /// Gets the guild which the rule is in. - /// - [JsonIgnore] - public DiscordGuild? Guild => this.Discord.Guilds.TryGetValue(this.GuildId, out DiscordGuild? guild) ? guild : null; - - /// - /// Gets the rule name. - /// - [JsonProperty("name")] - public string? Name { get; internal set; } - - [JsonProperty("creator_id")] - internal ulong CreatorId { get; set; } - - /// - /// Gets the user that created the rule. - /// - [JsonIgnore] - public DiscordUser? Creator => this.Discord.TryGetCachedUserInternal(this.CreatorId, out DiscordUser creator) ? creator : null; - - /// - /// Gets the rule event type. - /// - [JsonProperty("event_type")] - public DiscordRuleEventType EventType { get; internal set; } - - /// - /// Gets the rule trigger type. - /// - [JsonProperty("trigger_type")] - public DiscordRuleTriggerType TriggerType { get; internal set; } - - /// - /// Gets the additional data to determine whether a rule should be triggered. - /// - [JsonProperty("trigger_metadata")] - public DiscordRuleTriggerMetadata? Metadata { get; internal set; } - - /// - /// Gets actions which will execute when the rule is triggered. - /// - [JsonProperty("actions")] - public IReadOnlyList? Actions { get; internal set; } - - /// - /// Gets whether the rule is enabled. - /// - [JsonProperty("enabled", NullValueHandling = NullValueHandling.Ignore)] - public bool IsEnabled { get; internal set; } - - /// - /// Gets ids of roles that will not trigger the rule. - /// - /// - /// Maximum of 20. - /// - [JsonProperty("exempt_roles", NullValueHandling = NullValueHandling.Ignore)] - public IReadOnlyList? ExemptRoles { get; internal set; } - - /// - /// Gets ids of channels in which rule will be not triggered. - /// - /// - /// Maximum of 50. - /// - [JsonProperty("exempt_channels", NullValueHandling = NullValueHandling.Ignore)] - public IReadOnlyList? ExemptChannels { get; internal set; } - - /// - /// Deletes the rule in the guild. - /// - /// Reason for audits logs. - public async Task DeleteAsync(string? reason = null) - => await this.Discord.ApiClient.DeleteGuildAutoModerationRuleAsync(this.GuildId, this.Id, reason); - - /// - /// Modify the rule in the guild. - /// - /// Action the perform on this rule. - /// The modified rule. - public async Task ModifyAsync(Action action) - { - AutoModerationRuleEditModel model = new(); - - action(model); - - return await this.Discord.ApiClient.ModifyGuildAutoModerationRuleAsync - ( - this.GuildId, - this.Id, - model.Name, - model.EventType, - model.TriggerMetadata, - model.Actions, - model.Enable, - model.ExemptRoles, - model.ExemptChannels, - model.AuditLogReason - ); - } -} +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using DSharpPlus.Net.Models; +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Represents a Discord auto-moderation rule. +/// +public class DiscordAutoModerationRule : SnowflakeObject +{ + [JsonProperty("guild_id")] + internal ulong GuildId { get; set; } + + /// + /// Gets the guild which the rule is in. + /// + [JsonIgnore] + public DiscordGuild? Guild => this.Discord.Guilds.TryGetValue(this.GuildId, out DiscordGuild? guild) ? guild : null; + + /// + /// Gets the rule name. + /// + [JsonProperty("name")] + public string? Name { get; internal set; } + + [JsonProperty("creator_id")] + internal ulong CreatorId { get; set; } + + /// + /// Gets the user that created the rule. + /// + [JsonIgnore] + public DiscordUser? Creator => this.Discord.TryGetCachedUserInternal(this.CreatorId, out DiscordUser creator) ? creator : null; + + /// + /// Gets the rule event type. + /// + [JsonProperty("event_type")] + public DiscordRuleEventType EventType { get; internal set; } + + /// + /// Gets the rule trigger type. + /// + [JsonProperty("trigger_type")] + public DiscordRuleTriggerType TriggerType { get; internal set; } + + /// + /// Gets the additional data to determine whether a rule should be triggered. + /// + [JsonProperty("trigger_metadata")] + public DiscordRuleTriggerMetadata? Metadata { get; internal set; } + + /// + /// Gets actions which will execute when the rule is triggered. + /// + [JsonProperty("actions")] + public IReadOnlyList? Actions { get; internal set; } + + /// + /// Gets whether the rule is enabled. + /// + [JsonProperty("enabled", NullValueHandling = NullValueHandling.Ignore)] + public bool IsEnabled { get; internal set; } + + /// + /// Gets ids of roles that will not trigger the rule. + /// + /// + /// Maximum of 20. + /// + [JsonProperty("exempt_roles", NullValueHandling = NullValueHandling.Ignore)] + public IReadOnlyList? ExemptRoles { get; internal set; } + + /// + /// Gets ids of channels in which rule will be not triggered. + /// + /// + /// Maximum of 50. + /// + [JsonProperty("exempt_channels", NullValueHandling = NullValueHandling.Ignore)] + public IReadOnlyList? ExemptChannels { get; internal set; } + + /// + /// Deletes the rule in the guild. + /// + /// Reason for audits logs. + public async Task DeleteAsync(string? reason = null) + => await this.Discord.ApiClient.DeleteGuildAutoModerationRuleAsync(this.GuildId, this.Id, reason); + + /// + /// Modify the rule in the guild. + /// + /// Action the perform on this rule. + /// The modified rule. + public async Task ModifyAsync(Action action) + { + AutoModerationRuleEditModel model = new(); + + action(model); + + return await this.Discord.ApiClient.ModifyGuildAutoModerationRuleAsync + ( + this.GuildId, + this.Id, + model.Name, + model.EventType, + model.TriggerMetadata, + model.Actions, + model.Enable, + model.ExemptRoles, + model.ExemptChannels, + model.AuditLogReason + ); + } +} diff --git a/DSharpPlus/Entities/AutoModeration/DiscordRuleEventType.cs b/DSharpPlus/Entities/AutoModeration/DiscordRuleEventType.cs index 35dc364b2e..2137a44f19 100644 --- a/DSharpPlus/Entities/AutoModeration/DiscordRuleEventType.cs +++ b/DSharpPlus/Entities/AutoModeration/DiscordRuleEventType.cs @@ -1,13 +1,13 @@ -namespace DSharpPlus.Entities; - - -/// -/// Indicates in what event context a rule should be checked. -/// -public enum DiscordRuleEventType -{ - /// - /// The rule will trigger when a member send or modify a message. - /// - MessageSend = 1 -} +namespace DSharpPlus.Entities; + + +/// +/// Indicates in what event context a rule should be checked. +/// +public enum DiscordRuleEventType +{ + /// + /// The rule will trigger when a member send or modify a message. + /// + MessageSend = 1 +} diff --git a/DSharpPlus/Entities/AutoModeration/DiscordRuleKeywordPresetType.cs b/DSharpPlus/Entities/AutoModeration/DiscordRuleKeywordPresetType.cs index 13b9120e49..92d438b9db 100644 --- a/DSharpPlus/Entities/AutoModeration/DiscordRuleKeywordPresetType.cs +++ b/DSharpPlus/Entities/AutoModeration/DiscordRuleKeywordPresetType.cs @@ -1,23 +1,23 @@ -namespace DSharpPlus.Entities; - - -/// -/// Characterizes which type of category a blocked word belongs to. -/// -public enum DiscordRuleKeywordPresetType -{ - /// - /// Words that may be considered forms of swearing or cursing. - /// - Profanity = 1, - - /// - /// Words that refer to sexually explicit behavior or activity. - /// - SexualContent = 2, - - /// - /// Personal insults or words that may be considered hate speech. - /// - Slurs = 3 -} +namespace DSharpPlus.Entities; + + +/// +/// Characterizes which type of category a blocked word belongs to. +/// +public enum DiscordRuleKeywordPresetType +{ + /// + /// Words that may be considered forms of swearing or cursing. + /// + Profanity = 1, + + /// + /// Words that refer to sexually explicit behavior or activity. + /// + SexualContent = 2, + + /// + /// Personal insults or words that may be considered hate speech. + /// + Slurs = 3 +} diff --git a/DSharpPlus/Entities/AutoModeration/DiscordRuleTriggerMetadata.cs b/DSharpPlus/Entities/AutoModeration/DiscordRuleTriggerMetadata.cs index 6120b5a40f..6d6ee7a2e5 100644 --- a/DSharpPlus/Entities/AutoModeration/DiscordRuleTriggerMetadata.cs +++ b/DSharpPlus/Entities/AutoModeration/DiscordRuleTriggerMetadata.cs @@ -1,43 +1,43 @@ -using System.Collections.Generic; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents metadata about the triggering of a Discord rule. -/// -public sealed class DiscordRuleTriggerMetadata -{ - /// - /// Gets substrings which will be searched in the content. - /// - [JsonProperty("keyword_filter", NullValueHandling = NullValueHandling.Ignore)] - public IReadOnlyList? KeywordFilter { get; internal set; } - - /// - /// Gets regex patterns which will be matched against the content. - /// - [JsonProperty("regex_patterns")] - public IReadOnlyList? RegexPatterns { get; internal set; } - - /// - /// Gets the internally pre-defined wordsets which will be searched in the content. - /// - [JsonProperty("presets", NullValueHandling = NullValueHandling.Ignore)] - public IReadOnlyList? KeywordPresetTypes { get; internal set; } - - /// - /// Gets the substrings which should not trigger the rule. - /// - [JsonProperty("allow_list")] - public IReadOnlyList? AllowedKeywords { get; internal set; } - - /// - /// Gets the total number of mentions (users and roles) allowed per message. - /// - [JsonProperty("mention_total_limit", NullValueHandling = NullValueHandling.Ignore)] - public int? MentionTotalLimit { get; internal set; } - - internal DiscordRuleTriggerMetadata() { } -} - +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Represents metadata about the triggering of a Discord rule. +/// +public sealed class DiscordRuleTriggerMetadata +{ + /// + /// Gets substrings which will be searched in the content. + /// + [JsonProperty("keyword_filter", NullValueHandling = NullValueHandling.Ignore)] + public IReadOnlyList? KeywordFilter { get; internal set; } + + /// + /// Gets regex patterns which will be matched against the content. + /// + [JsonProperty("regex_patterns")] + public IReadOnlyList? RegexPatterns { get; internal set; } + + /// + /// Gets the internally pre-defined wordsets which will be searched in the content. + /// + [JsonProperty("presets", NullValueHandling = NullValueHandling.Ignore)] + public IReadOnlyList? KeywordPresetTypes { get; internal set; } + + /// + /// Gets the substrings which should not trigger the rule. + /// + [JsonProperty("allow_list")] + public IReadOnlyList? AllowedKeywords { get; internal set; } + + /// + /// Gets the total number of mentions (users and roles) allowed per message. + /// + [JsonProperty("mention_total_limit", NullValueHandling = NullValueHandling.Ignore)] + public int? MentionTotalLimit { get; internal set; } + + internal DiscordRuleTriggerMetadata() { } +} + diff --git a/DSharpPlus/Entities/AutoModeration/DiscordRuleTriggerMetadataBuilder.cs b/DSharpPlus/Entities/AutoModeration/DiscordRuleTriggerMetadataBuilder.cs index 55c2e88884..f93023117f 100644 --- a/DSharpPlus/Entities/AutoModeration/DiscordRuleTriggerMetadataBuilder.cs +++ b/DSharpPlus/Entities/AutoModeration/DiscordRuleTriggerMetadataBuilder.cs @@ -1,135 +1,135 @@ -using System; -using System.Collections.Generic; - -namespace DSharpPlus.Entities.AutoModeration; - -public sealed class DiscordRuleTriggerMetadataBuilder -{ - /// - /// Sets substrings which will be searched in the content. - /// - public IReadOnlyList? KeywordFilter { get; private set; } - - /// - /// Sets regex patterns which will be matched against the content. - /// - public IReadOnlyList? RegexPatterns { get; private set; } - - /// - /// Sets the internally pre-defined wordsets which will be searched in the content. - /// - public IReadOnlyList? KeywordPresetTypes { get; private set; } - - /// - /// Sets the substrings which should not trigger the rule. - /// - public IReadOnlyList? AllowedKeywords { get; private set; } - - /// - /// Sets the total number of mentions (users and roles) allowed per message. - /// - public int? MentionTotalLimit { get; private set; } - - /// - /// Sets keywords that will be searched in messages content. - /// - /// The keywords that will be searched. - /// This builder. - /// - public DiscordRuleTriggerMetadataBuilder AddKeywordFilter(IReadOnlyList keywordFilter) - { - if (keywordFilter.Count > 1000) - { - throw new ArgumentException("Keyword filter can't contains more than 1000 substrings."); - } - - this.KeywordFilter = keywordFilter; - - return this; - } - - /// - /// Sets the regex patterns. - /// - /// - /// This builder. - /// - public DiscordRuleTriggerMetadataBuilder AddRegexPatterns(IReadOnlyList regexPatterns) - { - if (regexPatterns.Count > 10) - { - throw new ArgumentException("Regex patterns count can't be higher than 10."); - } - - this.RegexPatterns = regexPatterns; - - return this; - } - - /// - /// Sets the rule keyword preset types. - /// - /// The rule keyword preset types to set. - /// This builder. - /// - public DiscordRuleTriggerMetadataBuilder AddKeywordPresetTypes(IReadOnlyList keywordPresetTypes) - { - this.KeywordPresetTypes = keywordPresetTypes ?? throw new ArgumentNullException(nameof(keywordPresetTypes)); - - return this; - } - - /// - /// Sets an allowed keyword list. - /// - /// The keyword list to set. - /// This builder. - /// - public DiscordRuleTriggerMetadataBuilder AddAllowedKeywordList(IReadOnlyList allowList) - { - if (allowList.Count > 100) - { - throw new ArgumentException("Allowed keyword count can't be higher than 100."); - } - - this.AllowedKeywords = allowList; - - return this; - } - - /// - /// Sets the total mention limit. - /// - /// The total mention limit number. - /// This builder. - /// - public DiscordRuleTriggerMetadataBuilder WithMentionTotalLimit(int? mentionTotalLimit) - { - if (mentionTotalLimit > 50) - { - throw new ArgumentException("Mention total limit can't be higher than 50."); - } - - this.MentionTotalLimit = mentionTotalLimit; - - return this; - } - - /// - /// Constructs a new trigger rule metadata. - /// - /// The build trigger metadata. - public DiscordRuleTriggerMetadata Build() - { - DiscordRuleTriggerMetadata metadata = new() - { - AllowedKeywords = this.AllowedKeywords ?? Array.Empty(), - KeywordFilter = this.KeywordFilter, - KeywordPresetTypes = this.KeywordPresetTypes, - MentionTotalLimit = this.MentionTotalLimit, - RegexPatterns = this.RegexPatterns ?? Array.Empty() - }; - - return metadata; - } -} +using System; +using System.Collections.Generic; + +namespace DSharpPlus.Entities.AutoModeration; + +public sealed class DiscordRuleTriggerMetadataBuilder +{ + /// + /// Sets substrings which will be searched in the content. + /// + public IReadOnlyList? KeywordFilter { get; private set; } + + /// + /// Sets regex patterns which will be matched against the content. + /// + public IReadOnlyList? RegexPatterns { get; private set; } + + /// + /// Sets the internally pre-defined wordsets which will be searched in the content. + /// + public IReadOnlyList? KeywordPresetTypes { get; private set; } + + /// + /// Sets the substrings which should not trigger the rule. + /// + public IReadOnlyList? AllowedKeywords { get; private set; } + + /// + /// Sets the total number of mentions (users and roles) allowed per message. + /// + public int? MentionTotalLimit { get; private set; } + + /// + /// Sets keywords that will be searched in messages content. + /// + /// The keywords that will be searched. + /// This builder. + /// + public DiscordRuleTriggerMetadataBuilder AddKeywordFilter(IReadOnlyList keywordFilter) + { + if (keywordFilter.Count > 1000) + { + throw new ArgumentException("Keyword filter can't contains more than 1000 substrings."); + } + + this.KeywordFilter = keywordFilter; + + return this; + } + + /// + /// Sets the regex patterns. + /// + /// + /// This builder. + /// + public DiscordRuleTriggerMetadataBuilder AddRegexPatterns(IReadOnlyList regexPatterns) + { + if (regexPatterns.Count > 10) + { + throw new ArgumentException("Regex patterns count can't be higher than 10."); + } + + this.RegexPatterns = regexPatterns; + + return this; + } + + /// + /// Sets the rule keyword preset types. + /// + /// The rule keyword preset types to set. + /// This builder. + /// + public DiscordRuleTriggerMetadataBuilder AddKeywordPresetTypes(IReadOnlyList keywordPresetTypes) + { + this.KeywordPresetTypes = keywordPresetTypes ?? throw new ArgumentNullException(nameof(keywordPresetTypes)); + + return this; + } + + /// + /// Sets an allowed keyword list. + /// + /// The keyword list to set. + /// This builder. + /// + public DiscordRuleTriggerMetadataBuilder AddAllowedKeywordList(IReadOnlyList allowList) + { + if (allowList.Count > 100) + { + throw new ArgumentException("Allowed keyword count can't be higher than 100."); + } + + this.AllowedKeywords = allowList; + + return this; + } + + /// + /// Sets the total mention limit. + /// + /// The total mention limit number. + /// This builder. + /// + public DiscordRuleTriggerMetadataBuilder WithMentionTotalLimit(int? mentionTotalLimit) + { + if (mentionTotalLimit > 50) + { + throw new ArgumentException("Mention total limit can't be higher than 50."); + } + + this.MentionTotalLimit = mentionTotalLimit; + + return this; + } + + /// + /// Constructs a new trigger rule metadata. + /// + /// The build trigger metadata. + public DiscordRuleTriggerMetadata Build() + { + DiscordRuleTriggerMetadata metadata = new() + { + AllowedKeywords = this.AllowedKeywords ?? Array.Empty(), + KeywordFilter = this.KeywordFilter, + KeywordPresetTypes = this.KeywordPresetTypes, + MentionTotalLimit = this.MentionTotalLimit, + RegexPatterns = this.RegexPatterns ?? Array.Empty() + }; + + return metadata; + } +} diff --git a/DSharpPlus/Entities/AutoModeration/DiscordRuleTriggerType.cs b/DSharpPlus/Entities/AutoModeration/DiscordRuleTriggerType.cs index 0df6bd1ee9..e4b18cd083 100644 --- a/DSharpPlus/Entities/AutoModeration/DiscordRuleTriggerType.cs +++ b/DSharpPlus/Entities/AutoModeration/DiscordRuleTriggerType.cs @@ -1,28 +1,28 @@ -namespace DSharpPlus.Entities; - - -/// -/// Characterizes the type of content which can trigger a rule. -/// -public enum DiscordRuleTriggerType -{ - /// - /// Check if the content contains words from a definied list of keywords. - /// - Keyword = 1, - - /// - /// Check if the content is a spam. - /// - Spam = 3, - - /// - /// Check if the content contains words from pre-defined wordsets. - /// - KeywordPreset = 4, - - /// - /// Check if the content contains moure unique mentions than allowed. - /// - MentionSpam = 5, -} +namespace DSharpPlus.Entities; + + +/// +/// Characterizes the type of content which can trigger a rule. +/// +public enum DiscordRuleTriggerType +{ + /// + /// Check if the content contains words from a definied list of keywords. + /// + Keyword = 1, + + /// + /// Check if the content is a spam. + /// + Spam = 3, + + /// + /// Check if the content contains words from pre-defined wordsets. + /// + KeywordPreset = 4, + + /// + /// Check if the content contains moure unique mentions than allowed. + /// + MentionSpam = 5, +} diff --git a/DSharpPlus/Entities/BaseDiscordMessageBuilder.cs b/DSharpPlus/Entities/BaseDiscordMessageBuilder.cs index 90da7f24fe..c2050e758b 100644 --- a/DSharpPlus/Entities/BaseDiscordMessageBuilder.cs +++ b/DSharpPlus/Entities/BaseDiscordMessageBuilder.cs @@ -1,1063 +1,1063 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; -using DSharpPlus.Net; - -namespace DSharpPlus.Entities; - -/// -/// An abstraction for the different message builders in DSharpPlus. -/// -public abstract class BaseDiscordMessageBuilder : IDiscordMessageBuilder where T : BaseDiscordMessageBuilder - // This has got to be the most big brain thing I have ever done with interfaces lmfao -{ - /// - /// The contents of this message. - /// - public string? Content - { - get; - set - { - if (value != null && value.Length > 2000) - { - throw new ArgumentException("Content length cannot exceed 2000 characters.", nameof(value)); - } - - SetIfV2Disabled(ref field, value, nameof(Content)); - } - } - - public DiscordMessageFlags Flags { get; internal set; } - - public T SuppressNotifications() - { - this.Flags |= DiscordMessageFlags.SuppressNotifications; - return (T)this; - } - - /// - /// Enables support for V2 components; messages with the V2 flag cannot be downgraded. - /// - /// The builder to chain calls with. - public T EnableV2Components() - { - bool isAnyContentSet = this.Content is not null || this.Embeds is not []; - - if (isAnyContentSet) - { - throw new InvalidOperationException("Content and embeds are not supported with Components V2. Please call .Clear first."); - } - - this.Flags |= DiscordMessageFlags.IsComponentsV2; - return (T)this; - } - - /// - /// Disables V2 components IF this builder does not currently contain illegal components. - /// - /// The builder contains V2 components and cannot be downgraded. - /// This method only disables the V2 components flag; the message originally associated with this builder cannot be downgraded, and this method only exists for convenience. - public T DisableV2Components() - { - if (this.components.Any(c => c is not DiscordActionRowComponent)) - { - throw new InvalidOperationException - ( - "This builder cannot contain V2 components when disabling V2 component support. Call ClearComponents() first." - ); - } - - this.Flags &= ~DiscordMessageFlags.IsComponentsV2; - - return (T)this; - } - - public bool IsTTS { get; set; } - - /// - /// Gets or sets a poll for this message. - /// - public DiscordPollBuilder? Poll { get; set => SetIfV2Disabled(ref field, value, nameof(Poll)); } - - /// - /// Embeds to send on this webhook request. - /// - public IReadOnlyList Embeds => this.embeds; - internal List embeds = []; - - /// - /// Files to send on this webhook request. - /// - public IReadOnlyList Files => this.files; - internal List files = []; - - /// - /// Mentions to send on this webhook request. - /// - public IReadOnlyList Mentions => this.mentions; - internal List mentions = []; - - /// - /// Components to send on this message. - /// - public IReadOnlyList Components => this.components; - internal List components = []; - - /// - /// Components, filtered for only action rows. - /// - public IReadOnlyList? ComponentActionRows - => this.Components?.Where(x => x is DiscordActionRowComponent).Cast().ToList(); - - /// - /// Thou shalt NOT PASS! ⚡ - /// - // i'm very proud that we have the actual LOTR quote here, not the movie "you shall not pass" - internal BaseDiscordMessageBuilder() { } - - /// - /// Constructs a new based on an existing . - /// Existing file streams will have their position reset to 0. - /// - /// The builder to copy. - protected BaseDiscordMessageBuilder(IDiscordMessageBuilder builder) - { - this.Content = builder.Content; - this.mentions.AddRange([.. builder.Mentions]); - this.embeds.AddRange(builder.Embeds); - this.components.AddRange(builder.Components); - this.files.AddRange(builder.Files); - this.IsTTS = builder.IsTTS; - this.Poll = builder.Poll; - this.Flags = builder.Flags; - } - - /// - /// Sets the content of the Message. - /// - /// The content to be set. - /// The current builder to be chained. - public T WithContent(string content) - { - ThrowIfV2Enabled(); - this.Content = content; - return (T)this; - } - - public T AddActionRowComponent - ( - DiscordActionRowComponent component - ) - { - EnsureSufficientSpaceForComponent(component); - this.components.Add(component); - - return (T)this; - } - - /// - /// Adds a new action row with the given component. - /// - /// The select menu to add, if possible. - /// The builder to chain calls with. - /// Thrown if there is insufficient slots to support the component. - public T AddActionRowComponent - ( - BaseDiscordSelectComponent selectMenu - ) - { - DiscordActionRowComponent component = new DiscordActionRowComponent([selectMenu]); - - EnsureSufficientSpaceForComponent(component); - - this.components.Add(component); - return (T)this; - } - - /// - /// Adds buttons to the builder. - /// - /// The buttons to add to the message. They will automatically be chunked into separate action rows as necessary. - /// The builder to chain calls with. - /// Thrown if there is insufficient slots to support the component. - public T AddActionRowComponent - ( - params IEnumerable buttons - ) - { - IEnumerable> components = buttons.Chunk(5); - - foreach (IEnumerable component in components) - { - DiscordActionRowComponent currentComponent = new(component); - - EnsureSufficientSpaceForComponent(currentComponent); - this.components.Add(currentComponent); - } - - return (T)this; - } - - /// - /// Adds a media gallery to this builder. - /// - /// The items to add. - /// The builder to chain calls with. - /// Thrown if there is insufficient slots to support the component. - public T AddMediaGalleryComponent - ( - params IEnumerable galleryItems - ) - { - int itemCount = galleryItems.TryGetNonEnumeratedCount(out int fastCount) ? fastCount : galleryItems.Count(); - - if (itemCount is 0) - { - throw new InvalidOperationException("At least one item must be added to the media gallery."); - } - - if (itemCount > 10) - { - throw new InvalidOperationException("At most 10 items can be added to the media gallery."); - } - - DiscordMediaGalleryComponent component = new(galleryItems); - - EnsureSufficientSpaceForComponent(component); - this.components.Add(component); - - return (T)this; - } - - /// - /// Adds a section component to the builder. - /// - /// The builder to chain calls with. - /// Thrown if there is insufficient slots to support the component. - public T AddSectionComponent - ( - DiscordSectionComponent section - ) - { - EnsureSufficientSpaceForComponent(section); - this.components.Add(section); - - return (T)this; - } - - /// - /// Adds a text display to this builder. - /// - /// The builder to chain calls with. - /// Thrown if there is insufficient slots to support the component. - public T AddTextDisplayComponent - ( - DiscordTextDisplayComponent component - ) - { - EnsureSufficientSpaceForComponent(component); - this.components.Add(component); - - return (T)this; - } - - /// - /// Adds a text input to this builder. - /// - /// The component to add. - /// The builder to chain calls with. - /// Thrown if there is insufficient slots to support the component. - public T AddTextInputComponent - ( - DiscordTextInputComponent component - ) - { - var actionRow = new DiscordActionRowComponent([component]); - EnsureSufficientSpaceForComponent(actionRow); - this.components.Add(actionRow); - - return (T)this; - } - - /// - /// Adds a text display to this builder. - /// - /// - /// The builder to chain calls with. - /// Thrown if there is insufficient slots to support the component. - public T AddTextDisplayComponent - ( - string content - ) - { - DiscordTextDisplayComponent component = new(content); - - EnsureSufficientSpaceForComponent(component); - this.components.Add(component); - - return (T)this; - } - - /// - /// Adds a separator component to this builder. - /// - /// The component to add. - /// The builder to chain calls with. - /// Thrown if there is insufficient slots to support the component. - public T AddSeparatorComponent - ( - DiscordSeparatorComponent component - ) - { - EnsureSufficientSpaceForComponent(component); - this.components.Add(component); - - return (T)this; - } - - /// - /// Adds a file component to this builder. - /// - /// The component to add. - /// The builder to chain calls with. - /// Thrown if there is insufficient slots to support the component. - public T AddFileComponent - ( - DiscordFileComponent component - ) - { - EnsureSufficientSpaceForComponent(component); - this.components.Add(component); - - return (T)this; - } - - - /// - /// Adds a container component to this builder. - /// - /// The component to add. - /// The builder to chain calls with. - /// Thrown if there is insufficient slots to support the component. - public T AddContainerComponent - ( - DiscordContainerComponent component - ) - { - EnsureSufficientSpaceForComponent(component); - this.components.Add(component); - - return (T)this; - } - - /// - /// Sets if the message should be TTS. - /// - /// If TTS should be set. - /// The current builder to be chained. - public T WithTTS(bool isTTS) - { - this.IsTTS = isTTS; - return (T)this; - } - - public T WithPoll(DiscordPollBuilder poll) - { - ThrowIfV2Enabled(); - this.Poll = poll; - return (T)this; - } - - /// - /// Appends an embed to the current builder. - /// - /// The embed that should be appended. - /// The current builder to be chained. - public T AddEmbed(DiscordEmbed embed) - { - ThrowIfV2Enabled(); - if (embed is null) - { - return (T)this; //Providing null embeds will produce a 400 response from Discord.// - } - - this.embeds.Add(embed); - return (T)this; - } - - /// - /// Appends several embeds to the current builder. - /// - /// The embeds that should be appended. - /// The current builder to be chained. - public T AddEmbeds(IEnumerable embeds) - { - ThrowIfV2Enabled(); - this.embeds.AddRange(embeds); - return (T)this; - } - - /// - /// Clears the embeds on the current builder. - /// - /// The current builder for chaining. - public T ClearEmbeds() - { - this.embeds.Clear(); - return (T)this; - } - - /// - /// Removes the embed at the specified index. - /// - /// The current builder for chaining. - public T RemoveEmbedAt(int index) - { - this.embeds.RemoveAt(index); - return (T)this; - } - - /// - /// Removes the specified range of embeds. - /// - /// The starting index of the embeds to remove. - /// The amount of embeds to remove. - /// The current builder for chaining. - public T RemoveEmbeds(int index, int count) - { - this.embeds.RemoveRange(index, count); - return (T)this; - } - - /// - /// Sets if the message has files to be sent. - /// - /// The fileName that the file should be sent as. - /// The Stream to the file. - /// Tells the API Client to reset the stream position to what it was after the file is sent. - /// The current builder to be chained. - public T AddFile(string fileName, Stream stream, bool resetStreamPosition = false) => AddFile(fileName, stream, resetStreamPosition ? AddFileOptions.ResetStream : AddFileOptions.None); - - /// - /// Sets if the message has files to be sent. - /// - /// The Stream to the file. - /// Tells the API Client to reset the stream position to what it was after the file is sent. - /// The current builder to be chained. - public T AddFile(FileStream stream, bool resetStreamPosition = false) => AddFile(stream, resetStreamPosition ? AddFileOptions.ResetStream : AddFileOptions.None); - - /// - /// Sets if the message has files to be sent. - /// - /// The Files that should be sent. - /// Tells the API Client to reset the stream position to what it was after the file is sent. - /// The current builder to be chained. - public T AddFiles(IDictionary files, bool resetStreamPosition = false) => AddFiles(files, resetStreamPosition ? AddFileOptions.ResetStream : AddFileOptions.None); - - /// - /// Attaches a file to this message. - /// - /// Name of the file to attach. - /// Stream containing said file's contents. - /// Additional flags for the handling of the file stream. - /// The current builder to be chained. - public T AddFile(string fileName, Stream stream, AddFileOptions fileOptions) - { - if (this.Files.Count >= 10) - { - throw new ArgumentException("Cannot send more than 10 files with a single message."); - } - - if (this.files.Any(x => x.FileName == fileName)) - { - throw new ArgumentException("A file with that filename already exists"); - } - - stream = ResolveStream(stream, fileOptions); - long? resetPosition = fileOptions.HasFlag(AddFileOptions.ResetStream) ? stream.Position : null; - this.files.Add(new DiscordMessageFile(fileName, stream, resetPosition, fileOptions: fileOptions)); - - return (T)this; - } - - /// - /// Attaches a file to this message. - /// - /// FileStream pointing to the file to attach. - /// Additional flags for the handling of the file stream. - /// The current builder to be chained. - public T AddFile(FileStream stream, AddFileOptions fileOptions) => AddFile(stream.Name, stream, fileOptions); - - /// - /// Attaches multiple files to this message. - /// - /// Dictionary of files to add, where is a file name and is a stream containing the file's contents. - /// Additional flags for the handling of the file streams. - /// The current builder to be chained. - public T AddFiles(IDictionary files, AddFileOptions fileOptions) - { - if (this.Files.Count + files.Count > 10) - { - throw new ArgumentException("Cannot send more than 10 files with a single message."); - } - - foreach (KeyValuePair file in files) - { - if (this.files.Any(x => x.FileName == file.Key)) - { - throw new ArgumentException("A File with that filename already exists"); - } - - Stream stream = ResolveStream(file.Value, fileOptions); - long? resetPosition = fileOptions.HasFlag(AddFileOptions.ResetStream) ? stream.Position : null; - this.files.Add(new DiscordMessageFile(file.Key, stream, resetPosition, fileOptions: fileOptions)); - } - - return (T)this; - } - - public T AddFiles(IEnumerable files) - { - this.files.AddRange(files); - return (T)this; - } - - /// - /// Adds the mention to the mentions to parse, etc. with the interaction response. - /// - /// Mention to add. - public T AddMention(IMention mention) - { - this.mentions.Add(mention); - return (T)this; - } - - /// - /// Adds the mentions to the mentions to parse, etc. with the interaction response. - /// - /// Mentions to add. - public T AddMentions(IEnumerable mentions) - { - this.mentions.AddRange(mentions); - return (T)this; - } - - /// - /// Clears all message components on this builder. - /// - public virtual void ClearComponents() - => this.components.Clear(); - - /// - /// Allows for clearing the Message Builder so that it can be used again to send a new message. - /// - public virtual void Clear() - { - this.Content = ""; - this.embeds.Clear(); - this.IsTTS = false; - this.mentions.Clear(); - this.files.Clear(); - this.components.Clear(); - this.Flags = default; - } - - /// - public void Dispose() - { - // We don't bother to fully implement the dispose pattern - // since deriving from this type outside this assembly is unusual. - - foreach (DiscordMessageFile file in this.files) - { - if (file.FileOptions.HasFlag(AddFileOptions.CloseStream)) - { - if (file.Stream is RequestStreamWrapper wrapper) - { - wrapper.UnderlyingStream.Dispose(); - } - else - { - file.Stream.Dispose(); - } - } - } - - GC.SuppressFinalize(this); - } - - /// - public async ValueTask DisposeAsync() - { - foreach (DiscordMessageFile file in this.files) - { - if (file.FileOptions.HasFlag(AddFileOptions.CloseStream)) - { - if (file.Stream is RequestStreamWrapper wrapper) - { - await wrapper.UnderlyingStream.DisposeAsync(); - } - else - { - await file.Stream.DisposeAsync(); - } - } - } - - GC.SuppressFinalize(this); - } - - /// - /// Helper method to reset stream positions used several times by the API client. - /// - internal void ResetFileStreamPositions() - { - foreach (DiscordMessageFile file in this.files) - { - if (file.ResetPositionTo is long pos) - { - file.Stream.Seek(pos, SeekOrigin.Begin); - } - } - } - - /// - /// Helper method to resolve stream copies depending on the file mode parameter. - /// - private static Stream ResolveStream(Stream stream, AddFileOptions fileOptions) - { - if (!fileOptions.HasFlag(AddFileOptions.CopyStream)) - { - return new RequestStreamWrapper(stream); - } - - Stream originalStream = stream; - MemoryStream newStream = new(); - originalStream.CopyTo(newStream); - newStream.Position = 0; - if (fileOptions.HasFlag(AddFileOptions.CloseStream)) - { - originalStream.Dispose(); - } - - return newStream; - } - - [StackTraceHidden] - [DebuggerStepThrough] - private void EnsureSufficientSpaceForComponent - ( - DiscordComponent component - ) - { - const int CV2_MAX_TOTAL_COMPONENTS = 40; - int maxTopComponents = this.Flags.HasFlag(DiscordMessageFlags.IsComponentsV2) ? CV2_MAX_TOTAL_COMPONENTS : 5; - int maxAllComponents = this.Flags.HasFlag(DiscordMessageFlags.IsComponentsV2) ? CV2_MAX_TOTAL_COMPONENTS : 25; - - int allComponentCount = this.Components.Sum - ( - c => - { - return c switch - { - DiscordActionRowComponent arc => 1 + arc.Components.Count, - DiscordSectionComponent section => 2 + section.Components.Count, - DiscordContainerComponent container => 1 + container.Components.Sum - ( - nc => nc switch - { - DiscordActionRowComponent narc => narc.Components.Count + 1, - DiscordSectionComponent narc => narc.Components.Count + 2, - _ => 1, - } - ), - _ => 1 - }; - } - ); - - int requiredSpaceForComponent = component switch - { - DiscordActionRowComponent arc => arc.Components.Count + 1, // Action row + components - DiscordSectionComponent section => section.Components.Count + 2, // Section + Accessory - DiscordContainerComponent container => container.Components.Sum - ( - nc => nc switch - { - DiscordActionRowComponent narc => narc.Components.Count + 1, - DiscordSectionComponent narc => narc.Components.Count + 2, - _ => 1, - } - ), - _ => 1, - }; - - if (this.Components.Count + 1 > maxTopComponents) - { - throw new InvalidOperationException($"Too many top-level components! Maximum allowed is {maxTopComponents}."); - } - - if (allComponentCount + requiredSpaceForComponent > maxAllComponents) - { - throw new InvalidOperationException($"Too many components! Maximum allowed is {maxAllComponents}; {component.GetType().Name} requires {requiredSpaceForComponent} slots."); - } - } - - private void SetIfV2Disabled(ref TField field, TField value, string fieldName) - { - if (this.Flags.HasFlag(DiscordMessageFlags.IsComponentsV2)) - { - throw new ArgumentException("This field cannot be set when V2 components is enabled.", fieldName); - } - - field = value; - } - - [StackTraceHidden] - [DebuggerStepThrough] - private void ThrowIfV2Enabled([CallerMemberName] string caller = "") - { - if (this.Flags.HasFlag(DiscordMessageFlags.IsComponentsV2)) - { - throw new InvalidOperationException($"{caller} cannot be called when V2 components is enabled."); - } - } - - IDiscordMessageBuilder IDiscordMessageBuilder.EnableV2Components() => this.EnableV2Components(); - IDiscordMessageBuilder IDiscordMessageBuilder.DisableV2Components() => this.DisableV2Components(); - IDiscordMessageBuilder IDiscordMessageBuilder.AddActionRowComponent(DiscordActionRowComponent component) => this.AddActionRowComponent(component); - IDiscordMessageBuilder IDiscordMessageBuilder.AddActionRowComponent(DiscordSelectComponent selectMenu) => this.AddActionRowComponent(selectMenu); - IDiscordMessageBuilder IDiscordMessageBuilder.AddActionRowComponent(params IEnumerable buttons) => this.AddActionRowComponent(buttons); - IDiscordMessageBuilder IDiscordMessageBuilder.AddMediaGalleryComponent(params IEnumerable galleryItems) => this.AddMediaGalleryComponent(galleryItems); - IDiscordMessageBuilder IDiscordMessageBuilder.AddSectionComponent(DiscordSectionComponent section) => this.AddSectionComponent(section); - IDiscordMessageBuilder IDiscordMessageBuilder.AddTextDisplayComponent(DiscordTextDisplayComponent component) => this.AddTextDisplayComponent(component); - IDiscordMessageBuilder IDiscordMessageBuilder.AddTextDisplayComponent(string content) => this.AddTextDisplayComponent(content); - IDiscordMessageBuilder IDiscordMessageBuilder.AddTextInputComponent(DiscordTextInputComponent component) => this.AddTextInputComponent(component); - IDiscordMessageBuilder IDiscordMessageBuilder.AddSeparatorComponent(DiscordSeparatorComponent component) => this.AddSeparatorComponent(component); - IDiscordMessageBuilder IDiscordMessageBuilder.AddFileComponent(DiscordFileComponent component) => this.AddFileComponent(component); - IDiscordMessageBuilder IDiscordMessageBuilder.AddContainerComponent(DiscordContainerComponent component) => this.AddContainerComponent(component); - IDiscordMessageBuilder IDiscordMessageBuilder.SuppressNotifications() => SuppressNotifications(); - IDiscordMessageBuilder IDiscordMessageBuilder.WithContent(string content) => WithContent(content); - IDiscordMessageBuilder IDiscordMessageBuilder.WithTTS(bool isTTS) => WithTTS(isTTS); - IDiscordMessageBuilder IDiscordMessageBuilder.AddEmbed(DiscordEmbed embed) => AddEmbed(embed); - IDiscordMessageBuilder IDiscordMessageBuilder.AddEmbeds(IEnumerable embeds) => AddEmbeds(embeds); - IDiscordMessageBuilder IDiscordMessageBuilder.AddFile(string fileName, Stream stream, bool resetStream) => AddFile(fileName, stream, resetStream); - IDiscordMessageBuilder IDiscordMessageBuilder.AddFile(FileStream stream, bool resetStream) => AddFile(stream, resetStream); - IDiscordMessageBuilder IDiscordMessageBuilder.AddFiles(IDictionary files, bool resetStreams) => AddFiles(files, resetStreams); - IDiscordMessageBuilder IDiscordMessageBuilder.AddFiles(IEnumerable files) => AddFiles(files); - IDiscordMessageBuilder IDiscordMessageBuilder.AddFile(string fileName, Stream stream, AddFileOptions fileOptions) => AddFile(fileName, stream, fileOptions); - IDiscordMessageBuilder IDiscordMessageBuilder.AddFile(FileStream stream, AddFileOptions fileOptions) => AddFile(stream, fileOptions); - IDiscordMessageBuilder IDiscordMessageBuilder.AddFiles(IDictionary files, AddFileOptions fileOptions) => AddFiles(files, fileOptions); - IDiscordMessageBuilder IDiscordMessageBuilder.AddMention(IMention mention) => AddMention(mention); - IDiscordMessageBuilder IDiscordMessageBuilder.AddMentions(IEnumerable mentions) => AddMentions(mentions); -} - -/// -/// Additional flags for files added to a message builder. -/// -[Flags] -public enum AddFileOptions -{ - /// - /// No additional behavior. The stream will read to completion and is left at that position after sending. - /// - None = 0, - - /// - /// Resets the stream to its original position after sending. - /// - ResetStream = 0x1, - - /// - /// Closes the stream upon disposal of the message builder. - /// - /// - /// Streams will not be disposed upon sending. Disposal of the message builder is necessary. - /// - CloseStream = 0x2, - - /// - /// Immediately reads the stream to completion and copies its contents to an in-memory stream. - /// - /// - /// - /// Note that this incurs an additional memory overhead and may perform synchronous I/O and should only be used if the source stream cannot be kept open any longer. - /// - /// - /// If specified together with , the stream will closed immediately after the copy. - /// - /// - CopyStream = 0x4, -} - -/// -/// Base interface for any discord message builder. -/// -public interface IDiscordMessageBuilder : IDisposable, IAsyncDisposable -{ - /// - /// Getter / setter for message content. - /// - public string? Content { get; set; } - - /// - /// Whether this message will play as a text-to-speech message. - /// - public bool IsTTS { get; set; } - - /// - /// Gets or sets a poll for this message. - /// - public DiscordPollBuilder? Poll { get; set; } - - /// - /// All embeds on this message. - /// - public IReadOnlyList Embeds { get; } - - /// - /// All files on this message. - /// - public IReadOnlyList Files { get; } - - /// - /// All components on this message. - /// - public IReadOnlyList Components { get; } - - /// - /// All allowed mentions on this message. - /// - public IReadOnlyList Mentions { get; } - - public DiscordMessageFlags Flags { get; } - - /// - /// Adds content to this message - /// - /// Message content to use - /// - public IDiscordMessageBuilder WithContent(string content); - - /// - /// Enables support for V2 components; messages with the V2 flag cannot be downgraded. - /// - /// The builder to chain calls with. - public IDiscordMessageBuilder EnableV2Components(); - - /// - /// Disables V2 components IF this builder does not currently contain illegal components. - /// - /// The builder to chain calls with. - /// The builder contains V2 components and cannot be downgraded. - /// This method only disables the V2 components flag; the message originally associated with this builder cannot be downgraded, and this method only exists for convenience. - public IDiscordMessageBuilder DisableV2Components(); - - /// - /// Adds a raw action row. - /// - /// The select menu to add, if possible. - /// The builder to chain calls with. - /// Thrown if there is insufficient slots to support the component. - public IDiscordMessageBuilder AddActionRowComponent(DiscordActionRowComponent component); - - /// - /// Adds a new action row with the given component. - /// - /// The select menu to add, if possible. - /// The builder to chain calls with. - /// Thrown if there is insufficient slots to support the component. - public IDiscordMessageBuilder AddActionRowComponent(DiscordSelectComponent selectMenu); - - /// - /// Adds buttons to the builder. - /// - /// The buttons to add to the message. They will automatically be chunked into separate action rows as necessary. - /// The builder to chain calls with. - /// Thrown if there is insufficient slots to support the component. - public IDiscordMessageBuilder AddActionRowComponent(params IEnumerable buttons); - - /// - /// Adds a media gallery to this builder. - /// - /// The items to add. - /// The builder to chain calls with. - /// Thrown if there is insufficient slots to support the component. - public IDiscordMessageBuilder AddMediaGalleryComponent(params IEnumerable galleryItems); - - /// - /// Adds a section component to the builder. - /// - /// The builder to chain calls with. - /// Thrown if there is insufficient slots to support the component. - public IDiscordMessageBuilder AddSectionComponent(DiscordSectionComponent section); - - /// - /// Adds a text display to this builder. - /// - /// The builder to chain calls with. - /// Thrown if there is insufficient slots to support the component. - public IDiscordMessageBuilder AddTextDisplayComponent(DiscordTextDisplayComponent component); - - /// - /// Adds a text display to this builder. - /// - /// - /// The builder to chain calls with. - /// Thrown if there is insufficient slots to support the component. - public IDiscordMessageBuilder AddTextDisplayComponent(string content); - - /// - /// Adds a text input to this builder. - /// - /// The component to add. - /// The builder to chain calls with. - /// Thrown if there is insufficient slots to support the component. - public IDiscordMessageBuilder AddTextInputComponent(DiscordTextInputComponent component); - - /// - /// Adds a separator component to this builder. - /// - /// The component to add. - /// The builder to chain calls with. - /// Thrown if there is insufficient slots to support the component. - public IDiscordMessageBuilder AddSeparatorComponent(DiscordSeparatorComponent component); - - /// - /// Adds a file component to this builder. - /// - /// The component to add. - /// The builder to chain calls with. - /// Thrown if there is insufficient slots to support the component. - public IDiscordMessageBuilder AddFileComponent(DiscordFileComponent component); - - /// - /// Adds a container component to this builder. - /// - /// The component to add. - /// The builder to chain calls with. - /// Thrown if there is insufficient slots to support the component. - public IDiscordMessageBuilder AddContainerComponent(DiscordContainerComponent component); - - /// - /// Sets whether this message should play as a text-to-speech message. - /// - /// - /// - public IDiscordMessageBuilder WithTTS(bool isTTS); - - /// - /// Adds an embed to this message. - /// - /// Embed to add. - /// - public IDiscordMessageBuilder AddEmbed(DiscordEmbed embed); - - /// - /// Adds multiple embeds to this message. - /// - /// Collection of embeds to add. - /// - public IDiscordMessageBuilder AddEmbeds(IEnumerable embeds); - - /// - /// Attaches a file to this message. - /// - /// Name of the file to attach. - /// Stream containing said file's contents. - /// Whether to reset the stream to position 0 after sending. - /// - public IDiscordMessageBuilder AddFile(string fileName, Stream stream, bool resetStream = false); - - /// - /// Attaches a file to this message. - /// - /// FileStream pointing to the file to attach. - /// Whether to reset the stream position to 0 after sending. - /// - public IDiscordMessageBuilder AddFile(FileStream stream, bool resetStream = false); - - /// - /// Attaches multiple files to this message. - /// - /// Dictionary of files to add, where is a file name and is a stream containing the file's contents. - /// Whether to reset all stream positions to 0 after sending. - /// - public IDiscordMessageBuilder AddFiles(IDictionary files, bool resetStreams = false); - - /// - /// Attaches a file to this message. - /// - /// Name of the file to attach. - /// Stream containing said file's contents. - /// Additional flags for the handling of the file stream. - /// - public IDiscordMessageBuilder AddFile(string fileName, Stream stream, AddFileOptions fileOptions); - - /// - /// Attaches a file to this message. - /// - /// FileStream pointing to the file to attach. - /// Additional flags for the handling of the file stream. - /// - public IDiscordMessageBuilder AddFile(FileStream stream, AddFileOptions fileOptions); - - /// - /// Attaches multiple files to this message. - /// - /// Dictionary of files to add, where is a file name and is a stream containing the file's contents. - /// Additional flags for the handling of the file streams. - /// - public IDiscordMessageBuilder AddFiles(IDictionary files, AddFileOptions fileOptions); - - /// - /// Attaches previously used files to this file stream. - /// - /// Previously attached files to reattach - /// - public IDiscordMessageBuilder AddFiles(IEnumerable files); - - /// - /// Adds an allowed mention to this message. - /// - /// Mention to allow in this message. - /// - public IDiscordMessageBuilder AddMention(IMention mention); - - /// - /// Adds multiple allowed mentions to this message. - /// - /// Collection of mentions to allow in this message. - /// - public IDiscordMessageBuilder AddMentions(IEnumerable mentions); - - /// - /// Applies to the message. - /// - /// - /// - /// As per , this does not change the message's allowed mentions - /// (controlled by ), but instead prevents a mention from triggering a push notification. - /// - public IDiscordMessageBuilder SuppressNotifications(); - - /// - /// Clears all components attached to this builder. - /// - public void ClearComponents(); - - /// - /// Clears this builder. - /// - public void Clear(); -} - -/* -* Zǎoshang hǎo zhōngguó xiànzài wǒ yǒu BING CHILLING 🥶🍦 -* wǒ hěn xǐhuān BING CHILLING 🥶🍦 -*/ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using DSharpPlus.Net; + +namespace DSharpPlus.Entities; + +/// +/// An abstraction for the different message builders in DSharpPlus. +/// +public abstract class BaseDiscordMessageBuilder : IDiscordMessageBuilder where T : BaseDiscordMessageBuilder + // This has got to be the most big brain thing I have ever done with interfaces lmfao +{ + /// + /// The contents of this message. + /// + public string? Content + { + get; + set + { + if (value != null && value.Length > 2000) + { + throw new ArgumentException("Content length cannot exceed 2000 characters.", nameof(value)); + } + + SetIfV2Disabled(ref field, value, nameof(Content)); + } + } + + public DiscordMessageFlags Flags { get; internal set; } + + public T SuppressNotifications() + { + this.Flags |= DiscordMessageFlags.SuppressNotifications; + return (T)this; + } + + /// + /// Enables support for V2 components; messages with the V2 flag cannot be downgraded. + /// + /// The builder to chain calls with. + public T EnableV2Components() + { + bool isAnyContentSet = this.Content is not null || this.Embeds is not []; + + if (isAnyContentSet) + { + throw new InvalidOperationException("Content and embeds are not supported with Components V2. Please call .Clear first."); + } + + this.Flags |= DiscordMessageFlags.IsComponentsV2; + return (T)this; + } + + /// + /// Disables V2 components IF this builder does not currently contain illegal components. + /// + /// The builder contains V2 components and cannot be downgraded. + /// This method only disables the V2 components flag; the message originally associated with this builder cannot be downgraded, and this method only exists for convenience. + public T DisableV2Components() + { + if (this.components.Any(c => c is not DiscordActionRowComponent)) + { + throw new InvalidOperationException + ( + "This builder cannot contain V2 components when disabling V2 component support. Call ClearComponents() first." + ); + } + + this.Flags &= ~DiscordMessageFlags.IsComponentsV2; + + return (T)this; + } + + public bool IsTTS { get; set; } + + /// + /// Gets or sets a poll for this message. + /// + public DiscordPollBuilder? Poll { get; set => SetIfV2Disabled(ref field, value, nameof(Poll)); } + + /// + /// Embeds to send on this webhook request. + /// + public IReadOnlyList Embeds => this.embeds; + internal List embeds = []; + + /// + /// Files to send on this webhook request. + /// + public IReadOnlyList Files => this.files; + internal List files = []; + + /// + /// Mentions to send on this webhook request. + /// + public IReadOnlyList Mentions => this.mentions; + internal List mentions = []; + + /// + /// Components to send on this message. + /// + public IReadOnlyList Components => this.components; + internal List components = []; + + /// + /// Components, filtered for only action rows. + /// + public IReadOnlyList? ComponentActionRows + => this.Components?.Where(x => x is DiscordActionRowComponent).Cast().ToList(); + + /// + /// Thou shalt NOT PASS! ⚡ + /// + // i'm very proud that we have the actual LOTR quote here, not the movie "you shall not pass" + internal BaseDiscordMessageBuilder() { } + + /// + /// Constructs a new based on an existing . + /// Existing file streams will have their position reset to 0. + /// + /// The builder to copy. + protected BaseDiscordMessageBuilder(IDiscordMessageBuilder builder) + { + this.Content = builder.Content; + this.mentions.AddRange([.. builder.Mentions]); + this.embeds.AddRange(builder.Embeds); + this.components.AddRange(builder.Components); + this.files.AddRange(builder.Files); + this.IsTTS = builder.IsTTS; + this.Poll = builder.Poll; + this.Flags = builder.Flags; + } + + /// + /// Sets the content of the Message. + /// + /// The content to be set. + /// The current builder to be chained. + public T WithContent(string content) + { + ThrowIfV2Enabled(); + this.Content = content; + return (T)this; + } + + public T AddActionRowComponent + ( + DiscordActionRowComponent component + ) + { + EnsureSufficientSpaceForComponent(component); + this.components.Add(component); + + return (T)this; + } + + /// + /// Adds a new action row with the given component. + /// + /// The select menu to add, if possible. + /// The builder to chain calls with. + /// Thrown if there is insufficient slots to support the component. + public T AddActionRowComponent + ( + BaseDiscordSelectComponent selectMenu + ) + { + DiscordActionRowComponent component = new DiscordActionRowComponent([selectMenu]); + + EnsureSufficientSpaceForComponent(component); + + this.components.Add(component); + return (T)this; + } + + /// + /// Adds buttons to the builder. + /// + /// The buttons to add to the message. They will automatically be chunked into separate action rows as necessary. + /// The builder to chain calls with. + /// Thrown if there is insufficient slots to support the component. + public T AddActionRowComponent + ( + params IEnumerable buttons + ) + { + IEnumerable> components = buttons.Chunk(5); + + foreach (IEnumerable component in components) + { + DiscordActionRowComponent currentComponent = new(component); + + EnsureSufficientSpaceForComponent(currentComponent); + this.components.Add(currentComponent); + } + + return (T)this; + } + + /// + /// Adds a media gallery to this builder. + /// + /// The items to add. + /// The builder to chain calls with. + /// Thrown if there is insufficient slots to support the component. + public T AddMediaGalleryComponent + ( + params IEnumerable galleryItems + ) + { + int itemCount = galleryItems.TryGetNonEnumeratedCount(out int fastCount) ? fastCount : galleryItems.Count(); + + if (itemCount is 0) + { + throw new InvalidOperationException("At least one item must be added to the media gallery."); + } + + if (itemCount > 10) + { + throw new InvalidOperationException("At most 10 items can be added to the media gallery."); + } + + DiscordMediaGalleryComponent component = new(galleryItems); + + EnsureSufficientSpaceForComponent(component); + this.components.Add(component); + + return (T)this; + } + + /// + /// Adds a section component to the builder. + /// + /// The builder to chain calls with. + /// Thrown if there is insufficient slots to support the component. + public T AddSectionComponent + ( + DiscordSectionComponent section + ) + { + EnsureSufficientSpaceForComponent(section); + this.components.Add(section); + + return (T)this; + } + + /// + /// Adds a text display to this builder. + /// + /// The builder to chain calls with. + /// Thrown if there is insufficient slots to support the component. + public T AddTextDisplayComponent + ( + DiscordTextDisplayComponent component + ) + { + EnsureSufficientSpaceForComponent(component); + this.components.Add(component); + + return (T)this; + } + + /// + /// Adds a text input to this builder. + /// + /// The component to add. + /// The builder to chain calls with. + /// Thrown if there is insufficient slots to support the component. + public T AddTextInputComponent + ( + DiscordTextInputComponent component + ) + { + var actionRow = new DiscordActionRowComponent([component]); + EnsureSufficientSpaceForComponent(actionRow); + this.components.Add(actionRow); + + return (T)this; + } + + /// + /// Adds a text display to this builder. + /// + /// + /// The builder to chain calls with. + /// Thrown if there is insufficient slots to support the component. + public T AddTextDisplayComponent + ( + string content + ) + { + DiscordTextDisplayComponent component = new(content); + + EnsureSufficientSpaceForComponent(component); + this.components.Add(component); + + return (T)this; + } + + /// + /// Adds a separator component to this builder. + /// + /// The component to add. + /// The builder to chain calls with. + /// Thrown if there is insufficient slots to support the component. + public T AddSeparatorComponent + ( + DiscordSeparatorComponent component + ) + { + EnsureSufficientSpaceForComponent(component); + this.components.Add(component); + + return (T)this; + } + + /// + /// Adds a file component to this builder. + /// + /// The component to add. + /// The builder to chain calls with. + /// Thrown if there is insufficient slots to support the component. + public T AddFileComponent + ( + DiscordFileComponent component + ) + { + EnsureSufficientSpaceForComponent(component); + this.components.Add(component); + + return (T)this; + } + + + /// + /// Adds a container component to this builder. + /// + /// The component to add. + /// The builder to chain calls with. + /// Thrown if there is insufficient slots to support the component. + public T AddContainerComponent + ( + DiscordContainerComponent component + ) + { + EnsureSufficientSpaceForComponent(component); + this.components.Add(component); + + return (T)this; + } + + /// + /// Sets if the message should be TTS. + /// + /// If TTS should be set. + /// The current builder to be chained. + public T WithTTS(bool isTTS) + { + this.IsTTS = isTTS; + return (T)this; + } + + public T WithPoll(DiscordPollBuilder poll) + { + ThrowIfV2Enabled(); + this.Poll = poll; + return (T)this; + } + + /// + /// Appends an embed to the current builder. + /// + /// The embed that should be appended. + /// The current builder to be chained. + public T AddEmbed(DiscordEmbed embed) + { + ThrowIfV2Enabled(); + if (embed is null) + { + return (T)this; //Providing null embeds will produce a 400 response from Discord.// + } + + this.embeds.Add(embed); + return (T)this; + } + + /// + /// Appends several embeds to the current builder. + /// + /// The embeds that should be appended. + /// The current builder to be chained. + public T AddEmbeds(IEnumerable embeds) + { + ThrowIfV2Enabled(); + this.embeds.AddRange(embeds); + return (T)this; + } + + /// + /// Clears the embeds on the current builder. + /// + /// The current builder for chaining. + public T ClearEmbeds() + { + this.embeds.Clear(); + return (T)this; + } + + /// + /// Removes the embed at the specified index. + /// + /// The current builder for chaining. + public T RemoveEmbedAt(int index) + { + this.embeds.RemoveAt(index); + return (T)this; + } + + /// + /// Removes the specified range of embeds. + /// + /// The starting index of the embeds to remove. + /// The amount of embeds to remove. + /// The current builder for chaining. + public T RemoveEmbeds(int index, int count) + { + this.embeds.RemoveRange(index, count); + return (T)this; + } + + /// + /// Sets if the message has files to be sent. + /// + /// The fileName that the file should be sent as. + /// The Stream to the file. + /// Tells the API Client to reset the stream position to what it was after the file is sent. + /// The current builder to be chained. + public T AddFile(string fileName, Stream stream, bool resetStreamPosition = false) => AddFile(fileName, stream, resetStreamPosition ? AddFileOptions.ResetStream : AddFileOptions.None); + + /// + /// Sets if the message has files to be sent. + /// + /// The Stream to the file. + /// Tells the API Client to reset the stream position to what it was after the file is sent. + /// The current builder to be chained. + public T AddFile(FileStream stream, bool resetStreamPosition = false) => AddFile(stream, resetStreamPosition ? AddFileOptions.ResetStream : AddFileOptions.None); + + /// + /// Sets if the message has files to be sent. + /// + /// The Files that should be sent. + /// Tells the API Client to reset the stream position to what it was after the file is sent. + /// The current builder to be chained. + public T AddFiles(IDictionary files, bool resetStreamPosition = false) => AddFiles(files, resetStreamPosition ? AddFileOptions.ResetStream : AddFileOptions.None); + + /// + /// Attaches a file to this message. + /// + /// Name of the file to attach. + /// Stream containing said file's contents. + /// Additional flags for the handling of the file stream. + /// The current builder to be chained. + public T AddFile(string fileName, Stream stream, AddFileOptions fileOptions) + { + if (this.Files.Count >= 10) + { + throw new ArgumentException("Cannot send more than 10 files with a single message."); + } + + if (this.files.Any(x => x.FileName == fileName)) + { + throw new ArgumentException("A file with that filename already exists"); + } + + stream = ResolveStream(stream, fileOptions); + long? resetPosition = fileOptions.HasFlag(AddFileOptions.ResetStream) ? stream.Position : null; + this.files.Add(new DiscordMessageFile(fileName, stream, resetPosition, fileOptions: fileOptions)); + + return (T)this; + } + + /// + /// Attaches a file to this message. + /// + /// FileStream pointing to the file to attach. + /// Additional flags for the handling of the file stream. + /// The current builder to be chained. + public T AddFile(FileStream stream, AddFileOptions fileOptions) => AddFile(stream.Name, stream, fileOptions); + + /// + /// Attaches multiple files to this message. + /// + /// Dictionary of files to add, where is a file name and is a stream containing the file's contents. + /// Additional flags for the handling of the file streams. + /// The current builder to be chained. + public T AddFiles(IDictionary files, AddFileOptions fileOptions) + { + if (this.Files.Count + files.Count > 10) + { + throw new ArgumentException("Cannot send more than 10 files with a single message."); + } + + foreach (KeyValuePair file in files) + { + if (this.files.Any(x => x.FileName == file.Key)) + { + throw new ArgumentException("A File with that filename already exists"); + } + + Stream stream = ResolveStream(file.Value, fileOptions); + long? resetPosition = fileOptions.HasFlag(AddFileOptions.ResetStream) ? stream.Position : null; + this.files.Add(new DiscordMessageFile(file.Key, stream, resetPosition, fileOptions: fileOptions)); + } + + return (T)this; + } + + public T AddFiles(IEnumerable files) + { + this.files.AddRange(files); + return (T)this; + } + + /// + /// Adds the mention to the mentions to parse, etc. with the interaction response. + /// + /// Mention to add. + public T AddMention(IMention mention) + { + this.mentions.Add(mention); + return (T)this; + } + + /// + /// Adds the mentions to the mentions to parse, etc. with the interaction response. + /// + /// Mentions to add. + public T AddMentions(IEnumerable mentions) + { + this.mentions.AddRange(mentions); + return (T)this; + } + + /// + /// Clears all message components on this builder. + /// + public virtual void ClearComponents() + => this.components.Clear(); + + /// + /// Allows for clearing the Message Builder so that it can be used again to send a new message. + /// + public virtual void Clear() + { + this.Content = ""; + this.embeds.Clear(); + this.IsTTS = false; + this.mentions.Clear(); + this.files.Clear(); + this.components.Clear(); + this.Flags = default; + } + + /// + public void Dispose() + { + // We don't bother to fully implement the dispose pattern + // since deriving from this type outside this assembly is unusual. + + foreach (DiscordMessageFile file in this.files) + { + if (file.FileOptions.HasFlag(AddFileOptions.CloseStream)) + { + if (file.Stream is RequestStreamWrapper wrapper) + { + wrapper.UnderlyingStream.Dispose(); + } + else + { + file.Stream.Dispose(); + } + } + } + + GC.SuppressFinalize(this); + } + + /// + public async ValueTask DisposeAsync() + { + foreach (DiscordMessageFile file in this.files) + { + if (file.FileOptions.HasFlag(AddFileOptions.CloseStream)) + { + if (file.Stream is RequestStreamWrapper wrapper) + { + await wrapper.UnderlyingStream.DisposeAsync(); + } + else + { + await file.Stream.DisposeAsync(); + } + } + } + + GC.SuppressFinalize(this); + } + + /// + /// Helper method to reset stream positions used several times by the API client. + /// + internal void ResetFileStreamPositions() + { + foreach (DiscordMessageFile file in this.files) + { + if (file.ResetPositionTo is long pos) + { + file.Stream.Seek(pos, SeekOrigin.Begin); + } + } + } + + /// + /// Helper method to resolve stream copies depending on the file mode parameter. + /// + private static Stream ResolveStream(Stream stream, AddFileOptions fileOptions) + { + if (!fileOptions.HasFlag(AddFileOptions.CopyStream)) + { + return new RequestStreamWrapper(stream); + } + + Stream originalStream = stream; + MemoryStream newStream = new(); + originalStream.CopyTo(newStream); + newStream.Position = 0; + if (fileOptions.HasFlag(AddFileOptions.CloseStream)) + { + originalStream.Dispose(); + } + + return newStream; + } + + [StackTraceHidden] + [DebuggerStepThrough] + private void EnsureSufficientSpaceForComponent + ( + DiscordComponent component + ) + { + const int CV2_MAX_TOTAL_COMPONENTS = 40; + int maxTopComponents = this.Flags.HasFlag(DiscordMessageFlags.IsComponentsV2) ? CV2_MAX_TOTAL_COMPONENTS : 5; + int maxAllComponents = this.Flags.HasFlag(DiscordMessageFlags.IsComponentsV2) ? CV2_MAX_TOTAL_COMPONENTS : 25; + + int allComponentCount = this.Components.Sum + ( + c => + { + return c switch + { + DiscordActionRowComponent arc => 1 + arc.Components.Count, + DiscordSectionComponent section => 2 + section.Components.Count, + DiscordContainerComponent container => 1 + container.Components.Sum + ( + nc => nc switch + { + DiscordActionRowComponent narc => narc.Components.Count + 1, + DiscordSectionComponent narc => narc.Components.Count + 2, + _ => 1, + } + ), + _ => 1 + }; + } + ); + + int requiredSpaceForComponent = component switch + { + DiscordActionRowComponent arc => arc.Components.Count + 1, // Action row + components + DiscordSectionComponent section => section.Components.Count + 2, // Section + Accessory + DiscordContainerComponent container => container.Components.Sum + ( + nc => nc switch + { + DiscordActionRowComponent narc => narc.Components.Count + 1, + DiscordSectionComponent narc => narc.Components.Count + 2, + _ => 1, + } + ), + _ => 1, + }; + + if (this.Components.Count + 1 > maxTopComponents) + { + throw new InvalidOperationException($"Too many top-level components! Maximum allowed is {maxTopComponents}."); + } + + if (allComponentCount + requiredSpaceForComponent > maxAllComponents) + { + throw new InvalidOperationException($"Too many components! Maximum allowed is {maxAllComponents}; {component.GetType().Name} requires {requiredSpaceForComponent} slots."); + } + } + + private void SetIfV2Disabled(ref TField field, TField value, string fieldName) + { + if (this.Flags.HasFlag(DiscordMessageFlags.IsComponentsV2)) + { + throw new ArgumentException("This field cannot be set when V2 components is enabled.", fieldName); + } + + field = value; + } + + [StackTraceHidden] + [DebuggerStepThrough] + private void ThrowIfV2Enabled([CallerMemberName] string caller = "") + { + if (this.Flags.HasFlag(DiscordMessageFlags.IsComponentsV2)) + { + throw new InvalidOperationException($"{caller} cannot be called when V2 components is enabled."); + } + } + + IDiscordMessageBuilder IDiscordMessageBuilder.EnableV2Components() => this.EnableV2Components(); + IDiscordMessageBuilder IDiscordMessageBuilder.DisableV2Components() => this.DisableV2Components(); + IDiscordMessageBuilder IDiscordMessageBuilder.AddActionRowComponent(DiscordActionRowComponent component) => this.AddActionRowComponent(component); + IDiscordMessageBuilder IDiscordMessageBuilder.AddActionRowComponent(DiscordSelectComponent selectMenu) => this.AddActionRowComponent(selectMenu); + IDiscordMessageBuilder IDiscordMessageBuilder.AddActionRowComponent(params IEnumerable buttons) => this.AddActionRowComponent(buttons); + IDiscordMessageBuilder IDiscordMessageBuilder.AddMediaGalleryComponent(params IEnumerable galleryItems) => this.AddMediaGalleryComponent(galleryItems); + IDiscordMessageBuilder IDiscordMessageBuilder.AddSectionComponent(DiscordSectionComponent section) => this.AddSectionComponent(section); + IDiscordMessageBuilder IDiscordMessageBuilder.AddTextDisplayComponent(DiscordTextDisplayComponent component) => this.AddTextDisplayComponent(component); + IDiscordMessageBuilder IDiscordMessageBuilder.AddTextDisplayComponent(string content) => this.AddTextDisplayComponent(content); + IDiscordMessageBuilder IDiscordMessageBuilder.AddTextInputComponent(DiscordTextInputComponent component) => this.AddTextInputComponent(component); + IDiscordMessageBuilder IDiscordMessageBuilder.AddSeparatorComponent(DiscordSeparatorComponent component) => this.AddSeparatorComponent(component); + IDiscordMessageBuilder IDiscordMessageBuilder.AddFileComponent(DiscordFileComponent component) => this.AddFileComponent(component); + IDiscordMessageBuilder IDiscordMessageBuilder.AddContainerComponent(DiscordContainerComponent component) => this.AddContainerComponent(component); + IDiscordMessageBuilder IDiscordMessageBuilder.SuppressNotifications() => SuppressNotifications(); + IDiscordMessageBuilder IDiscordMessageBuilder.WithContent(string content) => WithContent(content); + IDiscordMessageBuilder IDiscordMessageBuilder.WithTTS(bool isTTS) => WithTTS(isTTS); + IDiscordMessageBuilder IDiscordMessageBuilder.AddEmbed(DiscordEmbed embed) => AddEmbed(embed); + IDiscordMessageBuilder IDiscordMessageBuilder.AddEmbeds(IEnumerable embeds) => AddEmbeds(embeds); + IDiscordMessageBuilder IDiscordMessageBuilder.AddFile(string fileName, Stream stream, bool resetStream) => AddFile(fileName, stream, resetStream); + IDiscordMessageBuilder IDiscordMessageBuilder.AddFile(FileStream stream, bool resetStream) => AddFile(stream, resetStream); + IDiscordMessageBuilder IDiscordMessageBuilder.AddFiles(IDictionary files, bool resetStreams) => AddFiles(files, resetStreams); + IDiscordMessageBuilder IDiscordMessageBuilder.AddFiles(IEnumerable files) => AddFiles(files); + IDiscordMessageBuilder IDiscordMessageBuilder.AddFile(string fileName, Stream stream, AddFileOptions fileOptions) => AddFile(fileName, stream, fileOptions); + IDiscordMessageBuilder IDiscordMessageBuilder.AddFile(FileStream stream, AddFileOptions fileOptions) => AddFile(stream, fileOptions); + IDiscordMessageBuilder IDiscordMessageBuilder.AddFiles(IDictionary files, AddFileOptions fileOptions) => AddFiles(files, fileOptions); + IDiscordMessageBuilder IDiscordMessageBuilder.AddMention(IMention mention) => AddMention(mention); + IDiscordMessageBuilder IDiscordMessageBuilder.AddMentions(IEnumerable mentions) => AddMentions(mentions); +} + +/// +/// Additional flags for files added to a message builder. +/// +[Flags] +public enum AddFileOptions +{ + /// + /// No additional behavior. The stream will read to completion and is left at that position after sending. + /// + None = 0, + + /// + /// Resets the stream to its original position after sending. + /// + ResetStream = 0x1, + + /// + /// Closes the stream upon disposal of the message builder. + /// + /// + /// Streams will not be disposed upon sending. Disposal of the message builder is necessary. + /// + CloseStream = 0x2, + + /// + /// Immediately reads the stream to completion and copies its contents to an in-memory stream. + /// + /// + /// + /// Note that this incurs an additional memory overhead and may perform synchronous I/O and should only be used if the source stream cannot be kept open any longer. + /// + /// + /// If specified together with , the stream will closed immediately after the copy. + /// + /// + CopyStream = 0x4, +} + +/// +/// Base interface for any discord message builder. +/// +public interface IDiscordMessageBuilder : IDisposable, IAsyncDisposable +{ + /// + /// Getter / setter for message content. + /// + public string? Content { get; set; } + + /// + /// Whether this message will play as a text-to-speech message. + /// + public bool IsTTS { get; set; } + + /// + /// Gets or sets a poll for this message. + /// + public DiscordPollBuilder? Poll { get; set; } + + /// + /// All embeds on this message. + /// + public IReadOnlyList Embeds { get; } + + /// + /// All files on this message. + /// + public IReadOnlyList Files { get; } + + /// + /// All components on this message. + /// + public IReadOnlyList Components { get; } + + /// + /// All allowed mentions on this message. + /// + public IReadOnlyList Mentions { get; } + + public DiscordMessageFlags Flags { get; } + + /// + /// Adds content to this message + /// + /// Message content to use + /// + public IDiscordMessageBuilder WithContent(string content); + + /// + /// Enables support for V2 components; messages with the V2 flag cannot be downgraded. + /// + /// The builder to chain calls with. + public IDiscordMessageBuilder EnableV2Components(); + + /// + /// Disables V2 components IF this builder does not currently contain illegal components. + /// + /// The builder to chain calls with. + /// The builder contains V2 components and cannot be downgraded. + /// This method only disables the V2 components flag; the message originally associated with this builder cannot be downgraded, and this method only exists for convenience. + public IDiscordMessageBuilder DisableV2Components(); + + /// + /// Adds a raw action row. + /// + /// The select menu to add, if possible. + /// The builder to chain calls with. + /// Thrown if there is insufficient slots to support the component. + public IDiscordMessageBuilder AddActionRowComponent(DiscordActionRowComponent component); + + /// + /// Adds a new action row with the given component. + /// + /// The select menu to add, if possible. + /// The builder to chain calls with. + /// Thrown if there is insufficient slots to support the component. + public IDiscordMessageBuilder AddActionRowComponent(DiscordSelectComponent selectMenu); + + /// + /// Adds buttons to the builder. + /// + /// The buttons to add to the message. They will automatically be chunked into separate action rows as necessary. + /// The builder to chain calls with. + /// Thrown if there is insufficient slots to support the component. + public IDiscordMessageBuilder AddActionRowComponent(params IEnumerable buttons); + + /// + /// Adds a media gallery to this builder. + /// + /// The items to add. + /// The builder to chain calls with. + /// Thrown if there is insufficient slots to support the component. + public IDiscordMessageBuilder AddMediaGalleryComponent(params IEnumerable galleryItems); + + /// + /// Adds a section component to the builder. + /// + /// The builder to chain calls with. + /// Thrown if there is insufficient slots to support the component. + public IDiscordMessageBuilder AddSectionComponent(DiscordSectionComponent section); + + /// + /// Adds a text display to this builder. + /// + /// The builder to chain calls with. + /// Thrown if there is insufficient slots to support the component. + public IDiscordMessageBuilder AddTextDisplayComponent(DiscordTextDisplayComponent component); + + /// + /// Adds a text display to this builder. + /// + /// + /// The builder to chain calls with. + /// Thrown if there is insufficient slots to support the component. + public IDiscordMessageBuilder AddTextDisplayComponent(string content); + + /// + /// Adds a text input to this builder. + /// + /// The component to add. + /// The builder to chain calls with. + /// Thrown if there is insufficient slots to support the component. + public IDiscordMessageBuilder AddTextInputComponent(DiscordTextInputComponent component); + + /// + /// Adds a separator component to this builder. + /// + /// The component to add. + /// The builder to chain calls with. + /// Thrown if there is insufficient slots to support the component. + public IDiscordMessageBuilder AddSeparatorComponent(DiscordSeparatorComponent component); + + /// + /// Adds a file component to this builder. + /// + /// The component to add. + /// The builder to chain calls with. + /// Thrown if there is insufficient slots to support the component. + public IDiscordMessageBuilder AddFileComponent(DiscordFileComponent component); + + /// + /// Adds a container component to this builder. + /// + /// The component to add. + /// The builder to chain calls with. + /// Thrown if there is insufficient slots to support the component. + public IDiscordMessageBuilder AddContainerComponent(DiscordContainerComponent component); + + /// + /// Sets whether this message should play as a text-to-speech message. + /// + /// + /// + public IDiscordMessageBuilder WithTTS(bool isTTS); + + /// + /// Adds an embed to this message. + /// + /// Embed to add. + /// + public IDiscordMessageBuilder AddEmbed(DiscordEmbed embed); + + /// + /// Adds multiple embeds to this message. + /// + /// Collection of embeds to add. + /// + public IDiscordMessageBuilder AddEmbeds(IEnumerable embeds); + + /// + /// Attaches a file to this message. + /// + /// Name of the file to attach. + /// Stream containing said file's contents. + /// Whether to reset the stream to position 0 after sending. + /// + public IDiscordMessageBuilder AddFile(string fileName, Stream stream, bool resetStream = false); + + /// + /// Attaches a file to this message. + /// + /// FileStream pointing to the file to attach. + /// Whether to reset the stream position to 0 after sending. + /// + public IDiscordMessageBuilder AddFile(FileStream stream, bool resetStream = false); + + /// + /// Attaches multiple files to this message. + /// + /// Dictionary of files to add, where is a file name and is a stream containing the file's contents. + /// Whether to reset all stream positions to 0 after sending. + /// + public IDiscordMessageBuilder AddFiles(IDictionary files, bool resetStreams = false); + + /// + /// Attaches a file to this message. + /// + /// Name of the file to attach. + /// Stream containing said file's contents. + /// Additional flags for the handling of the file stream. + /// + public IDiscordMessageBuilder AddFile(string fileName, Stream stream, AddFileOptions fileOptions); + + /// + /// Attaches a file to this message. + /// + /// FileStream pointing to the file to attach. + /// Additional flags for the handling of the file stream. + /// + public IDiscordMessageBuilder AddFile(FileStream stream, AddFileOptions fileOptions); + + /// + /// Attaches multiple files to this message. + /// + /// Dictionary of files to add, where is a file name and is a stream containing the file's contents. + /// Additional flags for the handling of the file streams. + /// + public IDiscordMessageBuilder AddFiles(IDictionary files, AddFileOptions fileOptions); + + /// + /// Attaches previously used files to this file stream. + /// + /// Previously attached files to reattach + /// + public IDiscordMessageBuilder AddFiles(IEnumerable files); + + /// + /// Adds an allowed mention to this message. + /// + /// Mention to allow in this message. + /// + public IDiscordMessageBuilder AddMention(IMention mention); + + /// + /// Adds multiple allowed mentions to this message. + /// + /// Collection of mentions to allow in this message. + /// + public IDiscordMessageBuilder AddMentions(IEnumerable mentions); + + /// + /// Applies to the message. + /// + /// + /// + /// As per , this does not change the message's allowed mentions + /// (controlled by ), but instead prevents a mention from triggering a push notification. + /// + public IDiscordMessageBuilder SuppressNotifications(); + + /// + /// Clears all components attached to this builder. + /// + public void ClearComponents(); + + /// + /// Clears this builder. + /// + public void Clear(); +} + +/* +* Zǎoshang hǎo zhōngguó xiànzài wǒ yǒu BING CHILLING 🥶🍦 +* wǒ hěn xǐhuān BING CHILLING 🥶🍦 +*/ diff --git a/DSharpPlus/Entities/Channel/DiscordAutoArchiveDuration.cs b/DSharpPlus/Entities/Channel/DiscordAutoArchiveDuration.cs index 714d742c51..d571151f9a 100644 --- a/DSharpPlus/Entities/Channel/DiscordAutoArchiveDuration.cs +++ b/DSharpPlus/Entities/Channel/DiscordAutoArchiveDuration.cs @@ -1,28 +1,28 @@ -namespace DSharpPlus.Entities; - - -/// -/// Represents the duration in minutes to automatically archive a thread after recent activity. -/// -public enum DiscordAutoArchiveDuration : int -{ - /// - /// Thread will auto-archive after one hour of inactivity. - /// - Hour = 60, - - /// - /// Thread will auto-archive after one day of inactivity. - /// - Day = 1440, - - /// - /// Thread will auto-archive after three days of inactivity. - /// - ThreeDays = 4320, - - /// - /// Thread will auto-archive after one week of inactivity. - /// - Week = 10080 -} +namespace DSharpPlus.Entities; + + +/// +/// Represents the duration in minutes to automatically archive a thread after recent activity. +/// +public enum DiscordAutoArchiveDuration : int +{ + /// + /// Thread will auto-archive after one hour of inactivity. + /// + Hour = 60, + + /// + /// Thread will auto-archive after one day of inactivity. + /// + Day = 1440, + + /// + /// Thread will auto-archive after three days of inactivity. + /// + ThreeDays = 4320, + + /// + /// Thread will auto-archive after one week of inactivity. + /// + Week = 10080 +} diff --git a/DSharpPlus/Entities/Channel/DiscordChannel.cs b/DSharpPlus/Entities/Channel/DiscordChannel.cs index 3a8b32a5c6..a148196c77 100644 --- a/DSharpPlus/Entities/Channel/DiscordChannel.cs +++ b/DSharpPlus/Entities/Channel/DiscordChannel.cs @@ -1,1287 +1,1287 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Threading; -using System.Threading.Tasks; -using DSharpPlus.Exceptions; -using DSharpPlus.Net.Abstractions; -using DSharpPlus.Net.Models; -using DSharpPlus.Net.Serialization; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents a discord channel. -/// -[JsonConverter(typeof(DiscordForumChannelJsonConverter))] -public class DiscordChannel : SnowflakeObject, IEquatable -{ - /// - /// Gets ID of the guild to which this channel belongs. - /// - [JsonProperty("guild_id", NullValueHandling = NullValueHandling.Ignore)] - public ulong? GuildId { get; internal set; } - - /// - /// Gets ID of the category that contains this channel. For threads, gets the ID of the channel this thread was created in. - /// - [JsonProperty("parent_id", NullValueHandling = NullValueHandling.Include)] - public ulong? ParentId { get; internal set; } - - /// - /// Gets the category that contains this channel. For threads, gets the channel this thread was created in. - /// - [JsonIgnore] - public DiscordChannel Parent - => this.ParentId.HasValue ? this.Guild.GetChannel(this.ParentId.Value) : null; - - /// - /// Gets the name of this channel. - /// - [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] - public string Name { get; internal set; } - - /// - /// Gets the type of this channel. - /// - [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] - public virtual DiscordChannelType Type { get; internal set; } - - /// - /// Gets the position of this channel. - /// - [JsonProperty("position", NullValueHandling = NullValueHandling.Ignore)] - public int Position { get; internal set; } - - /// - /// Gets whether this channel is a DM channel. - /// - [JsonIgnore] - public bool IsPrivate - => this.Type is DiscordChannelType.Private or DiscordChannelType.Group; - - /// - /// Gets whether this channel is a channel category. - /// - [JsonIgnore] - public bool IsCategory - => this.Type == DiscordChannelType.Category; - - /// - /// Gets whether this channel is a thread. - /// - [JsonIgnore] - public bool IsThread - => this.Type is DiscordChannelType.PrivateThread or DiscordChannelType.PublicThread or DiscordChannelType.NewsThread; - - /// - /// Gets the guild to which this channel belongs. - /// - [JsonIgnore] - public DiscordGuild Guild - => this.GuildId.HasValue && this.Discord.Guilds.TryGetValue(this.GuildId.Value, out DiscordGuild? guild) ? guild : null; - - /// - /// Gets a collection of permission overwrites for this channel. - /// - [JsonIgnore] - public IReadOnlyList PermissionOverwrites - => this.permissionOverwrites; - - [JsonProperty("permission_overwrites", NullValueHandling = NullValueHandling.Ignore)] - internal List permissionOverwrites = []; - - /// - /// Gets the channel's topic. This is applicable to text channels only. - /// - [JsonProperty("topic", NullValueHandling = NullValueHandling.Ignore)] - public string Topic { get; internal set; } - - /// - /// Gets the ID of the last message sent in this channel. This is applicable to text channels only. - /// - /// For forum posts, this ID may point to an invalid mesage (e.g. the OP deleted the initial forum message). - [JsonProperty("last_message_id", NullValueHandling = NullValueHandling.Ignore)] - public ulong? LastMessageId { get; internal set; } - - /// - /// Gets this channel's bitrate. This is applicable to voice channels only. - /// - [JsonProperty("bitrate", NullValueHandling = NullValueHandling.Ignore)] - public int? Bitrate { get; internal set; } - - /// - /// Gets this channel's user limit. This is applicable to voice channels only. - /// - [JsonProperty("user_limit", NullValueHandling = NullValueHandling.Ignore)] - public int? UserLimit { get; internal set; } - - /// - /// Gets the slow mode delay configured for this channel.
- /// All bots, as well as users with - /// or permissions in the channel are exempt from slow mode. - ///
- [JsonProperty("rate_limit_per_user")] - public int? PerUserRateLimit { get; internal set; } - - /// - /// Gets this channel's video quality mode. This is applicable to voice channels only. - /// - [JsonProperty("video_quality_mode", NullValueHandling = NullValueHandling.Ignore)] - public DiscordVideoQualityMode? QualityMode { get; internal set; } - - /// - /// Gets when the last pinned message was pinned. - /// - [JsonProperty("last_pin_timestamp", NullValueHandling = NullValueHandling.Ignore)] - public DateTimeOffset? LastPinTimestamp { get; internal set; } - - /// - /// Gets this channel's mention string. - /// - [JsonIgnore] - public string Mention - => Formatter.Mention(this); - - /// - /// Gets this channel's children. This applies only to channel categories. - /// - [JsonIgnore] - public IReadOnlyList Children - { - get - { - return !this.IsCategory - ? throw new ArgumentException("Only channel categories contain children.") - : this.Guild.channels.Values.Where(e => e.ParentId == this.Id).ToList(); - } - } - - /// - /// Gets this channel's threads. This applies only to text and news channels. - /// - [JsonIgnore] - public IReadOnlyList Threads - { - get - { - return this.Type is not (DiscordChannelType.Text or DiscordChannelType.News or DiscordChannelType.GuildForum) - ? throw new ArgumentException("Only text channels can have threads.") - : this.Guild.threads.Values.Where(e => e.ParentId == this.Id).ToArray(); - } - } - - /// - /// Gets the list of members currently in the channel (if voice channel), or members who can see the channel (otherwise). - /// - [JsonIgnore] - public virtual IReadOnlyList Users - { - get - { - return this.Guild is null - ? throw new InvalidOperationException("Cannot query users outside of guild channels.") - : (IReadOnlyList)(this.Type is DiscordChannelType.Voice or DiscordChannelType.Stage - ? this.Guild.Members.Values.Where(x => x.VoiceState?.ChannelId == this.Id).ToList() - : this.Guild.Members.Values.Where(x => PermissionsFor(x).HasPermission(DiscordPermission.ViewChannel)).ToList()); - } - } - - /// - /// Gets whether this channel is an NSFW channel. - /// - [JsonProperty("nsfw")] - public bool IsNSFW { get; internal set; } - - [JsonProperty("rtc_region", NullValueHandling = NullValueHandling.Ignore)] - internal string RtcRegionId { get; set; } - - /// - /// Gets this channel's region override (if voice channel). - /// - [JsonIgnore] - public DiscordVoiceRegion RtcRegion - => this.RtcRegionId != null ? this.Discord.VoiceRegions[this.RtcRegionId] : null; - - /// - /// Gets the permissions of the user who invoked the command in this channel. - /// Only sent on the resolved channels of interaction responses for application commands. - /// - [JsonProperty("permissions")] - public DiscordPermissions? UserPermissions { get; internal set; } - - internal DiscordChannel() { } - - #region Methods - - /// - /// Sends a message to this channel. - /// - /// Content of the message to send. - /// The sent message. - /// Thrown when the client does not have the - /// permission if TTS is 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 async Task SendMessageAsync(string content) => !Utilities.IsTextableChannel(this) - ? throw new ArgumentException($"{this.Type} channels do not support sending text messages.") - : await this.Discord.ApiClient.CreateMessageAsync(this.Id, content, null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false, suppressNotifications: false); - - /// - /// Sends a message to this channel. - /// - /// Embed to attach to the message. - /// The sent message. - /// Thrown when the client does not have the - /// permission 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 async Task SendMessageAsync(DiscordEmbed embed) => !Utilities.IsTextableChannel(this) - ? throw new ArgumentException($"{this.Type} channels do not support sending text messages.") - : await this.Discord.ApiClient.CreateMessageAsync(this.Id, null, embed != null ? new[] { embed } : null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false, suppressNotifications: false); - - /// - /// Sends a message to this channel. - /// - /// Embed to attach to the message. - /// Content of the message to send. - /// The sent message. - /// Thrown when the client does not have the - /// permission if TTS is 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 async Task SendMessageAsync(string content, DiscordEmbed embed) => !Utilities.IsTextableChannel(this) - ? throw new ArgumentException($"{this.Type} channels do not support sending text messages.") - : await this.Discord.ApiClient.CreateMessageAsync(this.Id, content, embed != null ? new[] { embed } : null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false, suppressNotifications: false); - - /// - /// Sends a message to this channel. - /// - /// The builder with all the items to send. - /// The sent message. - /// Thrown when the client does not have the - /// permission 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 async Task SendMessageAsync(DiscordMessageBuilder builder) => !Utilities.IsTextableChannel(this) - ? throw new ArgumentException($"{this.Type} channels do not support sending text messages.") - : await this.Discord.ApiClient.CreateMessageAsync(this.Id, builder); - - /// - /// Sends a message to this channel. - /// - /// The builder with all the items to send. - /// The sent message. - /// Thrown when the client does not have the - /// permission 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 async Task SendMessageAsync(Action action) - { - if (!Utilities.IsTextableChannel(this)) - { - throw new ArgumentException($"{this.Type} channels do not support sending text messages."); - } - - DiscordMessageBuilder builder = new(); - action(builder); - - return await this.Discord.ApiClient.CreateMessageAsync(this.Id, builder); - } - - /// - /// Creates an event bound to this channel. - /// - /// The name of the event, up to 100 characters. - /// The description of this event, up to 1000 characters. - /// The privacy level. Currently only is supported - /// When this event starts. - /// When this event ends. External events require an end time. - /// The created event. - /// - public Task CreateGuildEventAsync(string name, string description, DiscordScheduledGuildEventPrivacyLevel privacyLevel, DateTimeOffset start, DateTimeOffset? end) - => this.Type is not (DiscordChannelType.Voice or DiscordChannelType.Stage) ? throw new InvalidOperationException("Events can only be created on voice an stage chnanels") : - this.Guild.CreateEventAsync(name, description, this.Id, this.Type is DiscordChannelType.Stage ? DiscordScheduledGuildEventType.StageInstance : DiscordScheduledGuildEventType.VoiceChannel, privacyLevel, start, end); - - // Please send memes to Naamloos#2887 at discord <3 thank you - - /// - /// Deletes a guild channel - /// - /// Reason for audit logs. - /// - /// Thrown when the client does not have the permission. - /// Thrown when the channel does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public async Task DeleteAsync(string reason = null) - => await this.Discord.ApiClient.DeleteChannelAsync(this.Id, reason); - - /// - /// Clones this channel. This operation will create a channel with identical settings to this one. Note that this will not copy messages. - /// - /// Reason for audit logs. - /// Newly-created channel. - /// Thrown when the client does not have the permission. - /// Thrown when the channel does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public async Task CloneAsync(string reason = null) - { - if (this.Guild == null) - { - throw new InvalidOperationException("Non-guild channels cannot be cloned."); - } - - List ovrs = [.. this.permissionOverwrites.Select(DiscordOverwriteBuilder.From)]; - - int? bitrate = this.Bitrate; - int? userLimit = this.UserLimit; - Optional perUserRateLimit = this.PerUserRateLimit; - - if (this.Type != DiscordChannelType.Voice) - { - bitrate = null; - userLimit = null; - } - - if (this.Type != DiscordChannelType.Text) - { - perUserRateLimit = Optional.FromNoValue(); - } - - return await this.Guild.CreateChannelAsync(this.Name, this.Type, this.Parent, this.Topic, bitrate, userLimit, ovrs, this.IsNSFW, perUserRateLimit, this.QualityMode, this.Position, reason); - } - - /// - /// Returns a specific message - /// - /// The ID of the message - /// Whether to always make a REST request. - /// - /// Thrown when the client does not have the permission. - /// Thrown when the channel does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public async Task GetMessageAsync(ulong id, bool skipCache = false) => !skipCache - && this.Discord is DiscordClient dc - && dc.MessageCache != null - && dc.MessageCache.TryGet(id, out DiscordMessage? msg) - ? msg - : await this.Discord.ApiClient.GetMessageAsync(this.Id, id); - - /// - /// Modifies the current channel. - /// - /// Action to perform on this channel - /// - /// Thrown when the client does not have the permission. - /// Thrown when the channel does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public async Task ModifyAsync(Action action) - { - ChannelEditModel mdl = new(); - action(mdl); - await this.Discord.ApiClient.ModifyChannelAsync - ( - this.Id, - mdl.Name, - mdl.Position, - mdl.Topic, - mdl.Nsfw, - mdl.Parent.HasValue ? mdl.Parent.Value?.Id : default(Optional), - mdl.Bitrate, - mdl.Userlimit, - mdl.PerUserRateLimit, - mdl.RtcRegion.IfPresent(r => r?.Id), - mdl.QualityMode, - mdl.Type, - mdl.PermissionOverwrites, - mdl.Flags, - mdl.AvailableTags, - mdl.DefaultAutoArchiveDuration, - mdl.DefaultReaction, - mdl.DefaultThreadRateLimit, - mdl.DefaultSortOrder, - mdl.DefaultForumLayout, - mdl.AuditLogReason - ); - } - - /// - /// Updates the channel position - /// - /// Position the channel should be moved to. - /// Reason for audit logs. - /// Whether to sync channel permissions with the parent, if moving to a new category. - /// The new parent ID if the channel is to be moved to a new category. - /// - /// Thrown when the client does not have the permission. - /// Thrown when the channel does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public async Task ModifyPositionAsync(int position, string reason = null, bool? lockPermissions = null, ulong? parentId = null) - { - if (this.Guild is null) - { - throw new InvalidOperationException("Cannot modify order of non-guild channels."); - } - - DiscordChannel[] chns = [.. this.Guild.channels.Values.Where(xc => xc.Type == this.Type).OrderBy(xc => xc.Position)]; - RestGuildChannelReorderPayload[] pmds = new RestGuildChannelReorderPayload[chns.Length]; - for (int i = 0; i < chns.Length; i++) - { - pmds[i] = new() - { - ChannelId = chns[i].Id, - Position = chns[i].Id == this.Id ? position : chns[i].Position >= position ? chns[i].Position + 1 : chns[i].Position, - LockPermissions = chns[i].Id == this.Id ? lockPermissions : null, - ParentId = chns[i].Id == this.Id ? parentId : null - }; - } - - await this.Discord.ApiClient.ModifyGuildChannelPositionAsync(this.Guild.Id, pmds, reason); - } - - /// - /// Returns a list of messages before a certain message. This will execute one API request per 100 messages. - /// The amount of messages to fetch. - /// Message to fetch before from. - /// Cancels the enumeration before doing the next api request - /// - /// 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 IAsyncEnumerable GetMessagesBeforeAsync(ulong before, int limit = 100, CancellationToken cancellationToken = default) - => GetMessagesInternalAsync(limit, before, cancellationToken: cancellationToken); - - /// - /// Returns a list of messages after a certain message. This will execute one API request per 100 messages. - /// The amount of messages to fetch. - /// Message to fetch after from. - /// Cancels the enumeration before doing the next api request - /// - /// 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 IAsyncEnumerable GetMessagesAfterAsync(ulong after, int limit = 100, CancellationToken cancellationToken = default) - => GetMessagesInternalAsync(limit, after: after, cancellationToken: cancellationToken); - - /// - /// Returns a list of messages around a certain message. This will execute one API request per 100 messages. - /// The amount of messages to fetch. - /// Message to fetch around from. - /// Cancels the enumeration before doing the next api request - /// - /// 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 IAsyncEnumerable GetMessagesAroundAsync(ulong around, int limit = 100, CancellationToken cancellationToken = default) - => GetMessagesInternalAsync(limit, around: around, cancellationToken: cancellationToken); - - /// - /// Returns a list of messages from the last message in the channel. This will execute one API request per 100 messages. - /// The amount of messages to fetch. - /// Cancels the enumeration before doing the next api request - /// - /// 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 IAsyncEnumerable GetMessagesAsync(int limit = 100, CancellationToken cancellationToken = default) => - GetMessagesInternalAsync(limit, cancellationToken: cancellationToken); - - private async IAsyncEnumerable GetMessagesInternalAsync - ( - int limit = 100, - ulong? before = null, - ulong? after = null, - ulong? around = null, - [EnumeratorCancellation] - CancellationToken cancellationToken = default - ) - { - if (!Utilities.IsTextableChannel(this)) - { - throw new ArgumentException($"Cannot get the messages of a {this.Type} channel."); - } - - if (limit < 0) - { - throw new ArgumentException("Cannot get a negative number of messages."); - } - - if (limit == 0) - { - yield break; - } - - //return this.Discord.ApiClient.GetChannelMessagesAsync(this.Id, limit, before, after, around); - if (limit > 100 && around != null) - { - throw new InvalidOperationException("Cannot get more than 100 messages around the specified ID."); - } - - int remaining = limit; - ulong? last = null; - bool isbefore = before != null || (before is null && after is null && around is null); - - int lastCount; - do - { - if (cancellationToken.IsCancellationRequested) - { - yield break; - } - - int fetchSize = remaining > 100 ? 100 : remaining; - IReadOnlyList fetchedMessages = await this.Discord.ApiClient.GetChannelMessagesAsync(this.Id, fetchSize, isbefore ? last ?? before : null, !isbefore ? last ?? after : null, around); - - lastCount = fetchedMessages.Count; - remaining -= lastCount; - - //We sort the returned messages by ID so that they are in order in case Discord switches the order AGAIN. - DiscordMessage[] sortedMessageArray = [.. fetchedMessages]; - Array.Sort(sortedMessageArray, (x, y) => x.Id.CompareTo(y.Id)); - - if (!isbefore) - { - foreach (DiscordMessage msg in sortedMessageArray) - { - yield return msg; - } - - last = sortedMessageArray.LastOrDefault()?.Id; - } - else - { - for (int i = sortedMessageArray.Length - 1; i >= 0; i--) - { - yield return sortedMessageArray[i]; - } - - last = sortedMessageArray.FirstOrDefault()?.Id; - } - } - while (remaining > 0 && lastCount > 0 && lastCount == 100); - } - - /// - /// Gets the threads that are public and archived for this channel. - /// - /// A containing the threads for this query and if an other call will yield more threads. - /// Thrown when the client does not have the permission. - /// Thrown when the channel does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public async Task ListPublicArchivedThreadsAsync(DateTimeOffset? before = null, int limit = 0) => this.Type is not DiscordChannelType.Text and not DiscordChannelType.News and not DiscordChannelType.GuildForum - ? throw new InvalidOperationException() - : await this.Discord.ApiClient.ListPublicArchivedThreadsAsync(this.GuildId.Value, this.Id, before?.ToString("o"), limit); - - /// - /// Gets the threads that are private and archived for this channel. - /// - /// A containing the threads for this query and if an other call will yield more threads. - /// Thrown when the client does not have the - /// and the - /// permission. - /// Thrown when the channel does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public async Task ListPrivateArchivedThreadsAsync(DateTimeOffset? before = null, int limit = 0) => this.Type is not DiscordChannelType.Text and not DiscordChannelType.News and not DiscordChannelType.GuildForum - ? throw new InvalidOperationException() - : await this.Discord.ApiClient.ListPrivateArchivedThreadsAsync(this.GuildId.Value, this.Id, limit, before?.ToString("o")); - - /// - /// Gets the private and archived threads that the current member has joined in this channel. - /// - /// A containing the threads for this query and if an other call will yield more threads. - /// Thrown when the client does not have the - /// permission. - /// Thrown when the channel does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public async Task ListJoinedPrivateArchivedThreadsAsync(DateTimeOffset? before = null, int limit = 0) => this.Type is not DiscordChannelType.Text and not DiscordChannelType.News and not DiscordChannelType.GuildForum - ? throw new InvalidOperationException() - : await this.Discord.ApiClient.ListJoinedPrivateArchivedThreadsAsync(this.GuildId.Value, this.Id, limit, (ulong?)before?.ToUnixTimeSeconds()); - - /// - /// Deletes multiple messages if they are less than 14 days old. If they are older, none of the messages will be deleted and you will receive a error. - /// - /// A collection of messages to delete. - /// Reason for audit logs. - /// The number of deleted messages - /// One api call per 100 messages - public async Task DeleteMessagesAsync(IReadOnlyList messages, string? reason = null) - { - ArgumentNullException.ThrowIfNull(messages, nameof(messages)); - int count = messages.Count; - - if (count == 0) - { - throw new ArgumentException("You need to specify at least one message to delete."); - } - else if (count == 1) - { - await this.Discord.ApiClient.DeleteMessageAsync(this.Id, messages[0].Id, reason); - return 1; - } - - int deleteCount = 0; - - try - { - for (int i = 0; i < count; i += 100) - { - int takeCount = Math.Min(100, count - i); - DiscordMessage[] messageBatch = messages.Skip(i).Take(takeCount).ToArray(); - - foreach (DiscordMessage message in messageBatch) - { - if (message.ChannelId != this.Id) - { - throw new ArgumentException( - $"You cannot delete messages from channel {message.Channel.Name} through channel {this.Name}!"); - } - else if (message.Timestamp < DateTimeOffset.UtcNow.AddDays(-14)) - { - throw new ArgumentException("You can only delete messages that are less than 14 days old."); - } - } - - await this.Discord.ApiClient.DeleteMessagesAsync(this.Id, - messageBatch.Select(x => x.Id), reason); - deleteCount += takeCount; - } - } - catch (DiscordException e) - { - throw new BulkDeleteFailedException(deleteCount, e); - } - - return deleteCount; - } - - /// - /// Deletes multiple messages if they are less than 14 days old. Does one api request per 100 - /// - /// A collection of messages to delete. - /// Reason for audit logs. - /// The number of deleted messages - /// Exception which contains the exception which was thrown and the count of messages which were deleted successfully - /// One api call per 100 messages - public async Task DeleteMessagesAsync(IAsyncEnumerable messages, string? reason = null) - { - List list = new(100); - int count = 0; - try - { - await foreach (DiscordMessage message in messages) - { - list.Add(message); - - if (list.Count != 100) - { - continue; - } - - await DeleteMessagesAsync(list, reason); - list.Clear(); - count += 100; - } - - if (list.Count > 0) - { - await DeleteMessagesAsync(list, reason); - count += list.Count; - } - } - catch (BulkDeleteFailedException e) - { - throw new BulkDeleteFailedException(count + e.MessagesDeleted, e.InnerException); - } - catch (DiscordException e) - { - throw new BulkDeleteFailedException(count, e); - } - - return count; - } - - /// - /// Deletes a message - /// - /// The message to be deleted. - /// Reason for audit logs. - /// - /// Thrown when the client does not have the - /// permission. - /// Thrown when the channel does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public async Task DeleteMessageAsync(DiscordMessage message, string reason = null) - => await this.Discord.ApiClient.DeleteMessageAsync(this.Id, message.Id, reason); - - /// - /// Returns a list of invite objects - /// - /// - /// Thrown when the client does not have the - /// permission. - /// Thrown when the channel does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public async Task> GetInvitesAsync() => this.Guild == null - ? throw new ArgumentException("Cannot get the invites of a channel that does not belong to a guild.") - : await this.Discord.ApiClient.GetChannelInvitesAsync(this.Id); - - /// - /// Create a new invite object - /// - /// Duration of invite in seconds before expiry, or 0 for never. Defaults to 86400. - /// Max number of uses or 0 for unlimited. Defaults to 0 - /// Whether this invite only grants temporary membership. Defaults to false. - /// If true, don't try to reuse a similar invite (useful for creating many unique one time use invites) - /// Reason for audit logs. - /// The target type of the invite, for stream and embedded application invites. - /// The ID of the target user. - /// The ID of the target application. - /// - /// Thrown when the client does not have the - /// permission. - /// Thrown when the channel does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public async Task CreateInviteAsync(int max_age = 86400, int max_uses = 0, bool temporary = false, bool unique = false, string reason = null, DiscordInviteTargetType? targetType = null, ulong? targetUserId = null, ulong? targetApplicationId = null) - => await this.Discord.ApiClient.CreateChannelInviteAsync(this.Id, max_age, max_uses, temporary, unique, reason, targetType, targetUserId, targetApplicationId); - - /// - /// Adds a channel permission overwrite for specified member. - /// - /// The member to have the permission added. - /// The permissions to allow. - /// The permissions to deny. - /// Reason for audit logs. - /// - /// Thrown when the client does not have the permission. - /// Thrown when the channel does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public async Task AddOverwriteAsync(DiscordMember member, DiscordPermissions allow = default, DiscordPermissions deny = default, string? reason = null) - => await this.Discord.ApiClient.EditChannelPermissionsAsync(this.Id, member.Id, allow, deny, "member", reason); - - /// - /// Adds a channel permission overwrite for specified role. - /// - /// The role to have the permission added. - /// The permissions to allow. - /// The permissions to deny. - /// Reason for audit logs. - /// - /// Thrown when the client does not have the permission. - /// Thrown when the channel does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public async Task AddOverwriteAsync(DiscordRole role, DiscordPermissions allow = default, DiscordPermissions deny = default, string? reason = null) - => await this.Discord.ApiClient.EditChannelPermissionsAsync(this.Id, role.Id, allow, deny, "role", reason); - - /// - /// Deletes a channel permission overwrite for the specified member. - /// - /// The member to have the permission deleted. - /// Reason for audit logs. - /// - /// Thrown when the client does not have the permission. - /// Thrown when the channel does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public async Task DeleteOverwriteAsync(DiscordMember member, string reason = null) - => await this.Discord.ApiClient.DeleteChannelPermissionAsync(this.Id, member.Id, reason); - - /// - /// Deletes a channel permission overwrite for the specified role. - /// - /// The role to have the permission deleted. - /// Reason for audit logs. - /// - /// Thrown when the client does not have the permission. - /// Thrown when the channel does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public async Task DeleteOverwriteAsync(DiscordRole role, string reason = null) - => await this.Discord.ApiClient.DeleteChannelPermissionAsync(this.Id, role.Id, reason); - - /// - /// Post a typing indicator - /// - /// - /// Thrown when the channel does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public async Task TriggerTypingAsync() - { - if (!Utilities.IsTextableChannel(this)) - { - throw new ArgumentException("Cannot start typing in a non-text channel."); - } - else - { - await this.Discord.ApiClient.TriggerTypingAsync(this.Id); - } - } - - /// - /// Returns all pinned messages - /// - /// - /// Thrown when the client does not have the permission. - /// Thrown when the channel does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public async Task> GetPinnedMessagesAsync() => !Utilities.IsTextableChannel(this) || this.Type is DiscordChannelType.Voice - ? throw new ArgumentException("A non-text channel does not have pinned messages.") - : await this.Discord.ApiClient.GetPinnedMessagesAsync(this.Id); - - /// - /// Create a new webhook - /// - /// The name of the webhook. - /// The image for the default webhook avatar. - /// Reason for audit logs. - /// - /// Thrown when the client does not have the permission. - /// Thrown when the channel does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public async Task CreateWebhookAsync(string name, Optional avatar = default, string reason = null) - { - Optional av64 = Optional.FromNoValue(); - if (avatar.HasValue && avatar.Value != null) - { - using InlineMediaTool imgtool = new(avatar.Value); - av64 = imgtool.GetBase64(); - } - else if (avatar.HasValue) - { - av64 = null; - } - - return await this.Discord.ApiClient.CreateWebhookAsync(this.Id, name, av64, reason); - } - - /// - /// Returns a list of webhooks - /// - /// - /// Thrown when the client does not have the permission. - /// Thrown when the channel does not exist. - /// Thrown when Discord is unable to process the request. - public async Task> GetWebhooksAsync() - => await this.Discord.ApiClient.GetChannelWebhooksAsync(this.Id); - - /// - /// Moves a member to this voice channel - /// - /// The member to be moved. - /// - /// Thrown when the client does not have the permission. - /// Thrown when the channel does not exists or if the Member does not exists. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public async Task PlaceMemberAsync(DiscordMember member) - { - if (this.Type is not DiscordChannelType.Voice and not DiscordChannelType.Stage) - { - throw new ArgumentException("Cannot place a member in a non-voice channel!"); // be a little more angry, let em learn!!1 - } - - await this.Discord.ApiClient.ModifyGuildMemberAsync(this.Guild.Id, member.Id, voiceChannelId: this.Id); - } - - /// - /// Follows a news channel - /// - /// Channel to crosspost messages to - /// Thrown when trying to follow a non-news channel - /// Thrown when the current user doesn't have on the target channel - public async Task FollowAsync(DiscordChannel targetChannel) => this.Type != DiscordChannelType.News - ? throw new ArgumentException("Cannot follow a non-news channel.") - : await this.Discord.ApiClient.FollowChannelAsync(this.Id, targetChannel.Id); - - /// - /// Publishes a message in a news channel to following channels - /// - /// Message to publish - /// Thrown when the message has already been crossposted - /// - /// Thrown when the current user doesn't have and/or - /// - public async Task CrosspostMessageAsync(DiscordMessage message) => (message.Flags & DiscordMessageFlags.Crossposted) == DiscordMessageFlags.Crossposted - ? throw new ArgumentException("Message is already crossposted.") - : await this.Discord.ApiClient.CrosspostMessageAsync(this.Id, message.Id); - - /// - /// Updates the current user's suppress state in this channel, if stage channel. - /// - /// Toggles the suppress state. - /// Sets the time the user requested to speak. - /// Thrown when the channel is not a stage channel. - public async Task UpdateCurrentUserVoiceStateAsync(bool? suppress, DateTimeOffset? requestToSpeakTimestamp = null) - { - if (this.Type != DiscordChannelType.Stage) - { - throw new ArgumentException("Voice state can only be updated in a stage channel."); - } - - await this.Discord.ApiClient.UpdateCurrentUserVoiceStateAsync(this.GuildId.Value, this.Id, suppress, requestToSpeakTimestamp); - } - - /// - /// Creates a stage instance in this stage channel. - /// - /// The topic of the stage instance. - /// The privacy level of the stage instance. - /// The reason the stage instance was created. - /// The created stage instance. - public async Task CreateStageInstanceAsync(string topic, DiscordStagePrivacyLevel? privacyLevel = null, string reason = null) => this.Type != DiscordChannelType.Stage - ? throw new ArgumentException("A stage instance can only be created in a stage channel.") - : await this.Discord.ApiClient.CreateStageInstanceAsync(this.Id, topic, privacyLevel, reason); - - /// - /// Gets the stage instance in this stage channel. - /// - /// The stage instance in the channel. - public async Task GetStageInstanceAsync() => this.Type != DiscordChannelType.Stage - ? throw new ArgumentException("A stage instance can only be created in a stage channel.") - : await this.Discord.ApiClient.GetStageInstanceAsync(this.Id); - - /// - /// Modifies the stage instance in this stage channel. - /// - /// Action to perform. - /// The modified stage instance. - public async Task ModifyStageInstanceAsync(Action action) - { - if (this.Type != DiscordChannelType.Stage) - { - throw new ArgumentException("A stage instance can only be created in a stage channel."); - } - - StageInstanceEditModel mdl = new(); - action(mdl); - return await this.Discord.ApiClient.ModifyStageInstanceAsync(this.Id, mdl.Topic, mdl.PrivacyLevel, mdl.AuditLogReason); - } - - /// - /// Deletes the stage instance in this stage channel. - /// - /// The reason the stage instance was deleted. - public async Task DeleteStageInstanceAsync(string reason = null) - => await this.Discord.ApiClient.DeleteStageInstanceAsync(this.Id, reason); - - /// - /// Calculates permissions for a given member. - /// - /// Member to calculate permissions for. - /// Calculated permissions for a given member. - public DiscordPermissions PermissionsFor(DiscordMember mbr) - { - // future note: might be able to simplify @everyone role checks to just check any role... but I'm not sure - // xoxo, ~uwx - // - // you should use a single tilde - // ~emzi - - // user > role > everyone - // allow > deny > undefined - // => - // user allow > user deny > role allow > role deny > everyone allow > everyone deny - // thanks to meew0 - - // Two notes about this: // - // One: Threads are always synced to their parent. // - // Two: Threads always have a parent present(?). // - // If this is a thread, calculate on the parent; doing this on a thread does not work. // - if (this.IsThread) - { - return this.Parent.PermissionsFor(mbr); - } - - if (this.IsPrivate || this.Guild is null) - { - return DiscordPermissions.None; - } - - if (this.Guild.OwnerId == mbr.Id) - { - return DiscordPermissions.All; - } - - DiscordPermissions perms; - - // assign @everyone permissions - DiscordRole everyoneRole = this.Guild.EveryoneRole; - perms = everyoneRole.Permissions; - - // roles that member is in - DiscordRole[] mbRoles = mbr.Roles.Where(xr => xr.Id != everyoneRole.Id).ToArray(); - - // assign permissions from member's roles (in order) - perms |= mbRoles.Aggregate(DiscordPermissions.None, (c, role) => c | role.Permissions); - - // Administrator grants all permissions and cannot be overridden - if (perms.HasPermission(DiscordPermission.Administrator)) - { - return DiscordPermissions.All; - } - - // channel overrides for roles that member is in - List mbRoleOverrides = mbRoles - .Select(xr => this.permissionOverwrites.FirstOrDefault(xo => xo.Id == xr.Id)) - .Where(xo => xo != null) - .ToList(); - - // assign channel permission overwrites for @everyone pseudo-role - DiscordOverwrite? everyoneOverwrites = this.permissionOverwrites.FirstOrDefault(xo => xo.Id == everyoneRole.Id); - if (everyoneOverwrites != null) - { - perms &= ~everyoneOverwrites.Denied; - perms |= everyoneOverwrites.Allowed; - } - - // assign channel permission overwrites for member's roles (explicit deny) - perms &= ~mbRoleOverrides.Aggregate(DiscordPermissions.None, (c, overs) => c | overs.Denied); - // assign channel permission overwrites for member's roles (explicit allow) - perms |= mbRoleOverrides.Aggregate(DiscordPermissions.None, (c, overs) => c | overs.Allowed); - - // channel overrides for just this member - DiscordOverwrite? mbOverrides = this.permissionOverwrites.FirstOrDefault(xo => xo.Id == mbr.Id); - if (mbOverrides == null) - { - return perms; - } - - // assign channel permission overwrites for just this member - perms &= ~mbOverrides.Denied; - perms |= mbOverrides.Allowed; - - return perms; - } - - /// - /// Calculates permissions for a given role. - /// - /// Role to calculate permissions for. - /// Calculated permissions for a given role. - public DiscordPermissions PermissionsFor(DiscordRole role) - { - if (this.IsThread) - { - return this.Parent.PermissionsFor(role); - } - - if (this.IsPrivate || this.Guild is null) - { - return DiscordPermissions.None; - } - - if (role.guild_id != this.Guild.Id) - { - throw new ArgumentException("Given role does not belong to this channel's guild."); - } - - DiscordPermissions perms; - - // assign @everyone permissions - DiscordRole everyoneRole = this.Guild.EveryoneRole; - perms = everyoneRole.Permissions; - - // add role permissions - perms |= role.Permissions; - - // Administrator grants all permissions and cannot be overridden - if (perms.HasPermission(DiscordPermission.Administrator)) - { - return DiscordPermissions.All; - } - - // channel overrides for the @everyone role - DiscordOverwrite? everyoneRoleOverwrites = this.permissionOverwrites.FirstOrDefault(xo => xo.Id == everyoneRole.Id); - if (everyoneRoleOverwrites is not null) - { - // assign channel permission overwrites for the role (explicit deny) - perms &= ~everyoneRoleOverwrites.Denied; - - // assign channel permission overwrites for the role (explicit allow) - perms |= everyoneRoleOverwrites.Allowed; - } - - // channel overrides for the role - DiscordOverwrite? roleOverwrites = this.permissionOverwrites.FirstOrDefault(xo => xo.Id == role.Id); - if (roleOverwrites is null) - { - return perms; - } - - DiscordPermissions roleDenied = roleOverwrites.Denied; - - if (everyoneRoleOverwrites is not null) - { - roleDenied &= ~everyoneRoleOverwrites.Allowed; - } - - // assign channel permission overwrites for the role (explicit deny) - perms &= ~roleDenied; - - // assign channel permission overwrites for the role (explicit allow) - perms |= roleOverwrites.Allowed; - - return perms; - } - - /// - /// Returns a string representation of this channel. - /// - /// String representation of this channel. - public override string ToString() - { -#pragma warning disable IDE0046 // we don't want this to become a double ternary - if (this.Type == DiscordChannelType.Category) - { - return $"Channel Category {this.Name} ({this.Id})"; - } - - return this.Type is DiscordChannelType.Text or DiscordChannelType.News - ? $"Channel #{this.Name} ({this.Id})" - : !string.IsNullOrWhiteSpace(this.Name) ? $"Channel {this.Name} ({this.Id})" : $"Channel {this.Id}"; -#pragma warning restore IDE0046 - } - - #region ThreadMethods - - /// - /// Creates a new thread within this channel from the given message. - /// - /// Message to create the thread from. - /// The name of the thread. - /// The auto archive duration of the thread. - /// Reason for audit logs. - /// The created thread. - /// Thrown when the channel or message does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public async Task CreateThreadAsync(DiscordMessage message, string name, DiscordAutoArchiveDuration archiveAfter, string reason = null) - { - if (this.Type is not DiscordChannelType.Text and not DiscordChannelType.News) - { - throw new ArgumentException("Threads can only be created within text or news channels."); - } - else if (message.ChannelId != this.Id) - { - throw new ArgumentException("You must use a message from this channel to create a thread."); - } - - DiscordThreadChannel threadChannel = await this.Discord.ApiClient.CreateThreadFromMessageAsync(this.Id, message.Id, name, archiveAfter, reason); - this.Guild.threads.AddOrUpdate(threadChannel.Id, threadChannel, (_, _) => threadChannel); - return threadChannel; - } - - /// - /// Creates a new thread within this channel. - /// - /// The name of the thread. - /// The auto archive duration of the thread. - /// The type of thread to create, either a public, news or, private thread. - /// Reason for audit logs. - /// The created thread. - /// Thrown when the channel or message does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public async Task CreateThreadAsync(string name, DiscordAutoArchiveDuration archiveAfter, DiscordChannelType threadType, string reason = null) - { - if (this.Type is not DiscordChannelType.Text and not DiscordChannelType.News) - { - throw new InvalidOperationException("Threads can only be created within text or news channels."); - } - else if (this.Type != DiscordChannelType.News && threadType == DiscordChannelType.NewsThread) - { - throw new InvalidOperationException("News threads can only be created within a news channels."); - } - else if (threadType is not DiscordChannelType.PublicThread and not DiscordChannelType.PrivateThread and not DiscordChannelType.NewsThread) - { - throw new ArgumentException("Given channel type for creating a thread is not a valid type of thread."); - } - - DiscordThreadChannel threadChannel = await this.Discord.ApiClient.CreateThreadAsync(this.Id, name, archiveAfter, threadType, reason); - this.Guild.threads.AddOrUpdate(threadChannel.Id, threadChannel, (_, _) => threadChannel); - return threadChannel; - } - - #endregion - - #endregion - - /// - /// Checks whether this is equal to another object. - /// - /// Object to compare to. - /// Whether the object is equal to this . - public override bool Equals(object obj) => Equals(obj as DiscordChannel); - - /// - /// Checks whether this is equal to another . - /// - /// to compare to. - /// Whether the is equal to this . - public bool Equals(DiscordChannel e) => e is not null && (ReferenceEquals(this, e) || this.Id == e.Id); - - /// - /// Gets the hash code for this . - /// - /// The hash code for this . - public override int GetHashCode() => this.Id.GetHashCode(); - - /// - /// Gets whether the two objects are equal. - /// - /// First channel to compare. - /// Second channel to compare. - /// Whether the two channels are equal. - public static bool operator ==(DiscordChannel e1, DiscordChannel e2) - { - object? o1 = e1; - object? o2 = e2; - - return (o1 != null || o2 == null) && (o1 == null || o2 != null) && ((o1 == null && o2 == null) || e1.Id == e2.Id); - } - - /// - /// Gets whether the two objects are not equal. - /// - /// First channel to compare. - /// Second channel to compare. - /// Whether the two channels are not equal. - public static bool operator !=(DiscordChannel e1, DiscordChannel e2) - => !(e1 == e2); -} +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using DSharpPlus.Exceptions; +using DSharpPlus.Net.Abstractions; +using DSharpPlus.Net.Models; +using DSharpPlus.Net.Serialization; +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Represents a discord channel. +/// +[JsonConverter(typeof(DiscordForumChannelJsonConverter))] +public class DiscordChannel : SnowflakeObject, IEquatable +{ + /// + /// Gets ID of the guild to which this channel belongs. + /// + [JsonProperty("guild_id", NullValueHandling = NullValueHandling.Ignore)] + public ulong? GuildId { get; internal set; } + + /// + /// Gets ID of the category that contains this channel. For threads, gets the ID of the channel this thread was created in. + /// + [JsonProperty("parent_id", NullValueHandling = NullValueHandling.Include)] + public ulong? ParentId { get; internal set; } + + /// + /// Gets the category that contains this channel. For threads, gets the channel this thread was created in. + /// + [JsonIgnore] + public DiscordChannel Parent + => this.ParentId.HasValue ? this.Guild.GetChannel(this.ParentId.Value) : null; + + /// + /// Gets the name of this channel. + /// + [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] + public string Name { get; internal set; } + + /// + /// Gets the type of this channel. + /// + [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] + public virtual DiscordChannelType Type { get; internal set; } + + /// + /// Gets the position of this channel. + /// + [JsonProperty("position", NullValueHandling = NullValueHandling.Ignore)] + public int Position { get; internal set; } + + /// + /// Gets whether this channel is a DM channel. + /// + [JsonIgnore] + public bool IsPrivate + => this.Type is DiscordChannelType.Private or DiscordChannelType.Group; + + /// + /// Gets whether this channel is a channel category. + /// + [JsonIgnore] + public bool IsCategory + => this.Type == DiscordChannelType.Category; + + /// + /// Gets whether this channel is a thread. + /// + [JsonIgnore] + public bool IsThread + => this.Type is DiscordChannelType.PrivateThread or DiscordChannelType.PublicThread or DiscordChannelType.NewsThread; + + /// + /// Gets the guild to which this channel belongs. + /// + [JsonIgnore] + public DiscordGuild Guild + => this.GuildId.HasValue && this.Discord.Guilds.TryGetValue(this.GuildId.Value, out DiscordGuild? guild) ? guild : null; + + /// + /// Gets a collection of permission overwrites for this channel. + /// + [JsonIgnore] + public IReadOnlyList PermissionOverwrites + => this.permissionOverwrites; + + [JsonProperty("permission_overwrites", NullValueHandling = NullValueHandling.Ignore)] + internal List permissionOverwrites = []; + + /// + /// Gets the channel's topic. This is applicable to text channels only. + /// + [JsonProperty("topic", NullValueHandling = NullValueHandling.Ignore)] + public string Topic { get; internal set; } + + /// + /// Gets the ID of the last message sent in this channel. This is applicable to text channels only. + /// + /// For forum posts, this ID may point to an invalid mesage (e.g. the OP deleted the initial forum message). + [JsonProperty("last_message_id", NullValueHandling = NullValueHandling.Ignore)] + public ulong? LastMessageId { get; internal set; } + + /// + /// Gets this channel's bitrate. This is applicable to voice channels only. + /// + [JsonProperty("bitrate", NullValueHandling = NullValueHandling.Ignore)] + public int? Bitrate { get; internal set; } + + /// + /// Gets this channel's user limit. This is applicable to voice channels only. + /// + [JsonProperty("user_limit", NullValueHandling = NullValueHandling.Ignore)] + public int? UserLimit { get; internal set; } + + /// + /// Gets the slow mode delay configured for this channel.
+ /// All bots, as well as users with + /// or permissions in the channel are exempt from slow mode. + ///
+ [JsonProperty("rate_limit_per_user")] + public int? PerUserRateLimit { get; internal set; } + + /// + /// Gets this channel's video quality mode. This is applicable to voice channels only. + /// + [JsonProperty("video_quality_mode", NullValueHandling = NullValueHandling.Ignore)] + public DiscordVideoQualityMode? QualityMode { get; internal set; } + + /// + /// Gets when the last pinned message was pinned. + /// + [JsonProperty("last_pin_timestamp", NullValueHandling = NullValueHandling.Ignore)] + public DateTimeOffset? LastPinTimestamp { get; internal set; } + + /// + /// Gets this channel's mention string. + /// + [JsonIgnore] + public string Mention + => Formatter.Mention(this); + + /// + /// Gets this channel's children. This applies only to channel categories. + /// + [JsonIgnore] + public IReadOnlyList Children + { + get + { + return !this.IsCategory + ? throw new ArgumentException("Only channel categories contain children.") + : this.Guild.channels.Values.Where(e => e.ParentId == this.Id).ToList(); + } + } + + /// + /// Gets this channel's threads. This applies only to text and news channels. + /// + [JsonIgnore] + public IReadOnlyList Threads + { + get + { + return this.Type is not (DiscordChannelType.Text or DiscordChannelType.News or DiscordChannelType.GuildForum) + ? throw new ArgumentException("Only text channels can have threads.") + : this.Guild.threads.Values.Where(e => e.ParentId == this.Id).ToArray(); + } + } + + /// + /// Gets the list of members currently in the channel (if voice channel), or members who can see the channel (otherwise). + /// + [JsonIgnore] + public virtual IReadOnlyList Users + { + get + { + return this.Guild is null + ? throw new InvalidOperationException("Cannot query users outside of guild channels.") + : (IReadOnlyList)(this.Type is DiscordChannelType.Voice or DiscordChannelType.Stage + ? this.Guild.Members.Values.Where(x => x.VoiceState?.ChannelId == this.Id).ToList() + : this.Guild.Members.Values.Where(x => PermissionsFor(x).HasPermission(DiscordPermission.ViewChannel)).ToList()); + } + } + + /// + /// Gets whether this channel is an NSFW channel. + /// + [JsonProperty("nsfw")] + public bool IsNSFW { get; internal set; } + + [JsonProperty("rtc_region", NullValueHandling = NullValueHandling.Ignore)] + internal string RtcRegionId { get; set; } + + /// + /// Gets this channel's region override (if voice channel). + /// + [JsonIgnore] + public DiscordVoiceRegion RtcRegion + => this.RtcRegionId != null ? this.Discord.VoiceRegions[this.RtcRegionId] : null; + + /// + /// Gets the permissions of the user who invoked the command in this channel. + /// Only sent on the resolved channels of interaction responses for application commands. + /// + [JsonProperty("permissions")] + public DiscordPermissions? UserPermissions { get; internal set; } + + internal DiscordChannel() { } + + #region Methods + + /// + /// Sends a message to this channel. + /// + /// Content of the message to send. + /// The sent message. + /// Thrown when the client does not have the + /// permission if TTS is 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 async Task SendMessageAsync(string content) => !Utilities.IsTextableChannel(this) + ? throw new ArgumentException($"{this.Type} channels do not support sending text messages.") + : await this.Discord.ApiClient.CreateMessageAsync(this.Id, content, null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false, suppressNotifications: false); + + /// + /// Sends a message to this channel. + /// + /// Embed to attach to the message. + /// The sent message. + /// Thrown when the client does not have the + /// permission 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 async Task SendMessageAsync(DiscordEmbed embed) => !Utilities.IsTextableChannel(this) + ? throw new ArgumentException($"{this.Type} channels do not support sending text messages.") + : await this.Discord.ApiClient.CreateMessageAsync(this.Id, null, embed != null ? new[] { embed } : null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false, suppressNotifications: false); + + /// + /// Sends a message to this channel. + /// + /// Embed to attach to the message. + /// Content of the message to send. + /// The sent message. + /// Thrown when the client does not have the + /// permission if TTS is 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 async Task SendMessageAsync(string content, DiscordEmbed embed) => !Utilities.IsTextableChannel(this) + ? throw new ArgumentException($"{this.Type} channels do not support sending text messages.") + : await this.Discord.ApiClient.CreateMessageAsync(this.Id, content, embed != null ? new[] { embed } : null, replyMessageId: null, mentionReply: false, failOnInvalidReply: false, suppressNotifications: false); + + /// + /// Sends a message to this channel. + /// + /// The builder with all the items to send. + /// The sent message. + /// Thrown when the client does not have the + /// permission 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 async Task SendMessageAsync(DiscordMessageBuilder builder) => !Utilities.IsTextableChannel(this) + ? throw new ArgumentException($"{this.Type} channels do not support sending text messages.") + : await this.Discord.ApiClient.CreateMessageAsync(this.Id, builder); + + /// + /// Sends a message to this channel. + /// + /// The builder with all the items to send. + /// The sent message. + /// Thrown when the client does not have the + /// permission 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 async Task SendMessageAsync(Action action) + { + if (!Utilities.IsTextableChannel(this)) + { + throw new ArgumentException($"{this.Type} channels do not support sending text messages."); + } + + DiscordMessageBuilder builder = new(); + action(builder); + + return await this.Discord.ApiClient.CreateMessageAsync(this.Id, builder); + } + + /// + /// Creates an event bound to this channel. + /// + /// The name of the event, up to 100 characters. + /// The description of this event, up to 1000 characters. + /// The privacy level. Currently only is supported + /// When this event starts. + /// When this event ends. External events require an end time. + /// The created event. + /// + public Task CreateGuildEventAsync(string name, string description, DiscordScheduledGuildEventPrivacyLevel privacyLevel, DateTimeOffset start, DateTimeOffset? end) + => this.Type is not (DiscordChannelType.Voice or DiscordChannelType.Stage) ? throw new InvalidOperationException("Events can only be created on voice an stage chnanels") : + this.Guild.CreateEventAsync(name, description, this.Id, this.Type is DiscordChannelType.Stage ? DiscordScheduledGuildEventType.StageInstance : DiscordScheduledGuildEventType.VoiceChannel, privacyLevel, start, end); + + // Please send memes to Naamloos#2887 at discord <3 thank you + + /// + /// Deletes a guild channel + /// + /// Reason for audit logs. + /// + /// Thrown when the client does not have the permission. + /// Thrown when the channel does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task DeleteAsync(string reason = null) + => await this.Discord.ApiClient.DeleteChannelAsync(this.Id, reason); + + /// + /// Clones this channel. This operation will create a channel with identical settings to this one. Note that this will not copy messages. + /// + /// Reason for audit logs. + /// Newly-created channel. + /// Thrown when the client does not have the permission. + /// Thrown when the channel does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task CloneAsync(string reason = null) + { + if (this.Guild == null) + { + throw new InvalidOperationException("Non-guild channels cannot be cloned."); + } + + List ovrs = [.. this.permissionOverwrites.Select(DiscordOverwriteBuilder.From)]; + + int? bitrate = this.Bitrate; + int? userLimit = this.UserLimit; + Optional perUserRateLimit = this.PerUserRateLimit; + + if (this.Type != DiscordChannelType.Voice) + { + bitrate = null; + userLimit = null; + } + + if (this.Type != DiscordChannelType.Text) + { + perUserRateLimit = Optional.FromNoValue(); + } + + return await this.Guild.CreateChannelAsync(this.Name, this.Type, this.Parent, this.Topic, bitrate, userLimit, ovrs, this.IsNSFW, perUserRateLimit, this.QualityMode, this.Position, reason); + } + + /// + /// Returns a specific message + /// + /// The ID of the message + /// Whether to always make a REST request. + /// + /// Thrown when the client does not have the permission. + /// Thrown when the channel does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task GetMessageAsync(ulong id, bool skipCache = false) => !skipCache + && this.Discord is DiscordClient dc + && dc.MessageCache != null + && dc.MessageCache.TryGet(id, out DiscordMessage? msg) + ? msg + : await this.Discord.ApiClient.GetMessageAsync(this.Id, id); + + /// + /// Modifies the current channel. + /// + /// Action to perform on this channel + /// + /// Thrown when the client does not have the permission. + /// Thrown when the channel does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task ModifyAsync(Action action) + { + ChannelEditModel mdl = new(); + action(mdl); + await this.Discord.ApiClient.ModifyChannelAsync + ( + this.Id, + mdl.Name, + mdl.Position, + mdl.Topic, + mdl.Nsfw, + mdl.Parent.HasValue ? mdl.Parent.Value?.Id : default(Optional), + mdl.Bitrate, + mdl.Userlimit, + mdl.PerUserRateLimit, + mdl.RtcRegion.IfPresent(r => r?.Id), + mdl.QualityMode, + mdl.Type, + mdl.PermissionOverwrites, + mdl.Flags, + mdl.AvailableTags, + mdl.DefaultAutoArchiveDuration, + mdl.DefaultReaction, + mdl.DefaultThreadRateLimit, + mdl.DefaultSortOrder, + mdl.DefaultForumLayout, + mdl.AuditLogReason + ); + } + + /// + /// Updates the channel position + /// + /// Position the channel should be moved to. + /// Reason for audit logs. + /// Whether to sync channel permissions with the parent, if moving to a new category. + /// The new parent ID if the channel is to be moved to a new category. + /// + /// Thrown when the client does not have the permission. + /// Thrown when the channel does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task ModifyPositionAsync(int position, string reason = null, bool? lockPermissions = null, ulong? parentId = null) + { + if (this.Guild is null) + { + throw new InvalidOperationException("Cannot modify order of non-guild channels."); + } + + DiscordChannel[] chns = [.. this.Guild.channels.Values.Where(xc => xc.Type == this.Type).OrderBy(xc => xc.Position)]; + RestGuildChannelReorderPayload[] pmds = new RestGuildChannelReorderPayload[chns.Length]; + for (int i = 0; i < chns.Length; i++) + { + pmds[i] = new() + { + ChannelId = chns[i].Id, + Position = chns[i].Id == this.Id ? position : chns[i].Position >= position ? chns[i].Position + 1 : chns[i].Position, + LockPermissions = chns[i].Id == this.Id ? lockPermissions : null, + ParentId = chns[i].Id == this.Id ? parentId : null + }; + } + + await this.Discord.ApiClient.ModifyGuildChannelPositionAsync(this.Guild.Id, pmds, reason); + } + + /// + /// Returns a list of messages before a certain message. This will execute one API request per 100 messages. + /// The amount of messages to fetch. + /// Message to fetch before from. + /// Cancels the enumeration before doing the next api request + /// + /// 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 IAsyncEnumerable GetMessagesBeforeAsync(ulong before, int limit = 100, CancellationToken cancellationToken = default) + => GetMessagesInternalAsync(limit, before, cancellationToken: cancellationToken); + + /// + /// Returns a list of messages after a certain message. This will execute one API request per 100 messages. + /// The amount of messages to fetch. + /// Message to fetch after from. + /// Cancels the enumeration before doing the next api request + /// + /// 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 IAsyncEnumerable GetMessagesAfterAsync(ulong after, int limit = 100, CancellationToken cancellationToken = default) + => GetMessagesInternalAsync(limit, after: after, cancellationToken: cancellationToken); + + /// + /// Returns a list of messages around a certain message. This will execute one API request per 100 messages. + /// The amount of messages to fetch. + /// Message to fetch around from. + /// Cancels the enumeration before doing the next api request + /// + /// 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 IAsyncEnumerable GetMessagesAroundAsync(ulong around, int limit = 100, CancellationToken cancellationToken = default) + => GetMessagesInternalAsync(limit, around: around, cancellationToken: cancellationToken); + + /// + /// Returns a list of messages from the last message in the channel. This will execute one API request per 100 messages. + /// The amount of messages to fetch. + /// Cancels the enumeration before doing the next api request + /// + /// 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 IAsyncEnumerable GetMessagesAsync(int limit = 100, CancellationToken cancellationToken = default) => + GetMessagesInternalAsync(limit, cancellationToken: cancellationToken); + + private async IAsyncEnumerable GetMessagesInternalAsync + ( + int limit = 100, + ulong? before = null, + ulong? after = null, + ulong? around = null, + [EnumeratorCancellation] + CancellationToken cancellationToken = default + ) + { + if (!Utilities.IsTextableChannel(this)) + { + throw new ArgumentException($"Cannot get the messages of a {this.Type} channel."); + } + + if (limit < 0) + { + throw new ArgumentException("Cannot get a negative number of messages."); + } + + if (limit == 0) + { + yield break; + } + + //return this.Discord.ApiClient.GetChannelMessagesAsync(this.Id, limit, before, after, around); + if (limit > 100 && around != null) + { + throw new InvalidOperationException("Cannot get more than 100 messages around the specified ID."); + } + + int remaining = limit; + ulong? last = null; + bool isbefore = before != null || (before is null && after is null && around is null); + + int lastCount; + do + { + if (cancellationToken.IsCancellationRequested) + { + yield break; + } + + int fetchSize = remaining > 100 ? 100 : remaining; + IReadOnlyList fetchedMessages = await this.Discord.ApiClient.GetChannelMessagesAsync(this.Id, fetchSize, isbefore ? last ?? before : null, !isbefore ? last ?? after : null, around); + + lastCount = fetchedMessages.Count; + remaining -= lastCount; + + //We sort the returned messages by ID so that they are in order in case Discord switches the order AGAIN. + DiscordMessage[] sortedMessageArray = [.. fetchedMessages]; + Array.Sort(sortedMessageArray, (x, y) => x.Id.CompareTo(y.Id)); + + if (!isbefore) + { + foreach (DiscordMessage msg in sortedMessageArray) + { + yield return msg; + } + + last = sortedMessageArray.LastOrDefault()?.Id; + } + else + { + for (int i = sortedMessageArray.Length - 1; i >= 0; i--) + { + yield return sortedMessageArray[i]; + } + + last = sortedMessageArray.FirstOrDefault()?.Id; + } + } + while (remaining > 0 && lastCount > 0 && lastCount == 100); + } + + /// + /// Gets the threads that are public and archived for this channel. + /// + /// A containing the threads for this query and if an other call will yield more threads. + /// Thrown when the client does not have the permission. + /// Thrown when the channel does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task ListPublicArchivedThreadsAsync(DateTimeOffset? before = null, int limit = 0) => this.Type is not DiscordChannelType.Text and not DiscordChannelType.News and not DiscordChannelType.GuildForum + ? throw new InvalidOperationException() + : await this.Discord.ApiClient.ListPublicArchivedThreadsAsync(this.GuildId.Value, this.Id, before?.ToString("o"), limit); + + /// + /// Gets the threads that are private and archived for this channel. + /// + /// A containing the threads for this query and if an other call will yield more threads. + /// Thrown when the client does not have the + /// and the + /// permission. + /// Thrown when the channel does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task ListPrivateArchivedThreadsAsync(DateTimeOffset? before = null, int limit = 0) => this.Type is not DiscordChannelType.Text and not DiscordChannelType.News and not DiscordChannelType.GuildForum + ? throw new InvalidOperationException() + : await this.Discord.ApiClient.ListPrivateArchivedThreadsAsync(this.GuildId.Value, this.Id, limit, before?.ToString("o")); + + /// + /// Gets the private and archived threads that the current member has joined in this channel. + /// + /// A containing the threads for this query and if an other call will yield more threads. + /// Thrown when the client does not have the + /// permission. + /// Thrown when the channel does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task ListJoinedPrivateArchivedThreadsAsync(DateTimeOffset? before = null, int limit = 0) => this.Type is not DiscordChannelType.Text and not DiscordChannelType.News and not DiscordChannelType.GuildForum + ? throw new InvalidOperationException() + : await this.Discord.ApiClient.ListJoinedPrivateArchivedThreadsAsync(this.GuildId.Value, this.Id, limit, (ulong?)before?.ToUnixTimeSeconds()); + + /// + /// Deletes multiple messages if they are less than 14 days old. If they are older, none of the messages will be deleted and you will receive a error. + /// + /// A collection of messages to delete. + /// Reason for audit logs. + /// The number of deleted messages + /// One api call per 100 messages + public async Task DeleteMessagesAsync(IReadOnlyList messages, string? reason = null) + { + ArgumentNullException.ThrowIfNull(messages, nameof(messages)); + int count = messages.Count; + + if (count == 0) + { + throw new ArgumentException("You need to specify at least one message to delete."); + } + else if (count == 1) + { + await this.Discord.ApiClient.DeleteMessageAsync(this.Id, messages[0].Id, reason); + return 1; + } + + int deleteCount = 0; + + try + { + for (int i = 0; i < count; i += 100) + { + int takeCount = Math.Min(100, count - i); + DiscordMessage[] messageBatch = messages.Skip(i).Take(takeCount).ToArray(); + + foreach (DiscordMessage message in messageBatch) + { + if (message.ChannelId != this.Id) + { + throw new ArgumentException( + $"You cannot delete messages from channel {message.Channel.Name} through channel {this.Name}!"); + } + else if (message.Timestamp < DateTimeOffset.UtcNow.AddDays(-14)) + { + throw new ArgumentException("You can only delete messages that are less than 14 days old."); + } + } + + await this.Discord.ApiClient.DeleteMessagesAsync(this.Id, + messageBatch.Select(x => x.Id), reason); + deleteCount += takeCount; + } + } + catch (DiscordException e) + { + throw new BulkDeleteFailedException(deleteCount, e); + } + + return deleteCount; + } + + /// + /// Deletes multiple messages if they are less than 14 days old. Does one api request per 100 + /// + /// A collection of messages to delete. + /// Reason for audit logs. + /// The number of deleted messages + /// Exception which contains the exception which was thrown and the count of messages which were deleted successfully + /// One api call per 100 messages + public async Task DeleteMessagesAsync(IAsyncEnumerable messages, string? reason = null) + { + List list = new(100); + int count = 0; + try + { + await foreach (DiscordMessage message in messages) + { + list.Add(message); + + if (list.Count != 100) + { + continue; + } + + await DeleteMessagesAsync(list, reason); + list.Clear(); + count += 100; + } + + if (list.Count > 0) + { + await DeleteMessagesAsync(list, reason); + count += list.Count; + } + } + catch (BulkDeleteFailedException e) + { + throw new BulkDeleteFailedException(count + e.MessagesDeleted, e.InnerException); + } + catch (DiscordException e) + { + throw new BulkDeleteFailedException(count, e); + } + + return count; + } + + /// + /// Deletes a message + /// + /// The message to be deleted. + /// Reason for audit logs. + /// + /// Thrown when the client does not have the + /// permission. + /// Thrown when the channel does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task DeleteMessageAsync(DiscordMessage message, string reason = null) + => await this.Discord.ApiClient.DeleteMessageAsync(this.Id, message.Id, reason); + + /// + /// Returns a list of invite objects + /// + /// + /// Thrown when the client does not have the + /// permission. + /// Thrown when the channel does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task> GetInvitesAsync() => this.Guild == null + ? throw new ArgumentException("Cannot get the invites of a channel that does not belong to a guild.") + : await this.Discord.ApiClient.GetChannelInvitesAsync(this.Id); + + /// + /// Create a new invite object + /// + /// Duration of invite in seconds before expiry, or 0 for never. Defaults to 86400. + /// Max number of uses or 0 for unlimited. Defaults to 0 + /// Whether this invite only grants temporary membership. Defaults to false. + /// If true, don't try to reuse a similar invite (useful for creating many unique one time use invites) + /// Reason for audit logs. + /// The target type of the invite, for stream and embedded application invites. + /// The ID of the target user. + /// The ID of the target application. + /// + /// Thrown when the client does not have the + /// permission. + /// Thrown when the channel does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task CreateInviteAsync(int max_age = 86400, int max_uses = 0, bool temporary = false, bool unique = false, string reason = null, DiscordInviteTargetType? targetType = null, ulong? targetUserId = null, ulong? targetApplicationId = null) + => await this.Discord.ApiClient.CreateChannelInviteAsync(this.Id, max_age, max_uses, temporary, unique, reason, targetType, targetUserId, targetApplicationId); + + /// + /// Adds a channel permission overwrite for specified member. + /// + /// The member to have the permission added. + /// The permissions to allow. + /// The permissions to deny. + /// Reason for audit logs. + /// + /// Thrown when the client does not have the permission. + /// Thrown when the channel does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task AddOverwriteAsync(DiscordMember member, DiscordPermissions allow = default, DiscordPermissions deny = default, string? reason = null) + => await this.Discord.ApiClient.EditChannelPermissionsAsync(this.Id, member.Id, allow, deny, "member", reason); + + /// + /// Adds a channel permission overwrite for specified role. + /// + /// The role to have the permission added. + /// The permissions to allow. + /// The permissions to deny. + /// Reason for audit logs. + /// + /// Thrown when the client does not have the permission. + /// Thrown when the channel does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task AddOverwriteAsync(DiscordRole role, DiscordPermissions allow = default, DiscordPermissions deny = default, string? reason = null) + => await this.Discord.ApiClient.EditChannelPermissionsAsync(this.Id, role.Id, allow, deny, "role", reason); + + /// + /// Deletes a channel permission overwrite for the specified member. + /// + /// The member to have the permission deleted. + /// Reason for audit logs. + /// + /// Thrown when the client does not have the permission. + /// Thrown when the channel does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task DeleteOverwriteAsync(DiscordMember member, string reason = null) + => await this.Discord.ApiClient.DeleteChannelPermissionAsync(this.Id, member.Id, reason); + + /// + /// Deletes a channel permission overwrite for the specified role. + /// + /// The role to have the permission deleted. + /// Reason for audit logs. + /// + /// Thrown when the client does not have the permission. + /// Thrown when the channel does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task DeleteOverwriteAsync(DiscordRole role, string reason = null) + => await this.Discord.ApiClient.DeleteChannelPermissionAsync(this.Id, role.Id, reason); + + /// + /// Post a typing indicator + /// + /// + /// Thrown when the channel does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task TriggerTypingAsync() + { + if (!Utilities.IsTextableChannel(this)) + { + throw new ArgumentException("Cannot start typing in a non-text channel."); + } + else + { + await this.Discord.ApiClient.TriggerTypingAsync(this.Id); + } + } + + /// + /// Returns all pinned messages + /// + /// + /// Thrown when the client does not have the permission. + /// Thrown when the channel does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task> GetPinnedMessagesAsync() => !Utilities.IsTextableChannel(this) || this.Type is DiscordChannelType.Voice + ? throw new ArgumentException("A non-text channel does not have pinned messages.") + : await this.Discord.ApiClient.GetPinnedMessagesAsync(this.Id); + + /// + /// Create a new webhook + /// + /// The name of the webhook. + /// The image for the default webhook avatar. + /// Reason for audit logs. + /// + /// Thrown when the client does not have the permission. + /// Thrown when the channel does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task CreateWebhookAsync(string name, Optional avatar = default, string reason = null) + { + Optional av64 = Optional.FromNoValue(); + if (avatar.HasValue && avatar.Value != null) + { + using InlineMediaTool imgtool = new(avatar.Value); + av64 = imgtool.GetBase64(); + } + else if (avatar.HasValue) + { + av64 = null; + } + + return await this.Discord.ApiClient.CreateWebhookAsync(this.Id, name, av64, reason); + } + + /// + /// Returns a list of webhooks + /// + /// + /// Thrown when the client does not have the permission. + /// Thrown when the channel does not exist. + /// Thrown when Discord is unable to process the request. + public async Task> GetWebhooksAsync() + => await this.Discord.ApiClient.GetChannelWebhooksAsync(this.Id); + + /// + /// Moves a member to this voice channel + /// + /// The member to be moved. + /// + /// Thrown when the client does not have the permission. + /// Thrown when the channel does not exists or if the Member does not exists. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task PlaceMemberAsync(DiscordMember member) + { + if (this.Type is not DiscordChannelType.Voice and not DiscordChannelType.Stage) + { + throw new ArgumentException("Cannot place a member in a non-voice channel!"); // be a little more angry, let em learn!!1 + } + + await this.Discord.ApiClient.ModifyGuildMemberAsync(this.Guild.Id, member.Id, voiceChannelId: this.Id); + } + + /// + /// Follows a news channel + /// + /// Channel to crosspost messages to + /// Thrown when trying to follow a non-news channel + /// Thrown when the current user doesn't have on the target channel + public async Task FollowAsync(DiscordChannel targetChannel) => this.Type != DiscordChannelType.News + ? throw new ArgumentException("Cannot follow a non-news channel.") + : await this.Discord.ApiClient.FollowChannelAsync(this.Id, targetChannel.Id); + + /// + /// Publishes a message in a news channel to following channels + /// + /// Message to publish + /// Thrown when the message has already been crossposted + /// + /// Thrown when the current user doesn't have and/or + /// + public async Task CrosspostMessageAsync(DiscordMessage message) => (message.Flags & DiscordMessageFlags.Crossposted) == DiscordMessageFlags.Crossposted + ? throw new ArgumentException("Message is already crossposted.") + : await this.Discord.ApiClient.CrosspostMessageAsync(this.Id, message.Id); + + /// + /// Updates the current user's suppress state in this channel, if stage channel. + /// + /// Toggles the suppress state. + /// Sets the time the user requested to speak. + /// Thrown when the channel is not a stage channel. + public async Task UpdateCurrentUserVoiceStateAsync(bool? suppress, DateTimeOffset? requestToSpeakTimestamp = null) + { + if (this.Type != DiscordChannelType.Stage) + { + throw new ArgumentException("Voice state can only be updated in a stage channel."); + } + + await this.Discord.ApiClient.UpdateCurrentUserVoiceStateAsync(this.GuildId.Value, this.Id, suppress, requestToSpeakTimestamp); + } + + /// + /// Creates a stage instance in this stage channel. + /// + /// The topic of the stage instance. + /// The privacy level of the stage instance. + /// The reason the stage instance was created. + /// The created stage instance. + public async Task CreateStageInstanceAsync(string topic, DiscordStagePrivacyLevel? privacyLevel = null, string reason = null) => this.Type != DiscordChannelType.Stage + ? throw new ArgumentException("A stage instance can only be created in a stage channel.") + : await this.Discord.ApiClient.CreateStageInstanceAsync(this.Id, topic, privacyLevel, reason); + + /// + /// Gets the stage instance in this stage channel. + /// + /// The stage instance in the channel. + public async Task GetStageInstanceAsync() => this.Type != DiscordChannelType.Stage + ? throw new ArgumentException("A stage instance can only be created in a stage channel.") + : await this.Discord.ApiClient.GetStageInstanceAsync(this.Id); + + /// + /// Modifies the stage instance in this stage channel. + /// + /// Action to perform. + /// The modified stage instance. + public async Task ModifyStageInstanceAsync(Action action) + { + if (this.Type != DiscordChannelType.Stage) + { + throw new ArgumentException("A stage instance can only be created in a stage channel."); + } + + StageInstanceEditModel mdl = new(); + action(mdl); + return await this.Discord.ApiClient.ModifyStageInstanceAsync(this.Id, mdl.Topic, mdl.PrivacyLevel, mdl.AuditLogReason); + } + + /// + /// Deletes the stage instance in this stage channel. + /// + /// The reason the stage instance was deleted. + public async Task DeleteStageInstanceAsync(string reason = null) + => await this.Discord.ApiClient.DeleteStageInstanceAsync(this.Id, reason); + + /// + /// Calculates permissions for a given member. + /// + /// Member to calculate permissions for. + /// Calculated permissions for a given member. + public DiscordPermissions PermissionsFor(DiscordMember mbr) + { + // future note: might be able to simplify @everyone role checks to just check any role... but I'm not sure + // xoxo, ~uwx + // + // you should use a single tilde + // ~emzi + + // user > role > everyone + // allow > deny > undefined + // => + // user allow > user deny > role allow > role deny > everyone allow > everyone deny + // thanks to meew0 + + // Two notes about this: // + // One: Threads are always synced to their parent. // + // Two: Threads always have a parent present(?). // + // If this is a thread, calculate on the parent; doing this on a thread does not work. // + if (this.IsThread) + { + return this.Parent.PermissionsFor(mbr); + } + + if (this.IsPrivate || this.Guild is null) + { + return DiscordPermissions.None; + } + + if (this.Guild.OwnerId == mbr.Id) + { + return DiscordPermissions.All; + } + + DiscordPermissions perms; + + // assign @everyone permissions + DiscordRole everyoneRole = this.Guild.EveryoneRole; + perms = everyoneRole.Permissions; + + // roles that member is in + DiscordRole[] mbRoles = mbr.Roles.Where(xr => xr.Id != everyoneRole.Id).ToArray(); + + // assign permissions from member's roles (in order) + perms |= mbRoles.Aggregate(DiscordPermissions.None, (c, role) => c | role.Permissions); + + // Administrator grants all permissions and cannot be overridden + if (perms.HasPermission(DiscordPermission.Administrator)) + { + return DiscordPermissions.All; + } + + // channel overrides for roles that member is in + List mbRoleOverrides = mbRoles + .Select(xr => this.permissionOverwrites.FirstOrDefault(xo => xo.Id == xr.Id)) + .Where(xo => xo != null) + .ToList(); + + // assign channel permission overwrites for @everyone pseudo-role + DiscordOverwrite? everyoneOverwrites = this.permissionOverwrites.FirstOrDefault(xo => xo.Id == everyoneRole.Id); + if (everyoneOverwrites != null) + { + perms &= ~everyoneOverwrites.Denied; + perms |= everyoneOverwrites.Allowed; + } + + // assign channel permission overwrites for member's roles (explicit deny) + perms &= ~mbRoleOverrides.Aggregate(DiscordPermissions.None, (c, overs) => c | overs.Denied); + // assign channel permission overwrites for member's roles (explicit allow) + perms |= mbRoleOverrides.Aggregate(DiscordPermissions.None, (c, overs) => c | overs.Allowed); + + // channel overrides for just this member + DiscordOverwrite? mbOverrides = this.permissionOverwrites.FirstOrDefault(xo => xo.Id == mbr.Id); + if (mbOverrides == null) + { + return perms; + } + + // assign channel permission overwrites for just this member + perms &= ~mbOverrides.Denied; + perms |= mbOverrides.Allowed; + + return perms; + } + + /// + /// Calculates permissions for a given role. + /// + /// Role to calculate permissions for. + /// Calculated permissions for a given role. + public DiscordPermissions PermissionsFor(DiscordRole role) + { + if (this.IsThread) + { + return this.Parent.PermissionsFor(role); + } + + if (this.IsPrivate || this.Guild is null) + { + return DiscordPermissions.None; + } + + if (role.guild_id != this.Guild.Id) + { + throw new ArgumentException("Given role does not belong to this channel's guild."); + } + + DiscordPermissions perms; + + // assign @everyone permissions + DiscordRole everyoneRole = this.Guild.EveryoneRole; + perms = everyoneRole.Permissions; + + // add role permissions + perms |= role.Permissions; + + // Administrator grants all permissions and cannot be overridden + if (perms.HasPermission(DiscordPermission.Administrator)) + { + return DiscordPermissions.All; + } + + // channel overrides for the @everyone role + DiscordOverwrite? everyoneRoleOverwrites = this.permissionOverwrites.FirstOrDefault(xo => xo.Id == everyoneRole.Id); + if (everyoneRoleOverwrites is not null) + { + // assign channel permission overwrites for the role (explicit deny) + perms &= ~everyoneRoleOverwrites.Denied; + + // assign channel permission overwrites for the role (explicit allow) + perms |= everyoneRoleOverwrites.Allowed; + } + + // channel overrides for the role + DiscordOverwrite? roleOverwrites = this.permissionOverwrites.FirstOrDefault(xo => xo.Id == role.Id); + if (roleOverwrites is null) + { + return perms; + } + + DiscordPermissions roleDenied = roleOverwrites.Denied; + + if (everyoneRoleOverwrites is not null) + { + roleDenied &= ~everyoneRoleOverwrites.Allowed; + } + + // assign channel permission overwrites for the role (explicit deny) + perms &= ~roleDenied; + + // assign channel permission overwrites for the role (explicit allow) + perms |= roleOverwrites.Allowed; + + return perms; + } + + /// + /// Returns a string representation of this channel. + /// + /// String representation of this channel. + public override string ToString() + { +#pragma warning disable IDE0046 // we don't want this to become a double ternary + if (this.Type == DiscordChannelType.Category) + { + return $"Channel Category {this.Name} ({this.Id})"; + } + + return this.Type is DiscordChannelType.Text or DiscordChannelType.News + ? $"Channel #{this.Name} ({this.Id})" + : !string.IsNullOrWhiteSpace(this.Name) ? $"Channel {this.Name} ({this.Id})" : $"Channel {this.Id}"; +#pragma warning restore IDE0046 + } + + #region ThreadMethods + + /// + /// Creates a new thread within this channel from the given message. + /// + /// Message to create the thread from. + /// The name of the thread. + /// The auto archive duration of the thread. + /// Reason for audit logs. + /// The created thread. + /// Thrown when the channel or message does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task CreateThreadAsync(DiscordMessage message, string name, DiscordAutoArchiveDuration archiveAfter, string reason = null) + { + if (this.Type is not DiscordChannelType.Text and not DiscordChannelType.News) + { + throw new ArgumentException("Threads can only be created within text or news channels."); + } + else if (message.ChannelId != this.Id) + { + throw new ArgumentException("You must use a message from this channel to create a thread."); + } + + DiscordThreadChannel threadChannel = await this.Discord.ApiClient.CreateThreadFromMessageAsync(this.Id, message.Id, name, archiveAfter, reason); + this.Guild.threads.AddOrUpdate(threadChannel.Id, threadChannel, (_, _) => threadChannel); + return threadChannel; + } + + /// + /// Creates a new thread within this channel. + /// + /// The name of the thread. + /// The auto archive duration of the thread. + /// The type of thread to create, either a public, news or, private thread. + /// Reason for audit logs. + /// The created thread. + /// Thrown when the channel or message does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task CreateThreadAsync(string name, DiscordAutoArchiveDuration archiveAfter, DiscordChannelType threadType, string reason = null) + { + if (this.Type is not DiscordChannelType.Text and not DiscordChannelType.News) + { + throw new InvalidOperationException("Threads can only be created within text or news channels."); + } + else if (this.Type != DiscordChannelType.News && threadType == DiscordChannelType.NewsThread) + { + throw new InvalidOperationException("News threads can only be created within a news channels."); + } + else if (threadType is not DiscordChannelType.PublicThread and not DiscordChannelType.PrivateThread and not DiscordChannelType.NewsThread) + { + throw new ArgumentException("Given channel type for creating a thread is not a valid type of thread."); + } + + DiscordThreadChannel threadChannel = await this.Discord.ApiClient.CreateThreadAsync(this.Id, name, archiveAfter, threadType, reason); + this.Guild.threads.AddOrUpdate(threadChannel.Id, threadChannel, (_, _) => threadChannel); + return threadChannel; + } + + #endregion + + #endregion + + /// + /// Checks whether this is equal to another object. + /// + /// Object to compare to. + /// Whether the object is equal to this . + public override bool Equals(object obj) => Equals(obj as DiscordChannel); + + /// + /// Checks whether this is equal to another . + /// + /// to compare to. + /// Whether the is equal to this . + public bool Equals(DiscordChannel e) => e is not null && (ReferenceEquals(this, e) || this.Id == e.Id); + + /// + /// Gets the hash code for this . + /// + /// The hash code for this . + public override int GetHashCode() => this.Id.GetHashCode(); + + /// + /// Gets whether the two objects are equal. + /// + /// First channel to compare. + /// Second channel to compare. + /// Whether the two channels are equal. + public static bool operator ==(DiscordChannel e1, DiscordChannel e2) + { + object? o1 = e1; + object? o2 = e2; + + return (o1 != null || o2 == null) && (o1 == null || o2 != null) && ((o1 == null && o2 == null) || e1.Id == e2.Id); + } + + /// + /// Gets whether the two objects are not equal. + /// + /// First channel to compare. + /// Second channel to compare. + /// Whether the two channels are not equal. + public static bool operator !=(DiscordChannel e1, DiscordChannel e2) + => !(e1 == e2); +} diff --git a/DSharpPlus/Entities/Channel/DiscordChannelFlags.cs b/DSharpPlus/Entities/Channel/DiscordChannelFlags.cs index f7f05f5621..fb6c8d1df2 100644 --- a/DSharpPlus/Entities/Channel/DiscordChannelFlags.cs +++ b/DSharpPlus/Entities/Channel/DiscordChannelFlags.cs @@ -1,17 +1,17 @@ -using System; - -namespace DSharpPlus.Entities; - -[Flags] -public enum DiscordChannelFlags -{ - /// - /// The channel is pinned. - /// - Pinned = 1 << 1, - - /// - /// The [forum] channel requires tags to be applied. - /// - RequiresTag = 1 << 4 -} +using System; + +namespace DSharpPlus.Entities; + +[Flags] +public enum DiscordChannelFlags +{ + /// + /// The channel is pinned. + /// + Pinned = 1 << 1, + + /// + /// The [forum] channel requires tags to be applied. + /// + RequiresTag = 1 << 4 +} diff --git a/DSharpPlus/Entities/Channel/DiscordChannelType.cs b/DSharpPlus/Entities/Channel/DiscordChannelType.cs index 7c3f04c21f..58c0f0d18e 100644 --- a/DSharpPlus/Entities/Channel/DiscordChannelType.cs +++ b/DSharpPlus/Entities/Channel/DiscordChannelType.cs @@ -1,78 +1,78 @@ -namespace DSharpPlus.Entities; - - -/// -/// Represents a channel's type. -/// -public enum DiscordChannelType : int -{ - /// - /// Indicates that this is a text channel. - /// - Text = 0, - - /// - /// Indicates that this is a private channel. - /// - Private = 1, - - /// - /// Indicates that this is a voice channel. - /// - Voice = 2, - - /// - /// Indicates that this is a group direct message channel. - /// - Group = 3, - - /// - /// Indicates that this is a channel category. - /// - Category = 4, - - /// - /// Indicates that this is a news channel. - /// - News = 5, - - /// - /// Indicates that this is a thread within a news channel. - /// - NewsThread = 10, - - /// - /// Indicates that this is a public thread within a channel. - /// - PublicThread = 11, - - /// - /// Indicates that this is a private thread within a channel. - /// - PrivateThread = 12, - - /// - /// Indicates that this is a stage channel. - /// - Stage = 13, - - /// - /// Indicates that this is a directory channel. - /// - Directory = 14, - - /// - /// Indicates that this is a forum channel. - /// - GuildForum = 15, - - /// - /// Indicates that this channel is a guild media channel and can only contain threads. Similar to GUILD_FORUM channels. - /// - GuildMedia = 16, - - /// - /// Indicates unknown channel type. - /// - Unknown = int.MaxValue -} +namespace DSharpPlus.Entities; + + +/// +/// Represents a channel's type. +/// +public enum DiscordChannelType : int +{ + /// + /// Indicates that this is a text channel. + /// + Text = 0, + + /// + /// Indicates that this is a private channel. + /// + Private = 1, + + /// + /// Indicates that this is a voice channel. + /// + Voice = 2, + + /// + /// Indicates that this is a group direct message channel. + /// + Group = 3, + + /// + /// Indicates that this is a channel category. + /// + Category = 4, + + /// + /// Indicates that this is a news channel. + /// + News = 5, + + /// + /// Indicates that this is a thread within a news channel. + /// + NewsThread = 10, + + /// + /// Indicates that this is a public thread within a channel. + /// + PublicThread = 11, + + /// + /// Indicates that this is a private thread within a channel. + /// + PrivateThread = 12, + + /// + /// Indicates that this is a stage channel. + /// + Stage = 13, + + /// + /// Indicates that this is a directory channel. + /// + Directory = 14, + + /// + /// Indicates that this is a forum channel. + /// + GuildForum = 15, + + /// + /// Indicates that this channel is a guild media channel and can only contain threads. Similar to GUILD_FORUM channels. + /// + GuildMedia = 16, + + /// + /// Indicates unknown channel type. + /// + Unknown = int.MaxValue +} diff --git a/DSharpPlus/Entities/Channel/DiscordDmChannel.cs b/DSharpPlus/Entities/Channel/DiscordDmChannel.cs index 4bbaaa56b4..d7de43be81 100644 --- a/DSharpPlus/Entities/Channel/DiscordDmChannel.cs +++ b/DSharpPlus/Entities/Channel/DiscordDmChannel.cs @@ -1,66 +1,66 @@ -using System.Collections.Generic; -using System.Globalization; -using System.Threading.Tasks; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// -/// Represents a direct message channel. -/// -public class DiscordDmChannel : DiscordChannel -{ - /// - /// Gets the recipients of this direct message. - /// - [JsonProperty("recipients", NullValueHandling = NullValueHandling.Ignore)] - public IReadOnlyList Recipients { get; internal set; } - - /// - /// Gets the hash of this channel's icon. - /// - [JsonProperty("icon", NullValueHandling = NullValueHandling.Ignore)] - public string IconHash { get; internal set; } - - /// - /// Gets the ID of this direct message's creator. - /// - [JsonProperty("owner_id", NullValueHandling = NullValueHandling.Ignore)] - public ulong OwnerId { get; internal set; } - - /// - /// Gets the application ID of the direct message's creator if it a bot. - /// - [JsonProperty("application_id", NullValueHandling = NullValueHandling.Ignore)] - public ulong ApplicationId { get; internal set; } - - /// - /// Gets the URL of this channel's icon. - /// - [JsonIgnore] - public string IconUrl - => !string.IsNullOrWhiteSpace(this.IconHash) ? $"https://cdn.discordapp.com/channel-icons/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.IconHash}.png" : null; - - /// - /// Only use for Group DMs! Whitelisted bots only. Requires user's oauth2 access token - /// - /// The ID of the user to add. - /// The OAuth2 access token. - /// The nickname to give to the user. - /// 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 AddDmRecipientAsync(ulong user_id, string accesstoken, string nickname) - => await this.Discord.ApiClient.AddGroupDmRecipientAsync(this.Id, user_id, accesstoken, nickname); - - /// - /// Only use for Group DMs! - /// - /// The ID of the User to remove. - /// 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 RemoveDmRecipientAsync(ulong user_id) - => await this.Discord.ApiClient.RemoveGroupDmRecipientAsync(this.Id, user_id); -} +using System.Collections.Generic; +using System.Globalization; +using System.Threading.Tasks; +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// +/// Represents a direct message channel. +/// +public class DiscordDmChannel : DiscordChannel +{ + /// + /// Gets the recipients of this direct message. + /// + [JsonProperty("recipients", NullValueHandling = NullValueHandling.Ignore)] + public IReadOnlyList Recipients { get; internal set; } + + /// + /// Gets the hash of this channel's icon. + /// + [JsonProperty("icon", NullValueHandling = NullValueHandling.Ignore)] + public string IconHash { get; internal set; } + + /// + /// Gets the ID of this direct message's creator. + /// + [JsonProperty("owner_id", NullValueHandling = NullValueHandling.Ignore)] + public ulong OwnerId { get; internal set; } + + /// + /// Gets the application ID of the direct message's creator if it a bot. + /// + [JsonProperty("application_id", NullValueHandling = NullValueHandling.Ignore)] + public ulong ApplicationId { get; internal set; } + + /// + /// Gets the URL of this channel's icon. + /// + [JsonIgnore] + public string IconUrl + => !string.IsNullOrWhiteSpace(this.IconHash) ? $"https://cdn.discordapp.com/channel-icons/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.IconHash}.png" : null; + + /// + /// Only use for Group DMs! Whitelisted bots only. Requires user's oauth2 access token + /// + /// The ID of the user to add. + /// The OAuth2 access token. + /// The nickname to give to the user. + /// 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 AddDmRecipientAsync(ulong user_id, string accesstoken, string nickname) + => await this.Discord.ApiClient.AddGroupDmRecipientAsync(this.Id, user_id, accesstoken, nickname); + + /// + /// Only use for Group DMs! + /// + /// The ID of the User to remove. + /// 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 RemoveDmRecipientAsync(ulong user_id) + => await this.Discord.ApiClient.RemoveGroupDmRecipientAsync(this.Id, user_id); +} diff --git a/DSharpPlus/Entities/Channel/DiscordFollowedChannel.cs b/DSharpPlus/Entities/Channel/DiscordFollowedChannel.cs index 34f280c617..fdeaa8980e 100644 --- a/DSharpPlus/Entities/Channel/DiscordFollowedChannel.cs +++ b/DSharpPlus/Entities/Channel/DiscordFollowedChannel.cs @@ -1,21 +1,21 @@ -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents a followed channel. -/// -public class DiscordFollowedChannel -{ - /// - /// Gets the ID of the channel following the announcement channel. - /// - [JsonProperty("channel_id", NullValueHandling = NullValueHandling.Ignore)] - public ulong ChannelId { get; internal set; } - - /// - /// Gets the ID of the webhook that posts crossposted messages to the channel. - /// - [JsonProperty("webhook_id", NullValueHandling = NullValueHandling.Ignore)] - public ulong WebhookId { get; internal set; } -} +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Represents a followed channel. +/// +public class DiscordFollowedChannel +{ + /// + /// Gets the ID of the channel following the announcement channel. + /// + [JsonProperty("channel_id", NullValueHandling = NullValueHandling.Ignore)] + public ulong ChannelId { get; internal set; } + + /// + /// Gets the ID of the webhook that posts crossposted messages to the channel. + /// + [JsonProperty("webhook_id", NullValueHandling = NullValueHandling.Ignore)] + public ulong WebhookId { get; internal set; } +} diff --git a/DSharpPlus/Entities/Channel/DiscordOverwriteType.cs b/DSharpPlus/Entities/Channel/DiscordOverwriteType.cs index 05fd64513f..77f3cedeae 100644 --- a/DSharpPlus/Entities/Channel/DiscordOverwriteType.cs +++ b/DSharpPlus/Entities/Channel/DiscordOverwriteType.cs @@ -1,23 +1,23 @@ -namespace DSharpPlus.Entities; - - -/// -/// Represents a channel permissions overwrite's type. -/// -public enum DiscordOverwriteType : int -{ - /// - /// The overwrite type is not currently defined. - /// - None = -1, - - /// - /// Specifies that this overwrite applies to a role. - /// - Role = 0, - - /// - /// Specifies that this overwrite applies to a member. - /// - Member = 1 -} +namespace DSharpPlus.Entities; + + +/// +/// Represents a channel permissions overwrite's type. +/// +public enum DiscordOverwriteType : int +{ + /// + /// The overwrite type is not currently defined. + /// + None = -1, + + /// + /// Specifies that this overwrite applies to a role. + /// + Role = 0, + + /// + /// Specifies that this overwrite applies to a member. + /// + Member = 1 +} diff --git a/DSharpPlus/Entities/Channel/DiscordPartialChannel.cs b/DSharpPlus/Entities/Channel/DiscordPartialChannel.cs index 90d54a1368..761febce42 100644 --- a/DSharpPlus/Entities/Channel/DiscordPartialChannel.cs +++ b/DSharpPlus/Entities/Channel/DiscordPartialChannel.cs @@ -1,125 +1,125 @@ -using System; -using System.Collections.Generic; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// A partial channel object -/// -/// -/// Partial objects can have any or no data, but the ID is always returned -/// -public class DiscordPartialChannel -{ - /// - /// Gets the ID of this object. - /// - [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)] - public ulong Id { get; internal set; } - - /// - /// Gets ID of the guild to which this channel belongs. - /// - [JsonProperty("guild_id", NullValueHandling = NullValueHandling.Ignore)] - public ulong? GuildId { get; internal set; } - - /// - /// Gets ID of the category that contains this channel. For threads, gets the ID of the channel this thread was created in. - /// - [JsonProperty("parent_id", NullValueHandling = NullValueHandling.Include)] - public ulong? ParentId { get; internal set; } - - /// - /// Gets the name of this channel. - /// - [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] - public string? Name { get; internal set; } - - /// - /// Gets the type of this channel. - /// - [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] - public DiscordChannelType? Type { get; internal set; } - - /// - /// Gets the position of this channel. - /// - [JsonProperty("position", NullValueHandling = NullValueHandling.Ignore)] - public int? Position { get; internal set; } - - /// - /// Gets a list of permission overwrites - /// - [JsonProperty("permission_overwrites", NullValueHandling = NullValueHandling.Ignore)] - public List? PermissionOverwrites = []; - - /// - /// Gets the channel's topic. This is applicable to text channels only. - /// - [JsonProperty("topic", NullValueHandling = NullValueHandling.Ignore)] - public string? Topic { get; internal set; } - - /// - /// Gets the ID of the last message sent in this channel. This is applicable to text channels only. - /// - /// - /// For forum posts, this ID may point to an invalid message (e.g. the OP deleted the initial forum message). - /// - [JsonProperty("last_message_id", NullValueHandling = NullValueHandling.Ignore)] - public ulong? LastMessageId { get; internal set; } - - /// - /// Gets this channel's bitrate. This is applicable to voice channels only. - /// - [JsonProperty("bitrate", NullValueHandling = NullValueHandling.Ignore)] - public int? Bitrate { get; internal set; } - - /// - /// Gets this channel's user limit. This is applicable to voice channels only. - /// - [JsonProperty("user_limit", NullValueHandling = NullValueHandling.Ignore)] - public int? UserLimit { get; internal set; } - - /// - /// Gets the slow mode delay configured for this channel. - /// All bots, as well as users with or permissions in the channel are exempt from slow mode. - /// - [JsonProperty("rate_limit_per_user", NullValueHandling = NullValueHandling.Ignore)] - public int? PerUserRateLimit { get; internal set; } - - /// - /// Gets this channel's video quality mode. This is applicable to voice channels only. - /// - [JsonProperty("video_quality_mode", NullValueHandling = NullValueHandling.Ignore)] - public DiscordVideoQualityMode? QualityMode { get; internal set; } - - /// - /// Gets when the last pinned message was pinned. - /// - [JsonProperty("last_pin_timestamp", NullValueHandling = NullValueHandling.Ignore)] - public DateTimeOffset? LastPinTimestamp { get; internal set; } - - /// - /// Gets whether this channel is an NSFW channel. - /// - [JsonProperty("nsfw", NullValueHandling = NullValueHandling.Ignore)] - public bool? IsNsfw { get; internal set; } - - /// - /// Get the name of the voice region - /// - [JsonProperty("rtc_region", NullValueHandling = NullValueHandling.Ignore)] - public string? RtcRegionId { get; set; } - - /// - /// Gets the permissions of the user who invoked the command in this channel. - /// Only sent on the resolved channels of interaction responses for application commands. - /// - [JsonProperty("permissions", NullValueHandling = NullValueHandling.Ignore)] - public DiscordPermissions? UserPermissions { get; internal set; } - - internal DiscordPartialChannel() - { - } -} +using System; +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// A partial channel object +/// +/// +/// Partial objects can have any or no data, but the ID is always returned +/// +public class DiscordPartialChannel +{ + /// + /// Gets the ID of this object. + /// + [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)] + public ulong Id { get; internal set; } + + /// + /// Gets ID of the guild to which this channel belongs. + /// + [JsonProperty("guild_id", NullValueHandling = NullValueHandling.Ignore)] + public ulong? GuildId { get; internal set; } + + /// + /// Gets ID of the category that contains this channel. For threads, gets the ID of the channel this thread was created in. + /// + [JsonProperty("parent_id", NullValueHandling = NullValueHandling.Include)] + public ulong? ParentId { get; internal set; } + + /// + /// Gets the name of this channel. + /// + [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] + public string? Name { get; internal set; } + + /// + /// Gets the type of this channel. + /// + [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] + public DiscordChannelType? Type { get; internal set; } + + /// + /// Gets the position of this channel. + /// + [JsonProperty("position", NullValueHandling = NullValueHandling.Ignore)] + public int? Position { get; internal set; } + + /// + /// Gets a list of permission overwrites + /// + [JsonProperty("permission_overwrites", NullValueHandling = NullValueHandling.Ignore)] + public List? PermissionOverwrites = []; + + /// + /// Gets the channel's topic. This is applicable to text channels only. + /// + [JsonProperty("topic", NullValueHandling = NullValueHandling.Ignore)] + public string? Topic { get; internal set; } + + /// + /// Gets the ID of the last message sent in this channel. This is applicable to text channels only. + /// + /// + /// For forum posts, this ID may point to an invalid message (e.g. the OP deleted the initial forum message). + /// + [JsonProperty("last_message_id", NullValueHandling = NullValueHandling.Ignore)] + public ulong? LastMessageId { get; internal set; } + + /// + /// Gets this channel's bitrate. This is applicable to voice channels only. + /// + [JsonProperty("bitrate", NullValueHandling = NullValueHandling.Ignore)] + public int? Bitrate { get; internal set; } + + /// + /// Gets this channel's user limit. This is applicable to voice channels only. + /// + [JsonProperty("user_limit", NullValueHandling = NullValueHandling.Ignore)] + public int? UserLimit { get; internal set; } + + /// + /// Gets the slow mode delay configured for this channel. + /// All bots, as well as users with or permissions in the channel are exempt from slow mode. + /// + [JsonProperty("rate_limit_per_user", NullValueHandling = NullValueHandling.Ignore)] + public int? PerUserRateLimit { get; internal set; } + + /// + /// Gets this channel's video quality mode. This is applicable to voice channels only. + /// + [JsonProperty("video_quality_mode", NullValueHandling = NullValueHandling.Ignore)] + public DiscordVideoQualityMode? QualityMode { get; internal set; } + + /// + /// Gets when the last pinned message was pinned. + /// + [JsonProperty("last_pin_timestamp", NullValueHandling = NullValueHandling.Ignore)] + public DateTimeOffset? LastPinTimestamp { get; internal set; } + + /// + /// Gets whether this channel is an NSFW channel. + /// + [JsonProperty("nsfw", NullValueHandling = NullValueHandling.Ignore)] + public bool? IsNsfw { get; internal set; } + + /// + /// Get the name of the voice region + /// + [JsonProperty("rtc_region", NullValueHandling = NullValueHandling.Ignore)] + public string? RtcRegionId { get; set; } + + /// + /// Gets the permissions of the user who invoked the command in this channel. + /// Only sent on the resolved channels of interaction responses for application commands. + /// + [JsonProperty("permissions", NullValueHandling = NullValueHandling.Ignore)] + public DiscordPermissions? UserPermissions { get; internal set; } + + internal DiscordPartialChannel() + { + } +} diff --git a/DSharpPlus/Entities/Channel/DiscordSystemChannelFlags.cs b/DSharpPlus/Entities/Channel/DiscordSystemChannelFlags.cs index 192879d2a5..f11601d518 100644 --- a/DSharpPlus/Entities/Channel/DiscordSystemChannelFlags.cs +++ b/DSharpPlus/Entities/Channel/DiscordSystemChannelFlags.cs @@ -1,41 +1,41 @@ -using System; - -namespace DSharpPlus.Entities; - -public static class SystemChannelFlagsExtension -{ - /// - /// Calculates whether these system channel flags contain a specific flag. - /// - /// The existing flags. - /// The flag to search for. - /// - public static bool HasSystemChannelFlag(this DiscordSystemChannelFlags baseFlags, DiscordSystemChannelFlags flag) => (baseFlags & flag) == flag; -} - -/// -/// Represents settings for a guild's system channel. -/// -[Flags] -public enum DiscordSystemChannelFlags -{ - /// - /// Member join messages are disabled. - /// - SuppressJoinNotifications = 1 << 0, - - /// - /// Server boost messages are disabled. - /// - SuppressPremiumSubscriptions = 1 << 1, - - /// - /// Server setup tips are disabled. - /// - SuppressGuildReminderNotifications = 1 << 2, - - /// - /// Server join messages suppress the wave sticker button. - /// - SuppressJoinNotificationReplies = 1 << 3 -} +using System; + +namespace DSharpPlus.Entities; + +public static class SystemChannelFlagsExtension +{ + /// + /// Calculates whether these system channel flags contain a specific flag. + /// + /// The existing flags. + /// The flag to search for. + /// + public static bool HasSystemChannelFlag(this DiscordSystemChannelFlags baseFlags, DiscordSystemChannelFlags flag) => (baseFlags & flag) == flag; +} + +/// +/// Represents settings for a guild's system channel. +/// +[Flags] +public enum DiscordSystemChannelFlags +{ + /// + /// Member join messages are disabled. + /// + SuppressJoinNotifications = 1 << 0, + + /// + /// Server boost messages are disabled. + /// + SuppressPremiumSubscriptions = 1 << 1, + + /// + /// Server setup tips are disabled. + /// + SuppressGuildReminderNotifications = 1 << 2, + + /// + /// Server join messages suppress the wave sticker button. + /// + SuppressJoinNotificationReplies = 1 << 3 +} diff --git a/DSharpPlus/Entities/Channel/DiscordVideoQualityMode.cs b/DSharpPlus/Entities/Channel/DiscordVideoQualityMode.cs index 6b68c2e85f..1b360691fe 100644 --- a/DSharpPlus/Entities/Channel/DiscordVideoQualityMode.cs +++ b/DSharpPlus/Entities/Channel/DiscordVideoQualityMode.cs @@ -1,18 +1,18 @@ -namespace DSharpPlus.Entities; - - -/// -/// Represents the video quality mode of a voice channel. This is applicable to voice channels only. -/// -public enum DiscordVideoQualityMode : int -{ - /// - /// Indicates that the video quality is automatically chosen, or there is no value set. - /// - Auto = 1, - - /// - /// Indicates that the video quality is 720p. - /// - Full = 2, -} +namespace DSharpPlus.Entities; + + +/// +/// Represents the video quality mode of a voice channel. This is applicable to voice channels only. +/// +public enum DiscordVideoQualityMode : int +{ + /// + /// Indicates that the video quality is automatically chosen, or there is no value set. + /// + Auto = 1, + + /// + /// Indicates that the video quality is 720p. + /// + Full = 2, +} diff --git a/DSharpPlus/Entities/Channel/Message/DiscordAttachment.cs b/DSharpPlus/Entities/Channel/Message/DiscordAttachment.cs index a1e02aef3f..01f0955cd2 100644 --- a/DSharpPlus/Entities/Channel/Message/DiscordAttachment.cs +++ b/DSharpPlus/Entities/Channel/Message/DiscordAttachment.cs @@ -1,59 +1,59 @@ -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents an attachment for a message. -/// -public class DiscordAttachment : SnowflakeObject -{ - /// - /// Gets the name of the file. - /// - [JsonProperty("filename", NullValueHandling = NullValueHandling.Ignore)] - public string? FileName { get; internal set; } - - /// - /// Gets the file size in bytes. - /// - [JsonProperty("size", NullValueHandling = NullValueHandling.Ignore)] - public int FileSize { get; internal set; } - - /// - /// Gets the media, or MIME, type of the file. - /// - [JsonProperty("content_type", NullValueHandling = NullValueHandling.Ignore)] - public string? MediaType { get; internal set; } - - /// - /// Gets the URL of the file. - /// - [JsonProperty("url", NullValueHandling = NullValueHandling.Ignore)] - public string? Url { get; internal set; } - - /// - /// Gets the proxied URL of the file. - /// - [JsonProperty("proxy_url", NullValueHandling = NullValueHandling.Ignore)] - public string? ProxyUrl { get; internal set; } - - /// - /// Gets the height. Applicable only if the attachment is an image. - /// - [JsonProperty("height", NullValueHandling = NullValueHandling.Ignore)] - public int? Height { get; internal set; } - - /// - /// Gets the width. Applicable only if the attachment is an image. - /// - [JsonProperty("width", NullValueHandling = NullValueHandling.Ignore)] - public int? Width { get; internal set; } - - /// - /// Gets whether this attachment is ephemeral. - /// - [JsonProperty("ephemeral", NullValueHandling = NullValueHandling.Ignore)] - public bool? Ephemeral { get; internal set; } - - internal DiscordAttachment() { } -} +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Represents an attachment for a message. +/// +public class DiscordAttachment : SnowflakeObject +{ + /// + /// Gets the name of the file. + /// + [JsonProperty("filename", NullValueHandling = NullValueHandling.Ignore)] + public string? FileName { get; internal set; } + + /// + /// Gets the file size in bytes. + /// + [JsonProperty("size", NullValueHandling = NullValueHandling.Ignore)] + public int FileSize { get; internal set; } + + /// + /// Gets the media, or MIME, type of the file. + /// + [JsonProperty("content_type", NullValueHandling = NullValueHandling.Ignore)] + public string? MediaType { get; internal set; } + + /// + /// Gets the URL of the file. + /// + [JsonProperty("url", NullValueHandling = NullValueHandling.Ignore)] + public string? Url { get; internal set; } + + /// + /// Gets the proxied URL of the file. + /// + [JsonProperty("proxy_url", NullValueHandling = NullValueHandling.Ignore)] + public string? ProxyUrl { get; internal set; } + + /// + /// Gets the height. Applicable only if the attachment is an image. + /// + [JsonProperty("height", NullValueHandling = NullValueHandling.Ignore)] + public int? Height { get; internal set; } + + /// + /// Gets the width. Applicable only if the attachment is an image. + /// + [JsonProperty("width", NullValueHandling = NullValueHandling.Ignore)] + public int? Width { get; internal set; } + + /// + /// Gets whether this attachment is ephemeral. + /// + [JsonProperty("ephemeral", NullValueHandling = NullValueHandling.Ignore)] + public bool? Ephemeral { get; internal set; } + + internal DiscordAttachment() { } +} diff --git a/DSharpPlus/Entities/Channel/Message/DiscordMentions.cs b/DSharpPlus/Entities/Channel/Message/DiscordMentions.cs index a281607f8f..e54cd849c2 100644 --- a/DSharpPlus/Entities/Channel/Message/DiscordMentions.cs +++ b/DSharpPlus/Entities/Channel/Message/DiscordMentions.cs @@ -1,121 +1,121 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Handles mentionables -/// -internal class DiscordMentions -{ - //https://discord.com/developers/docs/resources/channel#allowed-mentions-object - - private const string ParseUsers = "users"; - private const string ParseRoles = "roles"; - private const string ParseEveryone = "everyone"; - - /// - /// Collection roles to serialize - /// - [JsonProperty("roles", NullValueHandling = NullValueHandling.Ignore)] - public IEnumerable? Roles { get; } - - /// - /// Collection of users to serialize - /// - [JsonProperty("users", NullValueHandling = NullValueHandling.Ignore)] - public IEnumerable? Users { get; } - - /// - /// The values to be parsed - /// - [JsonProperty("parse", NullValueHandling = NullValueHandling.Ignore)] - public IEnumerable? Parse { get; } - - // WHY IS THERE NO DOCSTRING HERE - [JsonProperty("replied_user", NullValueHandling = NullValueHandling.Ignore)] - public bool? RepliedUser { get; } - - internal DiscordMentions(IEnumerable mentions, bool repliedUser = false) - { - //Null check just to be safe - if (mentions is null) - { - return; - } - - this.RepliedUser = repliedUser; - //If we have no item in our mentions, its likely to be a empty array. - // This is a special case were we want parse to be a empty array - // Doing this allows for "no parsing" - if (!mentions.Any()) - { - this.Parse = []; - return; - } - - //Prepare a list of allowed IDs. We will be adding to these IDs. - HashSet roles = []; - HashSet users = []; - HashSet parse = []; - - foreach (IMention m in mentions) - { - switch (m) - { - case UserMention u: - if (u.Id.HasValue) - { - users.Add(u.Id.Value); //We have a user ID so we will add them to the implicit - } - else - { - parse.Add(ParseUsers); //We have no ID, so let all users through - } - - break; - - case RoleMention r: - if (r.Id.HasValue) - { - roles.Add(r.Id.Value); //We have a role ID so we will add them to the implicit - } - else - { - parse.Add(ParseRoles); //We have role ID, so let all users through - } - - break; - - case EveryoneMention: - parse.Add(ParseEveryone); - break; - - case RepliedUserMention: - break; - - default: - throw new NotSupportedException($"The type {m.GetType()} is not supported in allowed mentions."); - } - } - - //Check the validity of each item. If it isn't in the explicit allow list and they have items, then add them. - if (!parse.Contains(ParseUsers) && users.Count > 0) - { - this.Users = users; - } - - if (!parse.Contains(ParseRoles) && roles.Count > 0) - { - this.Roles = roles; - } - - //If we have a empty parse array, we don't want to add it. - if (parse.Count > 0) - { - this.Parse = parse; - } - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Handles mentionables +/// +internal class DiscordMentions +{ + //https://discord.com/developers/docs/resources/channel#allowed-mentions-object + + private const string ParseUsers = "users"; + private const string ParseRoles = "roles"; + private const string ParseEveryone = "everyone"; + + /// + /// Collection roles to serialize + /// + [JsonProperty("roles", NullValueHandling = NullValueHandling.Ignore)] + public IEnumerable? Roles { get; } + + /// + /// Collection of users to serialize + /// + [JsonProperty("users", NullValueHandling = NullValueHandling.Ignore)] + public IEnumerable? Users { get; } + + /// + /// The values to be parsed + /// + [JsonProperty("parse", NullValueHandling = NullValueHandling.Ignore)] + public IEnumerable? Parse { get; } + + // WHY IS THERE NO DOCSTRING HERE + [JsonProperty("replied_user", NullValueHandling = NullValueHandling.Ignore)] + public bool? RepliedUser { get; } + + internal DiscordMentions(IEnumerable mentions, bool repliedUser = false) + { + //Null check just to be safe + if (mentions is null) + { + return; + } + + this.RepliedUser = repliedUser; + //If we have no item in our mentions, its likely to be a empty array. + // This is a special case were we want parse to be a empty array + // Doing this allows for "no parsing" + if (!mentions.Any()) + { + this.Parse = []; + return; + } + + //Prepare a list of allowed IDs. We will be adding to these IDs. + HashSet roles = []; + HashSet users = []; + HashSet parse = []; + + foreach (IMention m in mentions) + { + switch (m) + { + case UserMention u: + if (u.Id.HasValue) + { + users.Add(u.Id.Value); //We have a user ID so we will add them to the implicit + } + else + { + parse.Add(ParseUsers); //We have no ID, so let all users through + } + + break; + + case RoleMention r: + if (r.Id.HasValue) + { + roles.Add(r.Id.Value); //We have a role ID so we will add them to the implicit + } + else + { + parse.Add(ParseRoles); //We have role ID, so let all users through + } + + break; + + case EveryoneMention: + parse.Add(ParseEveryone); + break; + + case RepliedUserMention: + break; + + default: + throw new NotSupportedException($"The type {m.GetType()} is not supported in allowed mentions."); + } + } + + //Check the validity of each item. If it isn't in the explicit allow list and they have items, then add them. + if (!parse.Contains(ParseUsers) && users.Count > 0) + { + this.Users = users; + } + + if (!parse.Contains(ParseRoles) && roles.Count > 0) + { + this.Roles = roles; + } + + //If we have a empty parse array, we don't want to add it. + if (parse.Count > 0) + { + this.Parse = parse; + } + } +} diff --git a/DSharpPlus/Entities/Channel/Message/DiscordMessage.cs b/DSharpPlus/Entities/Channel/Message/DiscordMessage.cs index cf30a02570..5225997555 100644 --- a/DSharpPlus/Entities/Channel/Message/DiscordMessage.cs +++ b/DSharpPlus/Entities/Channel/Message/DiscordMessage.cs @@ -1,1009 +1,1009 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Threading; -using System.Threading.Tasks; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents a Discord text message. -/// -public class DiscordMessage : SnowflakeObject, IEquatable -{ - internal DiscordMessage() - { - - } - - internal DiscordMessage(DiscordMessage other) - : this() - { - this.Discord = other.Discord; - - this.attachments = new List(other.attachments); - this.embeds = new List(other.embeds); - - if (other.mentionedChannels is not null) - { - this.mentionedChannels = new List(other.mentionedChannels); - } - - if (other.mentionedRoles is not null) - { - this.mentionedRoles = new List(other.mentionedRoles); - } - - if (other.mentionedRoleIds is not null) - { - this.mentionedRoleIds = new List(other.mentionedRoleIds); - } - - this.mentionedUsers = new List(other.mentionedUsers); - this.reactions = new List(other.reactions); - this.stickers = new List(other.stickers); - - this.Author = other.Author; - this.ChannelId = other.ChannelId; - this.Content = other.Content; - this.EditedTimestamp = other.EditedTimestamp; - this.Id = other.Id; - this.IsTTS = other.IsTTS; - this.Poll = other.Poll; - this.MessageType = other.MessageType; - this.Pinned = other.Pinned; - this.Timestamp = other.Timestamp; - this.WebhookId = other.WebhookId; - this.ApplicationId = other.ApplicationId; - this.Components = other.Components; - } - - /// - /// Gets the channel in which the message was sent. - /// - [JsonIgnore] - public DiscordChannel? Channel - { - get - { - DiscordClient? client = this.Discord as DiscordClient; - - return client?.InternalGetCachedChannel(this.ChannelId, this.guildId) ?? - client?.InternalGetCachedThread(this.ChannelId, this.guildId) ?? this.channel; - } - internal set => this.channel = value; - } - - private DiscordChannel? channel; - - /// - /// 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 IReadOnlyList? Components { get; internal set; } - - /// - /// Gets the action rows this message was sent with - components holding buttons, selects and the likes. - /// - public IReadOnlyList? ComponentActionRows - => this.Components?.Where(x => x is DiscordActionRowComponent).Cast().ToList(); - - /// - /// 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. - /// - [JsonProperty("timestamp", NullValueHandling = NullValueHandling.Ignore)] - public DateTimeOffset Timestamp { get; set; } - - /// - /// Gets the message's edit timestamp. Will be null if the message was not edited. - /// - [JsonProperty("edited_timestamp", NullValueHandling = NullValueHandling.Ignore)] - public DateTimeOffset? EditedTimestamp { get; internal set; } - - /// - /// Gets whether this message was edited. - /// - [JsonIgnore] - public bool IsEdited => this.EditedTimestamp is not null; - - /// - /// 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.mentionedUsers; - - [JsonProperty("mentions", NullValueHandling = NullValueHandling.Ignore)] - internal List mentionedUsers = []; - - // 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.mentionedRoles; - - [JsonIgnore] - internal List mentionedRoles = []; - - [JsonProperty("mention_roles")] - internal List mentionedRoleIds = []; - - /// - /// Gets channels mentioned by this message. - /// - [JsonIgnore] - public IReadOnlyList MentionedChannels - => this.mentionedChannels; - - [JsonIgnore] - internal List mentionedChannels = []; - - /// - /// Gets files attached to this message. - /// - [JsonIgnore] - public IReadOnlyList Attachments - => this.attachments; - - [JsonProperty("attachments", NullValueHandling = NullValueHandling.Ignore)] - internal List attachments = []; - - /// - /// Gets embeds attached to this message. - /// - [JsonIgnore] - public IReadOnlyList Embeds - => this.embeds; - - [JsonProperty("embeds", NullValueHandling = NullValueHandling.Ignore)] - internal List embeds = []; - - /// - /// Gets reactions used on this message. - /// - [JsonIgnore] - public IReadOnlyList Reactions - => this.reactions; - - [JsonProperty("reactions", NullValueHandling = NullValueHandling.Ignore)] - internal List reactions = []; - - /* - /// - /// 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 DiscordMessageType? 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; } - - [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 DiscordMessageFlags? 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 - { - get - { - string gid = this.Channel is DiscordDmChannel ? "@me" : this.Channel?.GuildId?.ToString(CultureInfo.InvariantCulture) ?? "@me"; - string cid = this.ChannelId.ToString(CultureInfo.InvariantCulture); - string mid = this.Id.ToString(CultureInfo.InvariantCulture); - - return new Uri($"https://discord.com/channels/{gid}/{cid}/{mid}"); - } - } - - /// - /// Gets stickers for this message. - /// - [JsonIgnore] - public IReadOnlyList? Stickers - => this.stickers; - - [JsonProperty("sticker_items", NullValueHandling = NullValueHandling.Ignore)] - internal List stickers = []; - - [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 the message object for the referenced message - /// - [JsonProperty("message_snapshots", NullValueHandling = NullValueHandling.Ignore)] - public IReadOnlyList? MessageSnapshots { get; internal set; } - - /// - /// Gets the poll object for the message. - /// - [JsonProperty("poll", NullValueHandling = NullValueHandling.Ignore)] - public DiscordPoll? Poll { 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 id of the interaction application, if a response to an interaction. - /// - [JsonProperty("application_id", NullValueHandling = NullValueHandling.Ignore)] - public ulong? ApplicationId { get; internal set; } - - internal DiscordMessageReference InternalBuildMessageReference() - { - DiscordClient client = (DiscordClient)this.Discord; - ulong? guildId = this.internalReference?.GuildId; - ulong? channelId = this.internalReference?.ChannelId; - ulong? messageId = this.internalReference?.MessageId; - - DiscordMessageReference reference = new(); - - if (guildId.HasValue) - { - reference.Guild = client!.guilds.TryGetValue(guildId.Value, out DiscordGuild? g) - ? g - : new DiscordGuild - { - Id = guildId.Value, - Discord = client - }; - } - - DiscordChannel? channel = client.InternalGetCachedChannel(channelId!.Value, this.guildId); - - reference.Type = this.internalReference?.Type; - - if (channel is 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(messageId!.Value, out DiscordMessage? msg)) - { - reference.Message = msg; - } - else - { - reference.Message = new DiscordMessage - { - ChannelId = this.ChannelId, - Discord = client - }; - - if (messageId.HasValue) - { - reference.Message.Id = messageId.Value; - } - } - - return reference; - } - - private IMention[] GetMentions() - { - List mentions = []; - - if (this.ReferencedMessage is not null && this.mentionedUsers.Any(r => r.Id == this.ReferencedMessage.Author?.Id)) - { - mentions.Add(new RepliedUserMention()); // Return null to allow all mentions - } - - if ((this.mentionedUsers?.Count ?? 0) > 0) - { - mentions.AddRange(this.mentionedUsers!.Select(m => (IMention)new UserMention(m))); - } - - if ((this.mentionedRoleIds?.Count ?? 0) > 0) - { - mentions.AddRange(this.mentionedRoleIds!.Select(r => (IMention)new RoleMention(r))); - } - - return [.. mentions]; - } - - internal void PopulateMentions() - { - DiscordGuild? guild = this.Channel?.Guild; - this.mentionedUsers ??= []; - this.mentionedRoles ??= []; - this.mentionedChannels ??= []; - - // Create a Hashset that will replace 'this.mentionedUsers'. - HashSet mentionedUsers = new(new DiscordUserComparer()); - - foreach (DiscordUser usr in this.mentionedUsers) - { - // Assign the Discord instance and update the user cache. - usr.Discord = this.Discord; - this.Discord.UpdateUserCache(usr); - - if (guild is not null && usr is not DiscordMember && guild.members.TryGetValue(usr.Id, out DiscordMember? cachedMember)) - { - // If the message is from a guild, but a discord member isn't provided, try to get the discord member out of guild members cache. - mentionedUsers.Add(cachedMember); - } - else - { - // Add provided user otherwise. - mentionedUsers.Add(usr); - } - } - - // Replace 'this.mentionedUsers'. - this.mentionedUsers = [.. mentionedUsers]; - - if (guild is not null && !string.IsNullOrWhiteSpace(this.Content)) - { - this.mentionedChannels = this.mentionedChannels.Union(Utilities.GetChannelMentions(this).Select(guild.GetChannel)).ToList(); - this.mentionedRoles = this.mentionedRoles.Union(this.mentionedRoleIds.Select(x => guild.roles.GetValueOrDefault(x)!)).ToList(); - - //uncomment if this breaks - //mentionedUsers.UnionWith(Utilities.GetUserMentions(this).Select(this.Discord.GetCachedOrEmptyUserInternal)); - //this.mentionedRoles = this.mentionedRoles.Union(Utilities.GetRoleMentions(this).Select(xid => guild.GetRole(xid))).ToList(); - } - } - - /// - /// Searches the components on this message for an aggregate of all components of a certain type. - /// - public IReadOnlyList FilterComponents() - where T : DiscordComponent - { - List components = []; - - foreach (DiscordComponent component in this.Components) - { - if (component is DiscordActionRowComponent actionRowComponent) - { - foreach (DiscordComponent subComponent in actionRowComponent.Components) - { - if (subComponent is T filteredComponent) - { - components.Add(filteredComponent); - } - } - } - else if (component is T filteredComponent) - { - components.Add(filteredComponent); - } - } - - return components; - } - - /// - /// 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 async Task ModifyAsync(Optional content) - => await this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, content, default, GetMentions(), default, [], null, 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 async Task ModifyAsync(Optional embed = default) - => await this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, default, embed.HasValue ? [embed.Value] : Array.Empty(), GetMentions(), default, [], null, 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 async Task ModifyAsync(Optional content, Optional embed = default) - => await this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, content, embed.HasValue ? [embed.Value] : Array.Empty(), GetMentions(), default, [], null, 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 async Task ModifyAsync(Optional content, Optional> embeds = default) - => await this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, content, embeds, GetMentions(), default, [], null, default); - - /// - /// Edits the message. - /// - /// The builder of the message to edit. - /// Whether to suppress embeds on the message. - /// Attached files to keep. - /// - /// 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, bool suppressEmbeds = false, IEnumerable? attachments = default) - { - builder.Validate(); - return await this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, builder.Content, new Optional>(builder.Embeds), builder.mentions, builder.Components, builder.Files, suppressEmbeds ? DiscordMessageFlags.SuppressedEmbeds : null, attachments); - } - - /// - /// Edits the message. - /// - /// The builder of the message to edit. - /// Whether to suppress embeds on the message. - /// Attached files to keep. - /// - /// 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, bool suppressEmbeds = false, IEnumerable? attachments = default) - { - DiscordMessageBuilder builder = new(this); - action(builder); - builder.Validate(); - return await this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, builder.Content, new Optional>(builder.Embeds), builder.mentions, builder.Components, builder.Files, suppressEmbeds ? DiscordMessageFlags.SuppressedEmbeds : null, attachments); - } - - /// - /// Modifies the visibility of embeds in this message. - /// - /// Whether to hide all embeds. - /// - /// 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 async Task ModifyEmbedSuppressionAsync(bool hideEmbeds) - => await this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, default, default, default, default, [], hideEmbeds ? DiscordMessageFlags.SuppressedEmbeds : null, default); - - /// - /// 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 async Task DeleteAsync(string? reason = null) - => await this.Discord.ApiClient.DeleteMessageAsync(this.ChannelId, this.Id, reason); - - /// - /// 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 async Task PinAsync() - => await 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 async Task UnpinAsync() - => await 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 async Task RespondAsync(string content) - => await this.Discord.ApiClient.CreateMessageAsync(this.ChannelId, content, null, replyMessageId: this.Id, mentionReply: false, failOnInvalidReply: false, suppressNotifications: 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 async Task RespondAsync(DiscordEmbed embed) - => await this.Discord.ApiClient.CreateMessageAsync(this.ChannelId, null, embed != null ? new[] { embed } : null, replyMessageId: this.Id, mentionReply: false, failOnInvalidReply: false, suppressNotifications: 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 async Task RespondAsync(string content, DiscordEmbed embed) - => await this.Discord.ApiClient.CreateMessageAsync(this.ChannelId, content, embed != null ? new[] { embed } : null, replyMessageId: this.Id, mentionReply: false, failOnInvalidReply: false, suppressNotifications: 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 async Task RespondAsync(DiscordMessageBuilder builder) - => await 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 async Task RespondAsync(Action action) - { - DiscordMessageBuilder builder = new(); - action(builder); - return await this.Discord.ApiClient.CreateMessageAsync(this.ChannelId, builder.WithReply(this.Id, mention: false, failOnInvalidReply: false)); - } - - /// - /// Creates a new thread within this channel from this message. - /// - /// The name of the thread. - /// The auto archive duration of the thread. Three and seven day archive options are locked behind level 2 and level 3 server boosts respectively. - /// Reason for audit logs. - /// The created thread. - /// 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 async Task CreateThreadAsync(string name, DiscordAutoArchiveDuration archiveAfter, string? reason = null) => this.Channel?.Type is not DiscordChannelType.Text and not DiscordChannelType.News - ? throw new InvalidOperationException("Threads can only be created within text or news channels.") - : await this.Discord.ApiClient.CreateThreadFromMessageAsync(this.Channel.Id, this.Id, name, archiveAfter, reason); - - /// - /// 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 async Task CreateReactionAsync(DiscordEmoji emoji) - => await this.Discord.ApiClient.CreateReactionAsync(this.ChannelId, this.Id, emoji.ToReactionString()); - - /// - /// Creates a reaction to this message. - /// - /// The id of the emoji you want to react with - /// - /// 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. - /// This overload only works with guild or application emoji - public async Task CreateReactionAsync(ulong emojiId) - => await this.Discord.ApiClient.CreateReactionAsync(this.ChannelId, this.Id, $"_:{emojiId}"); - - /// - /// 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 async Task DeleteOwnReactionAsync(DiscordEmoji emoji) - => await this.Discord.ApiClient.DeleteOwnReactionAsync(this.ChannelId, this.Id, emoji.ToReactionString()); - - /// - /// Deletes your own reaction - /// - /// Emoji id for the reaction you want to remove - /// - /// Thrown when the emoji does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - /// This overload only works with guild or application emoji - public async Task DeleteOwnReactionAsync(ulong emojiId) - => await this.Discord.ApiClient.DeleteOwnReactionAsync(this.ChannelId, this.Id, $"_:{emojiId}"); - - /// - /// 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 async Task DeleteReactionAsync(DiscordEmoji emoji, DiscordUser user, string? reason = null) - => await this.Discord.ApiClient.DeleteUserReactionAsync(this.ChannelId, this.Id, user.Id, emoji.ToReactionString(), reason); - - /// - /// Deletes another user's reaction. - /// - /// Emoji id for the reaction you want to remove - /// 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. - /// This overload only works with guild or application emoji - public async Task DeleteReactionAsync(ulong emojiId, DiscordUser user, string? reason = null) - => await this.Discord.ApiClient.DeleteUserReactionAsync(this.ChannelId, this.Id, user.Id, $"_:{emojiId}" , reason); - - /// - /// Gets users that reacted with this emoji. - /// - /// The emoji those users reacted with. - /// Cancels enumeration before the next API request. - /// - /// Thrown when the emoji does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public IAsyncEnumerable GetReactionsAsync - ( - DiscordEmoji emoji, - - CancellationToken cancellationToken = default - ) => InternalGetReactionsAsync(emoji.ToReactionString(), cancellationToken); - - /// - /// Gets users that reacted with this emoji. - /// - /// Emoji id for the reaction you want to remove - /// Cancels enumeration before the next API request. - /// - /// Thrown when the emoji does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - /// This overload only works with guild or application emoji - public IAsyncEnumerable GetReactionsAsync - ( - ulong emojiId, - - CancellationToken cancellationToken = default - ) => InternalGetReactionsAsync($"_:{emojiId}", cancellationToken); - - /// - /// Gets users that reacted with this emoji. - /// - /// The emoji those users reacted with. - /// Cancels enumeration before the next API request. - /// - /// Thrown when the emoji does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - private async IAsyncEnumerable InternalGetReactionsAsync - ( - string emoji, - - [EnumeratorCancellation] - CancellationToken cancellationToken = default - ) - { - // the API request limit is 100, the default is 25 - int receivedOnLastCall = 100; - ulong? last = null; - - while (receivedOnLastCall == 100) - { - if (cancellationToken.IsCancellationRequested) - { - yield break; - } - - IReadOnlyList users = await this.Discord.ApiClient.GetReactionsAsync - ( - channelId: this.ChannelId, - messageId: this.Id, - emoji: emoji, - afterId: last, - limit: 100 - ); - - receivedOnLastCall = users.Count; - - foreach (DiscordUser user in users) - { - user.Discord = this.Discord; - - _ = this.Discord.UpdateUserCache(user); - - yield return user; - } - - last = users.LastOrDefault()?.Id; - } - } - - /// - /// 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 async Task DeleteAllReactionsAsync(string? reason = null) - => await 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 async Task DeleteReactionsEmojiAsync(DiscordEmoji emoji) - => await this.Discord.ApiClient.DeleteReactionsEmojiAsync(this.ChannelId, this.Id, emoji.ToReactionString()); - - /// - /// Deletes all reactions of a specific reaction for this message. - /// - /// The id of the emoji to clear - /// - /// 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. - /// This overload only works with guild or application emoji - public async Task DeleteReactionsEmojiAsync(ulong emojiId) - => await this.Discord.ApiClient.DeleteReactionsEmojiAsync(this.ChannelId, this.Id, $"_:{emojiId}"); - - /// - /// Forwards a message to the specified channel. - /// - /// The forwarded message belonging to the specified channel. - public async Task ForwardAsync(DiscordChannel target) - => await ForwardAsync(target.Id); - - /// - /// Forwards a message to the specified channel. - /// - /// The forwarded message belonging to the specified channel. - public async Task ForwardAsync(ulong targetId) - => await this.Discord.ApiClient.ForwardMessageAsync(targetId, this.ChannelId, this.Id); - - /// - /// Immediately ends the poll. You cannot end polls from other users. - /// - /// - /// Thrown when the client does not have the permission or if the poll is not owned by the client. - /// Thrown when the message does not have a poll. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public async Task EndPollAsync() - => await this.Discord.ApiClient.EndPollAsync(this.ChannelId, this.Id); - - /// - /// Retrieves a full list of users that voted a specified answer on a poll. This will execute one API request per 100 entities. - /// - /// The id of the answer to get the voters of. - /// Cancels the enumeration before the next api request - /// A collection of all users that voted the specified answer on the poll. - /// Thrown when Discord is unable to process the request. - public async IAsyncEnumerable GetAllPollAnswerVotersAsync - ( - int answerId, - [EnumeratorCancellation] - CancellationToken cancellationToken = default - ) - { - int recievedLastCall = 100; - ulong? last = null; - while (recievedLastCall == 100) - { - if (cancellationToken.IsCancellationRequested) - { - yield break; - } - - IReadOnlyList users = await this.Discord.ApiClient.GetPollAnswerVotersAsync(this.ChannelId, this.Id, answerId, last, 100); - recievedLastCall = users.Count; - - foreach (DiscordUser user in users) - { - user.Discord = this.Discord; - - _ = this.Discord.UpdateUserCache(user); - - yield return user; - } - - last = users.LastOrDefault()?.Id; - } - } - - /// - /// Returns a string representation of this message. - /// - /// String representation of this message. - public override string ToString() => $"Message {this.Id}; Attachment count: {this.attachments.Count}; Embed count: {this.embeds.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) => 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() - { - int 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) - => (e1 is not null || e2 is null) && (e1 is null || e2 is not null) && ((e1 is null && e2 is 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); -} +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Represents a Discord text message. +/// +public class DiscordMessage : SnowflakeObject, IEquatable +{ + internal DiscordMessage() + { + + } + + internal DiscordMessage(DiscordMessage other) + : this() + { + this.Discord = other.Discord; + + this.attachments = new List(other.attachments); + this.embeds = new List(other.embeds); + + if (other.mentionedChannels is not null) + { + this.mentionedChannels = new List(other.mentionedChannels); + } + + if (other.mentionedRoles is not null) + { + this.mentionedRoles = new List(other.mentionedRoles); + } + + if (other.mentionedRoleIds is not null) + { + this.mentionedRoleIds = new List(other.mentionedRoleIds); + } + + this.mentionedUsers = new List(other.mentionedUsers); + this.reactions = new List(other.reactions); + this.stickers = new List(other.stickers); + + this.Author = other.Author; + this.ChannelId = other.ChannelId; + this.Content = other.Content; + this.EditedTimestamp = other.EditedTimestamp; + this.Id = other.Id; + this.IsTTS = other.IsTTS; + this.Poll = other.Poll; + this.MessageType = other.MessageType; + this.Pinned = other.Pinned; + this.Timestamp = other.Timestamp; + this.WebhookId = other.WebhookId; + this.ApplicationId = other.ApplicationId; + this.Components = other.Components; + } + + /// + /// Gets the channel in which the message was sent. + /// + [JsonIgnore] + public DiscordChannel? Channel + { + get + { + DiscordClient? client = this.Discord as DiscordClient; + + return client?.InternalGetCachedChannel(this.ChannelId, this.guildId) ?? + client?.InternalGetCachedThread(this.ChannelId, this.guildId) ?? this.channel; + } + internal set => this.channel = value; + } + + private DiscordChannel? channel; + + /// + /// 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 IReadOnlyList? Components { get; internal set; } + + /// + /// Gets the action rows this message was sent with - components holding buttons, selects and the likes. + /// + public IReadOnlyList? ComponentActionRows + => this.Components?.Where(x => x is DiscordActionRowComponent).Cast().ToList(); + + /// + /// 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. + /// + [JsonProperty("timestamp", NullValueHandling = NullValueHandling.Ignore)] + public DateTimeOffset Timestamp { get; set; } + + /// + /// Gets the message's edit timestamp. Will be null if the message was not edited. + /// + [JsonProperty("edited_timestamp", NullValueHandling = NullValueHandling.Ignore)] + public DateTimeOffset? EditedTimestamp { get; internal set; } + + /// + /// Gets whether this message was edited. + /// + [JsonIgnore] + public bool IsEdited => this.EditedTimestamp is not null; + + /// + /// 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.mentionedUsers; + + [JsonProperty("mentions", NullValueHandling = NullValueHandling.Ignore)] + internal List mentionedUsers = []; + + // 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.mentionedRoles; + + [JsonIgnore] + internal List mentionedRoles = []; + + [JsonProperty("mention_roles")] + internal List mentionedRoleIds = []; + + /// + /// Gets channels mentioned by this message. + /// + [JsonIgnore] + public IReadOnlyList MentionedChannels + => this.mentionedChannels; + + [JsonIgnore] + internal List mentionedChannels = []; + + /// + /// Gets files attached to this message. + /// + [JsonIgnore] + public IReadOnlyList Attachments + => this.attachments; + + [JsonProperty("attachments", NullValueHandling = NullValueHandling.Ignore)] + internal List attachments = []; + + /// + /// Gets embeds attached to this message. + /// + [JsonIgnore] + public IReadOnlyList Embeds + => this.embeds; + + [JsonProperty("embeds", NullValueHandling = NullValueHandling.Ignore)] + internal List embeds = []; + + /// + /// Gets reactions used on this message. + /// + [JsonIgnore] + public IReadOnlyList Reactions + => this.reactions; + + [JsonProperty("reactions", NullValueHandling = NullValueHandling.Ignore)] + internal List reactions = []; + + /* + /// + /// 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 DiscordMessageType? 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; } + + [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 DiscordMessageFlags? 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 + { + get + { + string gid = this.Channel is DiscordDmChannel ? "@me" : this.Channel?.GuildId?.ToString(CultureInfo.InvariantCulture) ?? "@me"; + string cid = this.ChannelId.ToString(CultureInfo.InvariantCulture); + string mid = this.Id.ToString(CultureInfo.InvariantCulture); + + return new Uri($"https://discord.com/channels/{gid}/{cid}/{mid}"); + } + } + + /// + /// Gets stickers for this message. + /// + [JsonIgnore] + public IReadOnlyList? Stickers + => this.stickers; + + [JsonProperty("sticker_items", NullValueHandling = NullValueHandling.Ignore)] + internal List stickers = []; + + [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 the message object for the referenced message + /// + [JsonProperty("message_snapshots", NullValueHandling = NullValueHandling.Ignore)] + public IReadOnlyList? MessageSnapshots { get; internal set; } + + /// + /// Gets the poll object for the message. + /// + [JsonProperty("poll", NullValueHandling = NullValueHandling.Ignore)] + public DiscordPoll? Poll { 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 id of the interaction application, if a response to an interaction. + /// + [JsonProperty("application_id", NullValueHandling = NullValueHandling.Ignore)] + public ulong? ApplicationId { get; internal set; } + + internal DiscordMessageReference InternalBuildMessageReference() + { + DiscordClient client = (DiscordClient)this.Discord; + ulong? guildId = this.internalReference?.GuildId; + ulong? channelId = this.internalReference?.ChannelId; + ulong? messageId = this.internalReference?.MessageId; + + DiscordMessageReference reference = new(); + + if (guildId.HasValue) + { + reference.Guild = client!.guilds.TryGetValue(guildId.Value, out DiscordGuild? g) + ? g + : new DiscordGuild + { + Id = guildId.Value, + Discord = client + }; + } + + DiscordChannel? channel = client.InternalGetCachedChannel(channelId!.Value, this.guildId); + + reference.Type = this.internalReference?.Type; + + if (channel is 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(messageId!.Value, out DiscordMessage? msg)) + { + reference.Message = msg; + } + else + { + reference.Message = new DiscordMessage + { + ChannelId = this.ChannelId, + Discord = client + }; + + if (messageId.HasValue) + { + reference.Message.Id = messageId.Value; + } + } + + return reference; + } + + private IMention[] GetMentions() + { + List mentions = []; + + if (this.ReferencedMessage is not null && this.mentionedUsers.Any(r => r.Id == this.ReferencedMessage.Author?.Id)) + { + mentions.Add(new RepliedUserMention()); // Return null to allow all mentions + } + + if ((this.mentionedUsers?.Count ?? 0) > 0) + { + mentions.AddRange(this.mentionedUsers!.Select(m => (IMention)new UserMention(m))); + } + + if ((this.mentionedRoleIds?.Count ?? 0) > 0) + { + mentions.AddRange(this.mentionedRoleIds!.Select(r => (IMention)new RoleMention(r))); + } + + return [.. mentions]; + } + + internal void PopulateMentions() + { + DiscordGuild? guild = this.Channel?.Guild; + this.mentionedUsers ??= []; + this.mentionedRoles ??= []; + this.mentionedChannels ??= []; + + // Create a Hashset that will replace 'this.mentionedUsers'. + HashSet mentionedUsers = new(new DiscordUserComparer()); + + foreach (DiscordUser usr in this.mentionedUsers) + { + // Assign the Discord instance and update the user cache. + usr.Discord = this.Discord; + this.Discord.UpdateUserCache(usr); + + if (guild is not null && usr is not DiscordMember && guild.members.TryGetValue(usr.Id, out DiscordMember? cachedMember)) + { + // If the message is from a guild, but a discord member isn't provided, try to get the discord member out of guild members cache. + mentionedUsers.Add(cachedMember); + } + else + { + // Add provided user otherwise. + mentionedUsers.Add(usr); + } + } + + // Replace 'this.mentionedUsers'. + this.mentionedUsers = [.. mentionedUsers]; + + if (guild is not null && !string.IsNullOrWhiteSpace(this.Content)) + { + this.mentionedChannels = this.mentionedChannels.Union(Utilities.GetChannelMentions(this).Select(guild.GetChannel)).ToList(); + this.mentionedRoles = this.mentionedRoles.Union(this.mentionedRoleIds.Select(x => guild.roles.GetValueOrDefault(x)!)).ToList(); + + //uncomment if this breaks + //mentionedUsers.UnionWith(Utilities.GetUserMentions(this).Select(this.Discord.GetCachedOrEmptyUserInternal)); + //this.mentionedRoles = this.mentionedRoles.Union(Utilities.GetRoleMentions(this).Select(xid => guild.GetRole(xid))).ToList(); + } + } + + /// + /// Searches the components on this message for an aggregate of all components of a certain type. + /// + public IReadOnlyList FilterComponents() + where T : DiscordComponent + { + List components = []; + + foreach (DiscordComponent component in this.Components) + { + if (component is DiscordActionRowComponent actionRowComponent) + { + foreach (DiscordComponent subComponent in actionRowComponent.Components) + { + if (subComponent is T filteredComponent) + { + components.Add(filteredComponent); + } + } + } + else if (component is T filteredComponent) + { + components.Add(filteredComponent); + } + } + + return components; + } + + /// + /// 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 async Task ModifyAsync(Optional content) + => await this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, content, default, GetMentions(), default, [], null, 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 async Task ModifyAsync(Optional embed = default) + => await this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, default, embed.HasValue ? [embed.Value] : Array.Empty(), GetMentions(), default, [], null, 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 async Task ModifyAsync(Optional content, Optional embed = default) + => await this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, content, embed.HasValue ? [embed.Value] : Array.Empty(), GetMentions(), default, [], null, 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 async Task ModifyAsync(Optional content, Optional> embeds = default) + => await this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, content, embeds, GetMentions(), default, [], null, default); + + /// + /// Edits the message. + /// + /// The builder of the message to edit. + /// Whether to suppress embeds on the message. + /// Attached files to keep. + /// + /// 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, bool suppressEmbeds = false, IEnumerable? attachments = default) + { + builder.Validate(); + return await this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, builder.Content, new Optional>(builder.Embeds), builder.mentions, builder.Components, builder.Files, suppressEmbeds ? DiscordMessageFlags.SuppressedEmbeds : null, attachments); + } + + /// + /// Edits the message. + /// + /// The builder of the message to edit. + /// Whether to suppress embeds on the message. + /// Attached files to keep. + /// + /// 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, bool suppressEmbeds = false, IEnumerable? attachments = default) + { + DiscordMessageBuilder builder = new(this); + action(builder); + builder.Validate(); + return await this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, builder.Content, new Optional>(builder.Embeds), builder.mentions, builder.Components, builder.Files, suppressEmbeds ? DiscordMessageFlags.SuppressedEmbeds : null, attachments); + } + + /// + /// Modifies the visibility of embeds in this message. + /// + /// Whether to hide all embeds. + /// + /// 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 async Task ModifyEmbedSuppressionAsync(bool hideEmbeds) + => await this.Discord.ApiClient.EditMessageAsync(this.ChannelId, this.Id, default, default, default, default, [], hideEmbeds ? DiscordMessageFlags.SuppressedEmbeds : null, default); + + /// + /// 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 async Task DeleteAsync(string? reason = null) + => await this.Discord.ApiClient.DeleteMessageAsync(this.ChannelId, this.Id, reason); + + /// + /// 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 async Task PinAsync() + => await 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 async Task UnpinAsync() + => await 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 async Task RespondAsync(string content) + => await this.Discord.ApiClient.CreateMessageAsync(this.ChannelId, content, null, replyMessageId: this.Id, mentionReply: false, failOnInvalidReply: false, suppressNotifications: 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 async Task RespondAsync(DiscordEmbed embed) + => await this.Discord.ApiClient.CreateMessageAsync(this.ChannelId, null, embed != null ? new[] { embed } : null, replyMessageId: this.Id, mentionReply: false, failOnInvalidReply: false, suppressNotifications: 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 async Task RespondAsync(string content, DiscordEmbed embed) + => await this.Discord.ApiClient.CreateMessageAsync(this.ChannelId, content, embed != null ? new[] { embed } : null, replyMessageId: this.Id, mentionReply: false, failOnInvalidReply: false, suppressNotifications: 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 async Task RespondAsync(DiscordMessageBuilder builder) + => await 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 async Task RespondAsync(Action action) + { + DiscordMessageBuilder builder = new(); + action(builder); + return await this.Discord.ApiClient.CreateMessageAsync(this.ChannelId, builder.WithReply(this.Id, mention: false, failOnInvalidReply: false)); + } + + /// + /// Creates a new thread within this channel from this message. + /// + /// The name of the thread. + /// The auto archive duration of the thread. Three and seven day archive options are locked behind level 2 and level 3 server boosts respectively. + /// Reason for audit logs. + /// The created thread. + /// 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 async Task CreateThreadAsync(string name, DiscordAutoArchiveDuration archiveAfter, string? reason = null) => this.Channel?.Type is not DiscordChannelType.Text and not DiscordChannelType.News + ? throw new InvalidOperationException("Threads can only be created within text or news channels.") + : await this.Discord.ApiClient.CreateThreadFromMessageAsync(this.Channel.Id, this.Id, name, archiveAfter, reason); + + /// + /// 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 async Task CreateReactionAsync(DiscordEmoji emoji) + => await this.Discord.ApiClient.CreateReactionAsync(this.ChannelId, this.Id, emoji.ToReactionString()); + + /// + /// Creates a reaction to this message. + /// + /// The id of the emoji you want to react with + /// + /// 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. + /// This overload only works with guild or application emoji + public async Task CreateReactionAsync(ulong emojiId) + => await this.Discord.ApiClient.CreateReactionAsync(this.ChannelId, this.Id, $"_:{emojiId}"); + + /// + /// 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 async Task DeleteOwnReactionAsync(DiscordEmoji emoji) + => await this.Discord.ApiClient.DeleteOwnReactionAsync(this.ChannelId, this.Id, emoji.ToReactionString()); + + /// + /// Deletes your own reaction + /// + /// Emoji id for the reaction you want to remove + /// + /// Thrown when the emoji does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + /// This overload only works with guild or application emoji + public async Task DeleteOwnReactionAsync(ulong emojiId) + => await this.Discord.ApiClient.DeleteOwnReactionAsync(this.ChannelId, this.Id, $"_:{emojiId}"); + + /// + /// 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 async Task DeleteReactionAsync(DiscordEmoji emoji, DiscordUser user, string? reason = null) + => await this.Discord.ApiClient.DeleteUserReactionAsync(this.ChannelId, this.Id, user.Id, emoji.ToReactionString(), reason); + + /// + /// Deletes another user's reaction. + /// + /// Emoji id for the reaction you want to remove + /// 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. + /// This overload only works with guild or application emoji + public async Task DeleteReactionAsync(ulong emojiId, DiscordUser user, string? reason = null) + => await this.Discord.ApiClient.DeleteUserReactionAsync(this.ChannelId, this.Id, user.Id, $"_:{emojiId}" , reason); + + /// + /// Gets users that reacted with this emoji. + /// + /// The emoji those users reacted with. + /// Cancels enumeration before the next API request. + /// + /// Thrown when the emoji does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public IAsyncEnumerable GetReactionsAsync + ( + DiscordEmoji emoji, + + CancellationToken cancellationToken = default + ) => InternalGetReactionsAsync(emoji.ToReactionString(), cancellationToken); + + /// + /// Gets users that reacted with this emoji. + /// + /// Emoji id for the reaction you want to remove + /// Cancels enumeration before the next API request. + /// + /// Thrown when the emoji does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + /// This overload only works with guild or application emoji + public IAsyncEnumerable GetReactionsAsync + ( + ulong emojiId, + + CancellationToken cancellationToken = default + ) => InternalGetReactionsAsync($"_:{emojiId}", cancellationToken); + + /// + /// Gets users that reacted with this emoji. + /// + /// The emoji those users reacted with. + /// Cancels enumeration before the next API request. + /// + /// Thrown when the emoji does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + private async IAsyncEnumerable InternalGetReactionsAsync + ( + string emoji, + + [EnumeratorCancellation] + CancellationToken cancellationToken = default + ) + { + // the API request limit is 100, the default is 25 + int receivedOnLastCall = 100; + ulong? last = null; + + while (receivedOnLastCall == 100) + { + if (cancellationToken.IsCancellationRequested) + { + yield break; + } + + IReadOnlyList users = await this.Discord.ApiClient.GetReactionsAsync + ( + channelId: this.ChannelId, + messageId: this.Id, + emoji: emoji, + afterId: last, + limit: 100 + ); + + receivedOnLastCall = users.Count; + + foreach (DiscordUser user in users) + { + user.Discord = this.Discord; + + _ = this.Discord.UpdateUserCache(user); + + yield return user; + } + + last = users.LastOrDefault()?.Id; + } + } + + /// + /// 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 async Task DeleteAllReactionsAsync(string? reason = null) + => await 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 async Task DeleteReactionsEmojiAsync(DiscordEmoji emoji) + => await this.Discord.ApiClient.DeleteReactionsEmojiAsync(this.ChannelId, this.Id, emoji.ToReactionString()); + + /// + /// Deletes all reactions of a specific reaction for this message. + /// + /// The id of the emoji to clear + /// + /// 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. + /// This overload only works with guild or application emoji + public async Task DeleteReactionsEmojiAsync(ulong emojiId) + => await this.Discord.ApiClient.DeleteReactionsEmojiAsync(this.ChannelId, this.Id, $"_:{emojiId}"); + + /// + /// Forwards a message to the specified channel. + /// + /// The forwarded message belonging to the specified channel. + public async Task ForwardAsync(DiscordChannel target) + => await ForwardAsync(target.Id); + + /// + /// Forwards a message to the specified channel. + /// + /// The forwarded message belonging to the specified channel. + public async Task ForwardAsync(ulong targetId) + => await this.Discord.ApiClient.ForwardMessageAsync(targetId, this.ChannelId, this.Id); + + /// + /// Immediately ends the poll. You cannot end polls from other users. + /// + /// + /// Thrown when the client does not have the permission or if the poll is not owned by the client. + /// Thrown when the message does not have a poll. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task EndPollAsync() + => await this.Discord.ApiClient.EndPollAsync(this.ChannelId, this.Id); + + /// + /// Retrieves a full list of users that voted a specified answer on a poll. This will execute one API request per 100 entities. + /// + /// The id of the answer to get the voters of. + /// Cancels the enumeration before the next api request + /// A collection of all users that voted the specified answer on the poll. + /// Thrown when Discord is unable to process the request. + public async IAsyncEnumerable GetAllPollAnswerVotersAsync + ( + int answerId, + [EnumeratorCancellation] + CancellationToken cancellationToken = default + ) + { + int recievedLastCall = 100; + ulong? last = null; + while (recievedLastCall == 100) + { + if (cancellationToken.IsCancellationRequested) + { + yield break; + } + + IReadOnlyList users = await this.Discord.ApiClient.GetPollAnswerVotersAsync(this.ChannelId, this.Id, answerId, last, 100); + recievedLastCall = users.Count; + + foreach (DiscordUser user in users) + { + user.Discord = this.Discord; + + _ = this.Discord.UpdateUserCache(user); + + yield return user; + } + + last = users.LastOrDefault()?.Id; + } + } + + /// + /// Returns a string representation of this message. + /// + /// String representation of this message. + public override string ToString() => $"Message {this.Id}; Attachment count: {this.attachments.Count}; Embed count: {this.embeds.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) => 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() + { + int 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) + => (e1 is not null || e2 is null) && (e1 is null || e2 is not null) && ((e1 is null && e2 is 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/DSharpPlus/Entities/Channel/Message/DiscordMessageActivity.cs b/DSharpPlus/Entities/Channel/Message/DiscordMessageActivity.cs index 6669aaeee5..c860c89e55 100644 --- a/DSharpPlus/Entities/Channel/Message/DiscordMessageActivity.cs +++ b/DSharpPlus/Entities/Channel/Message/DiscordMessageActivity.cs @@ -1,23 +1,23 @@ -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents a Rich Presence activity. -/// -public class DiscordMessageActivity -{ - /// - /// Gets the activity type. - /// - [JsonProperty("type")] - public DiscordMessageActivityType Type { get; internal set; } - - /// - /// Gets the party id of the activity. - /// - [JsonProperty("party_id")] - public string? PartyId { get; internal set; } - - internal DiscordMessageActivity() { } -} +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Represents a Rich Presence activity. +/// +public class DiscordMessageActivity +{ + /// + /// Gets the activity type. + /// + [JsonProperty("type")] + public DiscordMessageActivityType Type { get; internal set; } + + /// + /// Gets the party id of the activity. + /// + [JsonProperty("party_id")] + public string? PartyId { get; internal set; } + + internal DiscordMessageActivity() { } +} diff --git a/DSharpPlus/Entities/Channel/Message/DiscordMessageActivityType.cs b/DSharpPlus/Entities/Channel/Message/DiscordMessageActivityType.cs index 2983b1e8bf..2578c59b20 100644 --- a/DSharpPlus/Entities/Channel/Message/DiscordMessageActivityType.cs +++ b/DSharpPlus/Entities/Channel/Message/DiscordMessageActivityType.cs @@ -1,28 +1,28 @@ -namespace DSharpPlus.Entities; - - -/// -/// Indicates the type of MessageActivity for the Rich Presence. -/// -public enum DiscordMessageActivityType -{ - /// - /// Invites the user to join. - /// - Join = 1, - - /// - /// Invites the user to spectate. - /// - Spectate = 2, - - /// - /// Invites the user to listen. - /// - Listen = 3, - - /// - /// Allows the user to request to join. - /// - JoinRequest = 5 -} +namespace DSharpPlus.Entities; + + +/// +/// Indicates the type of MessageActivity for the Rich Presence. +/// +public enum DiscordMessageActivityType +{ + /// + /// Invites the user to join. + /// + Join = 1, + + /// + /// Invites the user to spectate. + /// + Spectate = 2, + + /// + /// Invites the user to listen. + /// + Listen = 3, + + /// + /// Allows the user to request to join. + /// + JoinRequest = 5 +} diff --git a/DSharpPlus/Entities/Channel/Message/DiscordMessageApplication.cs b/DSharpPlus/Entities/Channel/Message/DiscordMessageApplication.cs index 5aa5cb661c..d6b0410a8e 100644 --- a/DSharpPlus/Entities/Channel/Message/DiscordMessageApplication.cs +++ b/DSharpPlus/Entities/Channel/Message/DiscordMessageApplication.cs @@ -1,35 +1,35 @@ -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents a Rich Presence application. -/// -public class DiscordMessageApplication : SnowflakeObject -{ - /// - /// Gets the ID of this application's cover image. - /// - [JsonProperty("cover_image")] - public virtual string? CoverImageUrl { get; internal set; } - - /// - /// Gets the application's description. - /// - [JsonProperty("description")] - public string? Description { get; internal set; } - - /// - /// Gets the ID of the application's icon. - /// - [JsonProperty("icon")] - public virtual string? Icon { get; internal set; } - - /// - /// Gets the application's name. - /// - [JsonProperty("name")] - public string Name { get; internal set; } = default!; - - internal DiscordMessageApplication() { } -} +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Represents a Rich Presence application. +/// +public class DiscordMessageApplication : SnowflakeObject +{ + /// + /// Gets the ID of this application's cover image. + /// + [JsonProperty("cover_image")] + public virtual string? CoverImageUrl { get; internal set; } + + /// + /// Gets the application's description. + /// + [JsonProperty("description")] + public string? Description { get; internal set; } + + /// + /// Gets the ID of the application's icon. + /// + [JsonProperty("icon")] + public virtual string? Icon { get; internal set; } + + /// + /// Gets the application's name. + /// + [JsonProperty("name")] + public string Name { get; internal set; } = default!; + + internal DiscordMessageApplication() { } +} diff --git a/DSharpPlus/Entities/Channel/Message/DiscordMessageBuilder.cs b/DSharpPlus/Entities/Channel/Message/DiscordMessageBuilder.cs index f029ff6090..2612344468 100644 --- a/DSharpPlus/Entities/Channel/Message/DiscordMessageBuilder.cs +++ b/DSharpPlus/Entities/Channel/Message/DiscordMessageBuilder.cs @@ -1,292 +1,292 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace DSharpPlus.Entities; - -/// -/// Constructs a Message to be sent. -/// -public sealed class DiscordMessageBuilder : BaseDiscordMessageBuilder -{ - /// - /// Gets or sets the embed for the builder. This will always set the builder to have one embed. - /// - [Obsolete("Use the features for manipulating multiple embeds instead.", true, DiagnosticId = "DSP1001")] - public DiscordEmbed? Embed - { - get => this.embeds.Count > 0 ? this.embeds[0] : null; - set - { - this.embeds.Clear(); - if (value != null) - { - this.embeds.Add(value); - } - } - } - - /// - /// Gets or sets the sticker for the builder. This will always set the builder to have one sticker. - /// - [Obsolete("Use the features for manipulating multiple stickers instead.", true, DiagnosticId = "DSP1002")] - public DiscordMessageSticker? Sticker - { - get => this.stickers.Count > 0 ? this.stickers[0] : null; - set - { - this.stickers.Clear(); - if (value != null) - { - this.stickers.Add(value); - } - } - } - - /// - /// The stickers to attach to the message. - /// - public IReadOnlyList Stickers => this.stickers; - internal List stickers = []; - - /// - /// Gets the Reply Message ID. - /// - public ulong? ReplyId { get; private set; } = null; - - /// - /// Gets if the Reply should mention the user. - /// - public bool MentionOnReply { get; private set; } = false; - - /// - /// Gets if the Reply will error if the Reply Message Id does not reference a valid message. - /// If set to false, invalid replies are send as a regular message. - /// Defaults to false. - /// - public bool FailOnInvalidReply { get; set; } - - /// - /// Constructs a new discord message builder - /// - public DiscordMessageBuilder() { } - - /// - /// Constructs a new discord message builder based on a previous builder. - /// - /// The builder to copy. - public DiscordMessageBuilder(DiscordMessageBuilder builder) : base(builder) - { - this.stickers = builder.stickers; - this.ReplyId = builder.ReplyId; - this.MentionOnReply = builder.MentionOnReply; - this.FailOnInvalidReply = builder.FailOnInvalidReply; - } - - /// - /// Copies the common properties from the passed builder. - /// - /// The builder to copy. - public DiscordMessageBuilder(IDiscordMessageBuilder builder) : base(builder) { } - - /// - /// Constructs a new discord message builder based on the passed message. - /// - /// The message to copy. - public DiscordMessageBuilder(DiscordMessage baseMessage) - { - this.IsTTS = baseMessage.IsTTS; - this.Poll = baseMessage.Poll == null ? null : new DiscordPollBuilder(baseMessage.Poll); - this.ReplyId = baseMessage.ReferencedMessage?.Id; - this.components = [.. baseMessage.Components?.OfType()]; - this.Content = baseMessage.Content; - this.embeds = [.. baseMessage.Embeds]; - this.stickers = [.. baseMessage.Stickers]; - this.mentions = []; - - if (baseMessage.mentionedUsers != null) - { - foreach (DiscordUser user in baseMessage.mentionedUsers) - { - this.mentions.Add(new UserMention(user.Id)); - } - } - - // Unsure about mentionedRoleIds - if (baseMessage.mentionedRoles != null) - { - foreach (DiscordRole role in baseMessage.mentionedRoles) - { - this.mentions.Add(new RoleMention(role.Id)); - } - } - } - - - /// - /// Constructs a new discord message builder based on the passed message snapshot. - /// - /// The message to copy. - public DiscordMessageBuilder(DiscordMessageSnapshotContent baseSnapshotMessage) - { - this.components = [.. baseSnapshotMessage.Components?.OfType()]; - this.Content = baseSnapshotMessage.Content; - this.embeds = [.. baseSnapshotMessage.Embeds]; - this.stickers = [.. baseSnapshotMessage.Stickers]; - this.mentions = []; - - if (baseSnapshotMessage.mentionedUsers != null) - { - foreach (DiscordUser user in baseSnapshotMessage.mentionedUsers) - { - this.mentions.Add(new UserMention(user.Id)); - } - } - - // Unsure about mentionedRoleIds - if (baseSnapshotMessage.mentionedRoles != null) - { - foreach (DiscordRole role in baseSnapshotMessage.mentionedRoles) - { - this.mentions.Add(new RoleMention(role.Id)); - } - } - } - - /// - /// Adds a sticker to the message. Sticker must be from current guild. - /// - /// The sticker to add. - /// The current builder to be chained. - [Obsolete("Use the features for manipulating multiple stickers instead.", true, DiagnosticId = "DSP1002")] - public DiscordMessageBuilder WithSticker(DiscordMessageSticker sticker) - { - this.Sticker = sticker; - return this; - } - - /// - /// Adds a sticker to the message. Sticker must be from current guild. - /// - /// The sticker to add. - /// The current builder to be chained. - public DiscordMessageBuilder WithStickers(IEnumerable stickers) - { - this.stickers = stickers.ToList(); - return this; - } - - /// - /// Sets the embed for the current builder. - /// - /// The embed that should be set. - /// The current builder to be chained. - [Obsolete("Use the features for manipulating multiple embeds instead.", true, DiagnosticId = "DSP1001")] - public DiscordMessageBuilder WithEmbed(DiscordEmbed embed) - { - if (embed == null) - { - return this; - } - - this.Embed = embed; - return this; - } - - /// - /// Sets if the message has allowed mentions. - /// - /// The allowed Mention that should be sent. - /// The current builder to be chained. - public DiscordMessageBuilder WithAllowedMention(IMention allowedMention) - => AddMention(allowedMention); - - /// - /// Sets if the message has allowed mentions. - /// - /// The allowed Mentions that should be sent. - /// The current builder to be chained. - public DiscordMessageBuilder WithAllowedMentions(IEnumerable allowedMentions) - => AddMentions(allowedMentions); - - /// - /// Sets if the message is a reply - /// - /// The ID of the message to reply to. - /// If we should mention the user in the reply. - /// Whether sending a reply that references an invalid message should be - /// The current builder to be chained. - public DiscordMessageBuilder WithReply(ulong? messageId, bool mention = false, bool failOnInvalidReply = false) - { - this.ReplyId = messageId; - this.MentionOnReply = mention; - this.FailOnInvalidReply = failOnInvalidReply; - - if (mention) - { - this.mentions ??= []; - this.mentions.Add(new RepliedUserMention()); - } - - return this; - } - - /// - /// Sends the Message to a specific channel - /// - /// The channel the message should be sent to. - /// The current builder to be chained. - public Task SendAsync(DiscordChannel channel) => channel.SendMessageAsync(this); - - /// - /// Sends the modified message. - /// Note: Message replies cannot be modified. To clear the reply, simply pass to . - /// - /// The original Message to modify. - /// The current builder to be chained. - public Task ModifyAsync(DiscordMessage msg) => msg.ModifyAsync(this); - - /// - /// Does the validation before we send a the Create/Modify request. - /// - internal void Validate() - { - if (this.embeds.Count > 10) - { - throw new ArgumentException("A message can only have up to 10 embeds."); - } - - if (!this.Flags.HasFlag(DiscordMessageFlags.IsComponentsV2) && this.Poll == null && this.Files?.Count == 0 && string.IsNullOrEmpty(this.Content) && (!this.Embeds?.Any() ?? true) && (!this.Stickers?.Any() ?? true)) - { - throw new ArgumentException("You must specify content, an embed, a sticker, a poll, or at least one file."); - } - - if (!this.Flags.HasFlag(DiscordMessageFlags.IsComponentsV2) && this.Components.Count > 5) - { - throw new InvalidOperationException("You can only have 5 action rows per message."); - } - else if (this.Components.Count > 10) - { - throw new InvalidOperationException("You can only have 10 surface-level components per message."); - } - - if (!this.Flags.HasFlag(DiscordMessageFlags.IsComponentsV2) && this.Components.Any(c => c is not DiscordActionRowComponent)) - { - throw new InvalidOperationException - ( - "V2 Components can only be added to a builder with the V2 components flag set." - ); - } - - if (this.Components.OfType().Any(c => c.Components.Count > 5)) - { - throw new InvalidOperationException("Action rows can only have 5 components"); - } - - if (this.Stickers?.Count > 3) - { - throw new InvalidOperationException("You can only have 3 stickers per message."); - } - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace DSharpPlus.Entities; + +/// +/// Constructs a Message to be sent. +/// +public sealed class DiscordMessageBuilder : BaseDiscordMessageBuilder +{ + /// + /// Gets or sets the embed for the builder. This will always set the builder to have one embed. + /// + [Obsolete("Use the features for manipulating multiple embeds instead.", true, DiagnosticId = "DSP1001")] + public DiscordEmbed? Embed + { + get => this.embeds.Count > 0 ? this.embeds[0] : null; + set + { + this.embeds.Clear(); + if (value != null) + { + this.embeds.Add(value); + } + } + } + + /// + /// Gets or sets the sticker for the builder. This will always set the builder to have one sticker. + /// + [Obsolete("Use the features for manipulating multiple stickers instead.", true, DiagnosticId = "DSP1002")] + public DiscordMessageSticker? Sticker + { + get => this.stickers.Count > 0 ? this.stickers[0] : null; + set + { + this.stickers.Clear(); + if (value != null) + { + this.stickers.Add(value); + } + } + } + + /// + /// The stickers to attach to the message. + /// + public IReadOnlyList Stickers => this.stickers; + internal List stickers = []; + + /// + /// Gets the Reply Message ID. + /// + public ulong? ReplyId { get; private set; } = null; + + /// + /// Gets if the Reply should mention the user. + /// + public bool MentionOnReply { get; private set; } = false; + + /// + /// Gets if the Reply will error if the Reply Message Id does not reference a valid message. + /// If set to false, invalid replies are send as a regular message. + /// Defaults to false. + /// + public bool FailOnInvalidReply { get; set; } + + /// + /// Constructs a new discord message builder + /// + public DiscordMessageBuilder() { } + + /// + /// Constructs a new discord message builder based on a previous builder. + /// + /// The builder to copy. + public DiscordMessageBuilder(DiscordMessageBuilder builder) : base(builder) + { + this.stickers = builder.stickers; + this.ReplyId = builder.ReplyId; + this.MentionOnReply = builder.MentionOnReply; + this.FailOnInvalidReply = builder.FailOnInvalidReply; + } + + /// + /// Copies the common properties from the passed builder. + /// + /// The builder to copy. + public DiscordMessageBuilder(IDiscordMessageBuilder builder) : base(builder) { } + + /// + /// Constructs a new discord message builder based on the passed message. + /// + /// The message to copy. + public DiscordMessageBuilder(DiscordMessage baseMessage) + { + this.IsTTS = baseMessage.IsTTS; + this.Poll = baseMessage.Poll == null ? null : new DiscordPollBuilder(baseMessage.Poll); + this.ReplyId = baseMessage.ReferencedMessage?.Id; + this.components = [.. baseMessage.Components?.OfType()]; + this.Content = baseMessage.Content; + this.embeds = [.. baseMessage.Embeds]; + this.stickers = [.. baseMessage.Stickers]; + this.mentions = []; + + if (baseMessage.mentionedUsers != null) + { + foreach (DiscordUser user in baseMessage.mentionedUsers) + { + this.mentions.Add(new UserMention(user.Id)); + } + } + + // Unsure about mentionedRoleIds + if (baseMessage.mentionedRoles != null) + { + foreach (DiscordRole role in baseMessage.mentionedRoles) + { + this.mentions.Add(new RoleMention(role.Id)); + } + } + } + + + /// + /// Constructs a new discord message builder based on the passed message snapshot. + /// + /// The message to copy. + public DiscordMessageBuilder(DiscordMessageSnapshotContent baseSnapshotMessage) + { + this.components = [.. baseSnapshotMessage.Components?.OfType()]; + this.Content = baseSnapshotMessage.Content; + this.embeds = [.. baseSnapshotMessage.Embeds]; + this.stickers = [.. baseSnapshotMessage.Stickers]; + this.mentions = []; + + if (baseSnapshotMessage.mentionedUsers != null) + { + foreach (DiscordUser user in baseSnapshotMessage.mentionedUsers) + { + this.mentions.Add(new UserMention(user.Id)); + } + } + + // Unsure about mentionedRoleIds + if (baseSnapshotMessage.mentionedRoles != null) + { + foreach (DiscordRole role in baseSnapshotMessage.mentionedRoles) + { + this.mentions.Add(new RoleMention(role.Id)); + } + } + } + + /// + /// Adds a sticker to the message. Sticker must be from current guild. + /// + /// The sticker to add. + /// The current builder to be chained. + [Obsolete("Use the features for manipulating multiple stickers instead.", true, DiagnosticId = "DSP1002")] + public DiscordMessageBuilder WithSticker(DiscordMessageSticker sticker) + { + this.Sticker = sticker; + return this; + } + + /// + /// Adds a sticker to the message. Sticker must be from current guild. + /// + /// The sticker to add. + /// The current builder to be chained. + public DiscordMessageBuilder WithStickers(IEnumerable stickers) + { + this.stickers = stickers.ToList(); + return this; + } + + /// + /// Sets the embed for the current builder. + /// + /// The embed that should be set. + /// The current builder to be chained. + [Obsolete("Use the features for manipulating multiple embeds instead.", true, DiagnosticId = "DSP1001")] + public DiscordMessageBuilder WithEmbed(DiscordEmbed embed) + { + if (embed == null) + { + return this; + } + + this.Embed = embed; + return this; + } + + /// + /// Sets if the message has allowed mentions. + /// + /// The allowed Mention that should be sent. + /// The current builder to be chained. + public DiscordMessageBuilder WithAllowedMention(IMention allowedMention) + => AddMention(allowedMention); + + /// + /// Sets if the message has allowed mentions. + /// + /// The allowed Mentions that should be sent. + /// The current builder to be chained. + public DiscordMessageBuilder WithAllowedMentions(IEnumerable allowedMentions) + => AddMentions(allowedMentions); + + /// + /// Sets if the message is a reply + /// + /// The ID of the message to reply to. + /// If we should mention the user in the reply. + /// Whether sending a reply that references an invalid message should be + /// The current builder to be chained. + public DiscordMessageBuilder WithReply(ulong? messageId, bool mention = false, bool failOnInvalidReply = false) + { + this.ReplyId = messageId; + this.MentionOnReply = mention; + this.FailOnInvalidReply = failOnInvalidReply; + + if (mention) + { + this.mentions ??= []; + this.mentions.Add(new RepliedUserMention()); + } + + return this; + } + + /// + /// Sends the Message to a specific channel + /// + /// The channel the message should be sent to. + /// The current builder to be chained. + public Task SendAsync(DiscordChannel channel) => channel.SendMessageAsync(this); + + /// + /// Sends the modified message. + /// Note: Message replies cannot be modified. To clear the reply, simply pass to . + /// + /// The original Message to modify. + /// The current builder to be chained. + public Task ModifyAsync(DiscordMessage msg) => msg.ModifyAsync(this); + + /// + /// Does the validation before we send a the Create/Modify request. + /// + internal void Validate() + { + if (this.embeds.Count > 10) + { + throw new ArgumentException("A message can only have up to 10 embeds."); + } + + if (!this.Flags.HasFlag(DiscordMessageFlags.IsComponentsV2) && this.Poll == null && this.Files?.Count == 0 && string.IsNullOrEmpty(this.Content) && (!this.Embeds?.Any() ?? true) && (!this.Stickers?.Any() ?? true)) + { + throw new ArgumentException("You must specify content, an embed, a sticker, a poll, or at least one file."); + } + + if (!this.Flags.HasFlag(DiscordMessageFlags.IsComponentsV2) && this.Components.Count > 5) + { + throw new InvalidOperationException("You can only have 5 action rows per message."); + } + else if (this.Components.Count > 10) + { + throw new InvalidOperationException("You can only have 10 surface-level components per message."); + } + + if (!this.Flags.HasFlag(DiscordMessageFlags.IsComponentsV2) && this.Components.Any(c => c is not DiscordActionRowComponent)) + { + throw new InvalidOperationException + ( + "V2 Components can only be added to a builder with the V2 components flag set." + ); + } + + if (this.Components.OfType().Any(c => c.Components.Count > 5)) + { + throw new InvalidOperationException("Action rows can only have 5 components"); + } + + if (this.Stickers?.Count > 3) + { + throw new InvalidOperationException("You can only have 3 stickers per message."); + } + } +} diff --git a/DSharpPlus/Entities/Channel/Message/DiscordMessageFile.cs b/DSharpPlus/Entities/Channel/Message/DiscordMessageFile.cs index 74c3e323b4..c892c84d98 100644 --- a/DSharpPlus/Entities/Channel/Message/DiscordMessageFile.cs +++ b/DSharpPlus/Entities/Channel/Message/DiscordMessageFile.cs @@ -1,47 +1,47 @@ -using System.IO; - -namespace DSharpPlus.Entities; - -/// -/// Represents files that should be sent to Discord as part of a . -/// -public record struct DiscordMessageFile -{ - public DiscordMessageFile - ( - string fileName, - Stream stream, - long? resetPositionTo = null, - string? fileType = null, - string? contentType = null, - AddFileOptions fileOptions = AddFileOptions.None - ) - { - this.FileName = fileName ?? "file"; - this.FileType = fileType; - this.ContentType = contentType; - this.FileOptions = fileOptions; - this.Stream = stream; - this.ResetPositionTo = resetPositionTo; - } - - /// - /// Gets the FileName of the File. - /// - public string FileName { get; internal set; } - - /// - /// Gets the stream of the File. - /// - public Stream Stream { get; internal set; } - - internal string? FileType { get; set; } - - internal string? ContentType { get; set; } - - /// - /// Gets the position the File should be reset to. - /// - internal long? ResetPositionTo { get; set; } - internal AddFileOptions FileOptions { get; set; } -} +using System.IO; + +namespace DSharpPlus.Entities; + +/// +/// Represents files that should be sent to Discord as part of a . +/// +public record struct DiscordMessageFile +{ + public DiscordMessageFile + ( + string fileName, + Stream stream, + long? resetPositionTo = null, + string? fileType = null, + string? contentType = null, + AddFileOptions fileOptions = AddFileOptions.None + ) + { + this.FileName = fileName ?? "file"; + this.FileType = fileType; + this.ContentType = contentType; + this.FileOptions = fileOptions; + this.Stream = stream; + this.ResetPositionTo = resetPositionTo; + } + + /// + /// Gets the FileName of the File. + /// + public string FileName { get; internal set; } + + /// + /// Gets the stream of the File. + /// + public Stream Stream { get; internal set; } + + internal string? FileType { get; set; } + + internal string? ContentType { get; set; } + + /// + /// Gets the position the File should be reset to. + /// + internal long? ResetPositionTo { get; set; } + internal AddFileOptions FileOptions { get; set; } +} diff --git a/DSharpPlus/Entities/Channel/Message/DiscordMessageFlags.cs b/DSharpPlus/Entities/Channel/Message/DiscordMessageFlags.cs index 503ec4b2ce..82b249c57e 100644 --- a/DSharpPlus/Entities/Channel/Message/DiscordMessageFlags.cs +++ b/DSharpPlus/Entities/Channel/Message/DiscordMessageFlags.cs @@ -1,76 +1,76 @@ -using System; - -namespace DSharpPlus.Entities; - -/// -/// Represents additional features of a message. -/// -[Flags] -public enum DiscordMessageFlags -{ - /// - /// Whether this message is the original message that was published from a news channel to subscriber channels. - /// - /// This flag is inbound only (it cannot be set). - Crossposted = 1 << 0, - - /// - /// Whether this message is crossposted (automatically posted in a subscriber channel). - /// - /// This flag is inbound only (it cannot be set). - IsCrosspost = 1 << 1, - - /// - /// Whether any embeds in the message are hidden. - /// - /// This flag is inbound only (it cannot be set). - SuppressedEmbeds = 1 << 2, - - /// - /// The source message for this crosspost has been deleted. - /// - /// This flag is inbound only (it cannot be set). - SourceMessageDeleted = 1 << 3, - - /// - /// The message came from the urgent message system. - /// - /// This flag is inbound only (it cannot be set). - Urgent = 1 << 4, - - /// - /// The message is only visible to the user who invoked the interaction. - /// - Ephemeral = 1 << 6, - - /// - /// The message is an interaction response and the bot is "thinking". - /// - /// This flag is inbound only (it cannot be set). - Loading = 1 << 7, - - /// - /// Indicates that some roles mentioned in the message could not be added to the current thread. - /// - /// This flag is inbound only (it cannot be set). - FailedToMentionSomeRolesInThread = 1 << 8, - - /// - /// Indicates that the message contains a link (usually to a file) that will prompt the user - /// with a precautionary message saying that the link may be unsafe. - /// - /// This flag is inbound only (it cannot be set). - ContainsSuspiciousThirdPartyLink = 1 << 10, - - /// - /// Indicates that this message will supress push notifications. - /// Mentions in the message will still have a mention indicator, however. - /// - SuppressNotifications = 1 << 12, - - /// - /// Indicates that this message is/will support Components V2. - /// Messages that are upgraded to components V2 cannot be downgraded. - /// - IsComponentsV2 = 1 << 15, -} +using System; + +namespace DSharpPlus.Entities; + +/// +/// Represents additional features of a message. +/// +[Flags] +public enum DiscordMessageFlags +{ + /// + /// Whether this message is the original message that was published from a news channel to subscriber channels. + /// + /// This flag is inbound only (it cannot be set). + Crossposted = 1 << 0, + + /// + /// Whether this message is crossposted (automatically posted in a subscriber channel). + /// + /// This flag is inbound only (it cannot be set). + IsCrosspost = 1 << 1, + + /// + /// Whether any embeds in the message are hidden. + /// + /// This flag is inbound only (it cannot be set). + SuppressedEmbeds = 1 << 2, + + /// + /// The source message for this crosspost has been deleted. + /// + /// This flag is inbound only (it cannot be set). + SourceMessageDeleted = 1 << 3, + + /// + /// The message came from the urgent message system. + /// + /// This flag is inbound only (it cannot be set). + Urgent = 1 << 4, + + /// + /// The message is only visible to the user who invoked the interaction. + /// + Ephemeral = 1 << 6, + + /// + /// The message is an interaction response and the bot is "thinking". + /// + /// This flag is inbound only (it cannot be set). + Loading = 1 << 7, + + /// + /// Indicates that some roles mentioned in the message could not be added to the current thread. + /// + /// This flag is inbound only (it cannot be set). + FailedToMentionSomeRolesInThread = 1 << 8, + + /// + /// Indicates that the message contains a link (usually to a file) that will prompt the user + /// with a precautionary message saying that the link may be unsafe. + /// + /// This flag is inbound only (it cannot be set). + ContainsSuspiciousThirdPartyLink = 1 << 10, + + /// + /// Indicates that this message will supress push notifications. + /// Mentions in the message will still have a mention indicator, however. + /// + SuppressNotifications = 1 << 12, + + /// + /// Indicates that this message is/will support Components V2. + /// Messages that are upgraded to components V2 cannot be downgraded. + /// + IsComponentsV2 = 1 << 15, +} diff --git a/DSharpPlus/Entities/Channel/Message/DiscordMessageInteraction.cs b/DSharpPlus/Entities/Channel/Message/DiscordMessageInteraction.cs index 4d8cd1a6c0..2953ce4465 100644 --- a/DSharpPlus/Entities/Channel/Message/DiscordMessageInteraction.cs +++ b/DSharpPlus/Entities/Channel/Message/DiscordMessageInteraction.cs @@ -1,27 +1,27 @@ -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents the message interaction data sent when a message is an interaction response. -/// -public class DiscordMessageInteraction : SnowflakeObject -{ - /// - /// Gets the type of the interaction. - /// - [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] - public DiscordInteractionType Type { get; internal set; } - - /// - /// Gets the name of the . - /// - [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] - public string? Name { get; internal set; } - - /// - /// Gets the user who invoked the interaction. - /// - [JsonProperty("user", NullValueHandling = NullValueHandling.Ignore)] - public DiscordUser? User { get; internal set; } -} +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Represents the message interaction data sent when a message is an interaction response. +/// +public class DiscordMessageInteraction : SnowflakeObject +{ + /// + /// Gets the type of the interaction. + /// + [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] + public DiscordInteractionType Type { get; internal set; } + + /// + /// Gets the name of the . + /// + [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] + public string? Name { get; internal set; } + + /// + /// Gets the user who invoked the interaction. + /// + [JsonProperty("user", NullValueHandling = NullValueHandling.Ignore)] + public DiscordUser? User { get; internal set; } +} diff --git a/DSharpPlus/Entities/Channel/Message/DiscordMessageReference.cs b/DSharpPlus/Entities/Channel/Message/DiscordMessageReference.cs index 5337b5214d..6fc8a8a003 100644 --- a/DSharpPlus/Entities/Channel/Message/DiscordMessageReference.cs +++ b/DSharpPlus/Entities/Channel/Message/DiscordMessageReference.cs @@ -1,52 +1,52 @@ -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents data from the original message. -/// -public class DiscordMessageReference -{ - /// - /// Gets the type of the reference. - /// - public DiscordMessageReferenceType? Type { get; set; } - - /// - /// Gets the original message. - /// - public DiscordMessage Message { get; internal set; } = default!; - - /// - /// Gets the channel of the original message. - /// - public DiscordChannel Channel { get; internal set; } = default!; - - /// - /// Gets the guild of the original message. - /// - public DiscordGuild? Guild { get; internal set; } - - public override string ToString() - => $"Guild: {this.Guild?.Id ?? 0}, Channel: {this.Channel.Id}, Message: {this.Message.Id}"; - - internal DiscordMessageReference() { } -} - -internal struct InternalDiscordMessageReference -{ - [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] - internal DiscordMessageReferenceType? Type { get; set; } - - [JsonProperty("message_id", NullValueHandling = NullValueHandling.Ignore)] - internal ulong? MessageId { get; set; } - - [JsonProperty("channel_id", NullValueHandling = NullValueHandling.Ignore)] - internal ulong? ChannelId { get; set; } - - [JsonProperty("guild_id", NullValueHandling = NullValueHandling.Ignore)] - internal ulong? GuildId { get; set; } - - [JsonProperty("fail_if_not_exists", NullValueHandling = NullValueHandling.Ignore)] - public bool FailIfNotExists { get; set; } -} +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Represents data from the original message. +/// +public class DiscordMessageReference +{ + /// + /// Gets the type of the reference. + /// + public DiscordMessageReferenceType? Type { get; set; } + + /// + /// Gets the original message. + /// + public DiscordMessage Message { get; internal set; } = default!; + + /// + /// Gets the channel of the original message. + /// + public DiscordChannel Channel { get; internal set; } = default!; + + /// + /// Gets the guild of the original message. + /// + public DiscordGuild? Guild { get; internal set; } + + public override string ToString() + => $"Guild: {this.Guild?.Id ?? 0}, Channel: {this.Channel.Id}, Message: {this.Message.Id}"; + + internal DiscordMessageReference() { } +} + +internal struct InternalDiscordMessageReference +{ + [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] + internal DiscordMessageReferenceType? Type { get; set; } + + [JsonProperty("message_id", NullValueHandling = NullValueHandling.Ignore)] + internal ulong? MessageId { get; set; } + + [JsonProperty("channel_id", NullValueHandling = NullValueHandling.Ignore)] + internal ulong? ChannelId { get; set; } + + [JsonProperty("guild_id", NullValueHandling = NullValueHandling.Ignore)] + internal ulong? GuildId { get; set; } + + [JsonProperty("fail_if_not_exists", NullValueHandling = NullValueHandling.Ignore)] + public bool FailIfNotExists { get; set; } +} diff --git a/DSharpPlus/Entities/Channel/Message/DiscordMessageSticker.cs b/DSharpPlus/Entities/Channel/Message/DiscordMessageSticker.cs index 948f6bc7a2..0dc2c036ba 100644 --- a/DSharpPlus/Entities/Channel/Message/DiscordMessageSticker.cs +++ b/DSharpPlus/Entities/Channel/Message/DiscordMessageSticker.cs @@ -1,141 +1,141 @@ -using System; -using System.Collections.Generic; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents a Discord Sticker. -/// -public class DiscordMessageSticker : SnowflakeObject, IEquatable -{ - /// - /// Gets the Pack ID of this sticker. - /// - [JsonProperty("pack_id")] - public ulong PackId { get; internal set; } - - /// - /// Gets the Name of the sticker. - /// - [JsonProperty("name")] - public string? Name { get; internal set; } - - /// - /// Gets the Description of the sticker. - /// - [JsonProperty("description")] - public string? Description { get; internal set; } - - /// - /// Gets the type of sticker. - /// - [JsonProperty("type")] - public DiscordStickerType Type { get; internal set; } - - /// - /// For guild stickers, gets the user that made the sticker. - /// - [JsonProperty("user")] - public DiscordUser? User { get; internal set; } - - /// - /// Gets the guild associated with this sticker, if any. - /// - public DiscordGuild Guild => (this.Discord as DiscordClient)!.InternalGetCachedGuild(this.GuildId); - - public string StickerUrl => $"https://cdn.discordapp.com/stickers/{this.Id}{(this.FormatType is DiscordStickerFormat.LOTTIE ? ".json" : ".png")}"; - - /// - /// Gets the Id of the sticker this guild belongs to, if any. - /// - [JsonProperty("guild_id")] - public ulong? GuildId { get; internal set; } - - /// - /// Gets whether this sticker is available. Only applicable to guild stickers. - /// - [JsonProperty("available")] - public bool Available { get; internal set; } - - /// - /// Gets the sticker's sort order, if it's in a pack. - /// - [JsonProperty("sort_value")] - public int SortValue { get; internal set; } - - /// - /// Gets the list of tags for the sticker. - /// - [JsonIgnore] - public IReadOnlyList Tags - => this.InternalTags != null ? this.InternalTags.Split(',') : []; - - /// - /// Gets the asset hash of the sticker. - /// - [JsonProperty("asset")] - public string? Asset { get; internal set; } - - /// - /// Gets the preview asset hash of the sticker. - /// - [JsonProperty("preview_asset", NullValueHandling = NullValueHandling.Ignore)] - public string? PreviewAsset { get; internal set; } - - /// - /// Gets the Format type of the sticker. - /// - [JsonProperty("format_type")] - public DiscordStickerFormat FormatType { get; internal set; } - - [JsonProperty("tags", NullValueHandling = NullValueHandling.Ignore)] - internal string? InternalTags { get; set; } - - public string BannerUrl => $"https://cdn.discordapp.com/app-assets/710982414301790216/store/{this.BannerAssetId}.png?size=4096"; - - [JsonProperty("banner_asset_id")] - internal ulong BannerAssetId { get; set; } - - public bool Equals(DiscordMessageSticker? other) => this.Id == other?.Id; - public override bool Equals(object obj) => Equals(obj as DiscordMessageSticker); - public override string ToString() => $"Sticker {this.Id}; {this.Name}; {this.FormatType}"; - public override int GetHashCode() - { - HashCode hash = new(); - hash.Add(this.Id); - hash.Add(this.CreationTimestamp); - hash.Add(this.Discord); - hash.Add(this.PackId); - hash.Add(this.Name); - hash.Add(this.Description); - hash.Add(this.Type); - hash.Add(this.User); - hash.Add(this.Guild); - hash.Add(this.StickerUrl); - hash.Add(this.GuildId); - hash.Add(this.Available); - hash.Add(this.SortValue); - hash.Add(this.Tags); - hash.Add(this.Asset); - hash.Add(this.PreviewAsset); - hash.Add(this.FormatType); - hash.Add(this.InternalTags); - hash.Add(this.BannerUrl); - hash.Add(this.BannerAssetId); - return hash.ToHashCode(); - } -} - -public enum DiscordStickerType -{ - Standard = 1, - Guild = 2 -} - -public enum DiscordStickerFormat -{ - PNG = 1, - APNG = 2, - LOTTIE = 3 -} +using System; +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Represents a Discord Sticker. +/// +public class DiscordMessageSticker : SnowflakeObject, IEquatable +{ + /// + /// Gets the Pack ID of this sticker. + /// + [JsonProperty("pack_id")] + public ulong PackId { get; internal set; } + + /// + /// Gets the Name of the sticker. + /// + [JsonProperty("name")] + public string? Name { get; internal set; } + + /// + /// Gets the Description of the sticker. + /// + [JsonProperty("description")] + public string? Description { get; internal set; } + + /// + /// Gets the type of sticker. + /// + [JsonProperty("type")] + public DiscordStickerType Type { get; internal set; } + + /// + /// For guild stickers, gets the user that made the sticker. + /// + [JsonProperty("user")] + public DiscordUser? User { get; internal set; } + + /// + /// Gets the guild associated with this sticker, if any. + /// + public DiscordGuild Guild => (this.Discord as DiscordClient)!.InternalGetCachedGuild(this.GuildId); + + public string StickerUrl => $"https://cdn.discordapp.com/stickers/{this.Id}{(this.FormatType is DiscordStickerFormat.LOTTIE ? ".json" : ".png")}"; + + /// + /// Gets the Id of the sticker this guild belongs to, if any. + /// + [JsonProperty("guild_id")] + public ulong? GuildId { get; internal set; } + + /// + /// Gets whether this sticker is available. Only applicable to guild stickers. + /// + [JsonProperty("available")] + public bool Available { get; internal set; } + + /// + /// Gets the sticker's sort order, if it's in a pack. + /// + [JsonProperty("sort_value")] + public int SortValue { get; internal set; } + + /// + /// Gets the list of tags for the sticker. + /// + [JsonIgnore] + public IReadOnlyList Tags + => this.InternalTags != null ? this.InternalTags.Split(',') : []; + + /// + /// Gets the asset hash of the sticker. + /// + [JsonProperty("asset")] + public string? Asset { get; internal set; } + + /// + /// Gets the preview asset hash of the sticker. + /// + [JsonProperty("preview_asset", NullValueHandling = NullValueHandling.Ignore)] + public string? PreviewAsset { get; internal set; } + + /// + /// Gets the Format type of the sticker. + /// + [JsonProperty("format_type")] + public DiscordStickerFormat FormatType { get; internal set; } + + [JsonProperty("tags", NullValueHandling = NullValueHandling.Ignore)] + internal string? InternalTags { get; set; } + + public string BannerUrl => $"https://cdn.discordapp.com/app-assets/710982414301790216/store/{this.BannerAssetId}.png?size=4096"; + + [JsonProperty("banner_asset_id")] + internal ulong BannerAssetId { get; set; } + + public bool Equals(DiscordMessageSticker? other) => this.Id == other?.Id; + public override bool Equals(object obj) => Equals(obj as DiscordMessageSticker); + public override string ToString() => $"Sticker {this.Id}; {this.Name}; {this.FormatType}"; + public override int GetHashCode() + { + HashCode hash = new(); + hash.Add(this.Id); + hash.Add(this.CreationTimestamp); + hash.Add(this.Discord); + hash.Add(this.PackId); + hash.Add(this.Name); + hash.Add(this.Description); + hash.Add(this.Type); + hash.Add(this.User); + hash.Add(this.Guild); + hash.Add(this.StickerUrl); + hash.Add(this.GuildId); + hash.Add(this.Available); + hash.Add(this.SortValue); + hash.Add(this.Tags); + hash.Add(this.Asset); + hash.Add(this.PreviewAsset); + hash.Add(this.FormatType); + hash.Add(this.InternalTags); + hash.Add(this.BannerUrl); + hash.Add(this.BannerAssetId); + return hash.ToHashCode(); + } +} + +public enum DiscordStickerType +{ + Standard = 1, + Guild = 2 +} + +public enum DiscordStickerFormat +{ + PNG = 1, + APNG = 2, + LOTTIE = 3 +} diff --git a/DSharpPlus/Entities/Channel/Message/DiscordMessageStickerPack.cs b/DSharpPlus/Entities/Channel/Message/DiscordMessageStickerPack.cs index c429398a3d..c501078dc1 100644 --- a/DSharpPlus/Entities/Channel/Message/DiscordMessageStickerPack.cs +++ b/DSharpPlus/Entities/Channel/Message/DiscordMessageStickerPack.cs @@ -1,50 +1,50 @@ -using System.Collections.Generic; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents a Discord message sticker pack. -/// -public sealed class DiscordMessageStickerPack : SnowflakeObject -{ - /// - /// Gets the stickers contained in this pack. - /// - public IReadOnlyDictionary Stickers => this.stickers; - - [JsonProperty("stickers")] - internal Dictionary stickers = []; - - /// - /// Gets the name of this sticker pack. - /// - [JsonProperty("name")] - public string Name { get; internal set; } = default!; - - /// - /// Gets the Id of this pack's SKU. - /// - [JsonProperty("sku_id")] - public ulong SkuId { get; internal set; } - - /// - /// Gets the Id of this pack's cover. - /// - [JsonProperty("cover_sticker_id")] - public ulong CoverStickerId { get; internal set; } - - /// - /// Gets the description of this sticker pack. - /// - [JsonProperty("description")] - public string Description { get; internal set; } = default!; - - /// - /// Gets the Id of the sticker pack's banner image. - /// - [JsonProperty("banner_asset_id")] - public ulong BannerAssetId { get; internal set; } - - internal DiscordMessageStickerPack() { } -} +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Represents a Discord message sticker pack. +/// +public sealed class DiscordMessageStickerPack : SnowflakeObject +{ + /// + /// Gets the stickers contained in this pack. + /// + public IReadOnlyDictionary Stickers => this.stickers; + + [JsonProperty("stickers")] + internal Dictionary stickers = []; + + /// + /// Gets the name of this sticker pack. + /// + [JsonProperty("name")] + public string Name { get; internal set; } = default!; + + /// + /// Gets the Id of this pack's SKU. + /// + [JsonProperty("sku_id")] + public ulong SkuId { get; internal set; } + + /// + /// Gets the Id of this pack's cover. + /// + [JsonProperty("cover_sticker_id")] + public ulong CoverStickerId { get; internal set; } + + /// + /// Gets the description of this sticker pack. + /// + [JsonProperty("description")] + public string Description { get; internal set; } = default!; + + /// + /// Gets the Id of the sticker pack's banner image. + /// + [JsonProperty("banner_asset_id")] + public ulong BannerAssetId { get; internal set; } + + internal DiscordMessageStickerPack() { } +} diff --git a/DSharpPlus/Entities/Channel/Message/DiscordMessageType.cs b/DSharpPlus/Entities/Channel/Message/DiscordMessageType.cs index 12701a5c8c..d612246b3f 100644 --- a/DSharpPlus/Entities/Channel/Message/DiscordMessageType.cs +++ b/DSharpPlus/Entities/Channel/Message/DiscordMessageType.cs @@ -1,136 +1,136 @@ -namespace DSharpPlus.Entities; - - -/// -/// Represents the type of a message. -/// -public enum DiscordMessageType : int -{ - /// - /// Indicates a regular message. - /// - Default = 0, - - /// - /// Message indicating a recipient was added to a group direct message. - /// - RecipientAdd = 1, - - /// - /// Message indicating a recipient was removed from a group direct message. - /// - RecipientRemove = 2, - - /// - /// Message indicating a call. - /// - Call = 3, - - /// - /// Message indicating a group direct message channel rename. - /// - ChannelNameChange = 4, - - /// - /// Message indicating a group direct message channel icon change. - /// - ChannelIconChange = 5, - - /// - /// Message indicating a user pinned a message to a channel. - /// - ChannelPinnedMessage = 6, - - /// - /// Message indicating a guild member joined. Most frequently seen in newer, smaller guilds. - /// - GuildMemberJoin = 7, - - /// - /// Message indicating a member nitro boosted a guild. - /// - UserPremiumGuildSubscription = 8, - - /// - /// Message indicating a guild reached tier one of nitro boosts. - /// - TierOneUserPremiumGuildSubscription = 9, - - /// - /// Message indicating a guild reached tier two of nitro boosts. - /// - TierTwoUserPremiumGuildSubscription = 10, - - /// - /// Message indicating a guild reached tier three of nitro boosts. - /// - TierThreeUserPremiumGuildSubscription = 11, - - /// - /// Message indicating a user followed a news channel. - /// - ChannelFollowAdd = 12, - - /// - /// Message indicating a guild was removed from guild discovery. - /// - GuildDiscoveryDisqualified = 14, - - /// - /// Message indicating a guild was re-added to guild discovery. - /// - GuildDiscoveryRequalified = 15, - - /// - /// Message indicating that a guild has failed to meet guild discovery requirements for a week. - /// - GuildDiscoveryGracePeriodInitialWarning = 16, - - /// - /// Message indicating that a guild has failed to meet guild discovery requirements for 3 weeks. - /// - GuildDiscoveryGracePeriodFinalWarning = 17, - - /// - /// - /// - ThreadCreated = 18, - - /// - /// Message indicating a user replied to another user. - /// - Reply = 19, - - /// - /// Message indicating an application command was invoked. - /// - ApplicationCommand = 20, - - /// - /// - /// - ThreadStarterMessage = 21, - - /// - /// Message reminding you to invite people to help you build the server. - /// - GuildInviteReminder = 22, - - /// - /// Message indicating a context menu was executed. - /// - ContextMenuCommand = 23, - - /// - /// Message indicating an auto-moderation alert. - /// - AutoModerationAlert = 24, - - RoleSubscriptionPurchase = 25, - InteractionPremiumUpsell = 26, - StageStart = 27, - StageEnd = 28, - StageSpeaker = 29, - StageTopic = 31, - GuildApplicationPremiumSubscription = 32, -} +namespace DSharpPlus.Entities; + + +/// +/// Represents the type of a message. +/// +public enum DiscordMessageType : int +{ + /// + /// Indicates a regular message. + /// + Default = 0, + + /// + /// Message indicating a recipient was added to a group direct message. + /// + RecipientAdd = 1, + + /// + /// Message indicating a recipient was removed from a group direct message. + /// + RecipientRemove = 2, + + /// + /// Message indicating a call. + /// + Call = 3, + + /// + /// Message indicating a group direct message channel rename. + /// + ChannelNameChange = 4, + + /// + /// Message indicating a group direct message channel icon change. + /// + ChannelIconChange = 5, + + /// + /// Message indicating a user pinned a message to a channel. + /// + ChannelPinnedMessage = 6, + + /// + /// Message indicating a guild member joined. Most frequently seen in newer, smaller guilds. + /// + GuildMemberJoin = 7, + + /// + /// Message indicating a member nitro boosted a guild. + /// + UserPremiumGuildSubscription = 8, + + /// + /// Message indicating a guild reached tier one of nitro boosts. + /// + TierOneUserPremiumGuildSubscription = 9, + + /// + /// Message indicating a guild reached tier two of nitro boosts. + /// + TierTwoUserPremiumGuildSubscription = 10, + + /// + /// Message indicating a guild reached tier three of nitro boosts. + /// + TierThreeUserPremiumGuildSubscription = 11, + + /// + /// Message indicating a user followed a news channel. + /// + ChannelFollowAdd = 12, + + /// + /// Message indicating a guild was removed from guild discovery. + /// + GuildDiscoveryDisqualified = 14, + + /// + /// Message indicating a guild was re-added to guild discovery. + /// + GuildDiscoveryRequalified = 15, + + /// + /// Message indicating that a guild has failed to meet guild discovery requirements for a week. + /// + GuildDiscoveryGracePeriodInitialWarning = 16, + + /// + /// Message indicating that a guild has failed to meet guild discovery requirements for 3 weeks. + /// + GuildDiscoveryGracePeriodFinalWarning = 17, + + /// + /// + /// + ThreadCreated = 18, + + /// + /// Message indicating a user replied to another user. + /// + Reply = 19, + + /// + /// Message indicating an application command was invoked. + /// + ApplicationCommand = 20, + + /// + /// + /// + ThreadStarterMessage = 21, + + /// + /// Message reminding you to invite people to help you build the server. + /// + GuildInviteReminder = 22, + + /// + /// Message indicating a context menu was executed. + /// + ContextMenuCommand = 23, + + /// + /// Message indicating an auto-moderation alert. + /// + AutoModerationAlert = 24, + + RoleSubscriptionPurchase = 25, + InteractionPremiumUpsell = 26, + StageStart = 27, + StageEnd = 28, + StageSpeaker = 29, + StageTopic = 31, + GuildApplicationPremiumSubscription = 32, +} diff --git a/DSharpPlus/Entities/Channel/Message/DiscordReaction.cs b/DSharpPlus/Entities/Channel/Message/DiscordReaction.cs index 78ab21733d..4ee3afe388 100644 --- a/DSharpPlus/Entities/Channel/Message/DiscordReaction.cs +++ b/DSharpPlus/Entities/Channel/Message/DiscordReaction.cs @@ -1,29 +1,29 @@ -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents a reaction to a message. -/// -public class DiscordReaction -{ - /// - /// Gets the total number of users who reacted with this emoji. - /// - [JsonProperty("count", NullValueHandling = NullValueHandling.Ignore)] - public int Count { get; internal set; } - - /// - /// Gets whether the current user reacted with this emoji. - /// - [JsonProperty("me", NullValueHandling = NullValueHandling.Ignore)] - public bool IsMe { get; internal set; } - - /// - /// Gets the emoji used to react to this message. - /// - [JsonProperty("emoji", NullValueHandling = NullValueHandling.Ignore)] - public DiscordEmoji Emoji { get; internal set; } = default!; - - internal DiscordReaction() { } -} +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Represents a reaction to a message. +/// +public class DiscordReaction +{ + /// + /// Gets the total number of users who reacted with this emoji. + /// + [JsonProperty("count", NullValueHandling = NullValueHandling.Ignore)] + public int Count { get; internal set; } + + /// + /// Gets whether the current user reacted with this emoji. + /// + [JsonProperty("me", NullValueHandling = NullValueHandling.Ignore)] + public bool IsMe { get; internal set; } + + /// + /// Gets the emoji used to react to this message. + /// + [JsonProperty("emoji", NullValueHandling = NullValueHandling.Ignore)] + public DiscordEmoji Emoji { get; internal set; } = default!; + + internal DiscordReaction() { } +} diff --git a/DSharpPlus/Entities/Channel/Message/Embed/DiscordEmbed.cs b/DSharpPlus/Entities/Channel/Message/Embed/DiscordEmbed.cs index cd2029fe8f..2113861af9 100644 --- a/DSharpPlus/Entities/Channel/Message/Embed/DiscordEmbed.cs +++ b/DSharpPlus/Entities/Channel/Message/Embed/DiscordEmbed.cs @@ -1,96 +1,96 @@ -using System; -using System.Collections.Generic; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents a discord embed. -/// -public sealed class DiscordEmbed -{ - /// - /// Gets the embed's title. - /// - [JsonProperty("title", NullValueHandling = NullValueHandling.Ignore)] - public string? Title { get; internal set; } - - /// - /// Gets the embed's type. - /// - [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] - public string? Type { get; internal set; } - - /// - /// Gets the embed's description. - /// - [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] - public string? Description { get; internal set; } - - /// - /// Gets the embed's url. - /// - [JsonProperty("url", NullValueHandling = NullValueHandling.Ignore)] - public Uri? Url { get; internal set; } - - /// - /// Gets the embed's timestamp. - /// - [JsonProperty("timestamp", NullValueHandling = NullValueHandling.Ignore)] - public DateTimeOffset? Timestamp { get; internal set; } - - /// - /// Gets the embed's color. - /// - [JsonIgnore] - public DiscordColor? Color => this.color.HasValue - ? (DiscordColor)this.color.Value - : null; - - [JsonProperty("color", NullValueHandling = NullValueHandling.Include)] - internal Optional color; - - /// - /// Gets the embed's footer. - /// - [JsonProperty("footer", NullValueHandling = NullValueHandling.Ignore)] - public DiscordEmbedFooter? Footer { get; internal set; } - - /// - /// Gets the embed's image. - /// - [JsonProperty("image", NullValueHandling = NullValueHandling.Ignore)] - public DiscordEmbedImage? Image { get; internal set; } - - /// - /// Gets the embed's thumbnail. - /// - [JsonProperty("thumbnail", NullValueHandling = NullValueHandling.Ignore)] - public DiscordEmbedThumbnail? Thumbnail { get; internal set; } - - /// - /// Gets the embed's video. - /// - [JsonProperty("video", NullValueHandling = NullValueHandling.Ignore)] - public DiscordEmbedVideo? Video { get; internal set; } - - /// - /// Gets the embed's provider. - /// - [JsonProperty("provider", NullValueHandling = NullValueHandling.Ignore)] - public DiscordEmbedProvider? Provider { get; internal set; } - - /// - /// Gets the embed's author. - /// - [JsonProperty("author", NullValueHandling = NullValueHandling.Ignore)] - public DiscordEmbedAuthor? Author { get; internal set; } - - /// - /// Gets the embed's fields. - /// - [JsonProperty("fields", NullValueHandling = NullValueHandling.Ignore)] - public IReadOnlyList? Fields { get; internal set; } - - internal DiscordEmbed() { } -} +using System; +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Represents a discord embed. +/// +public sealed class DiscordEmbed +{ + /// + /// Gets the embed's title. + /// + [JsonProperty("title", NullValueHandling = NullValueHandling.Ignore)] + public string? Title { get; internal set; } + + /// + /// Gets the embed's type. + /// + [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] + public string? Type { get; internal set; } + + /// + /// Gets the embed's description. + /// + [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] + public string? Description { get; internal set; } + + /// + /// Gets the embed's url. + /// + [JsonProperty("url", NullValueHandling = NullValueHandling.Ignore)] + public Uri? Url { get; internal set; } + + /// + /// Gets the embed's timestamp. + /// + [JsonProperty("timestamp", NullValueHandling = NullValueHandling.Ignore)] + public DateTimeOffset? Timestamp { get; internal set; } + + /// + /// Gets the embed's color. + /// + [JsonIgnore] + public DiscordColor? Color => this.color.HasValue + ? (DiscordColor)this.color.Value + : null; + + [JsonProperty("color", NullValueHandling = NullValueHandling.Include)] + internal Optional color; + + /// + /// Gets the embed's footer. + /// + [JsonProperty("footer", NullValueHandling = NullValueHandling.Ignore)] + public DiscordEmbedFooter? Footer { get; internal set; } + + /// + /// Gets the embed's image. + /// + [JsonProperty("image", NullValueHandling = NullValueHandling.Ignore)] + public DiscordEmbedImage? Image { get; internal set; } + + /// + /// Gets the embed's thumbnail. + /// + [JsonProperty("thumbnail", NullValueHandling = NullValueHandling.Ignore)] + public DiscordEmbedThumbnail? Thumbnail { get; internal set; } + + /// + /// Gets the embed's video. + /// + [JsonProperty("video", NullValueHandling = NullValueHandling.Ignore)] + public DiscordEmbedVideo? Video { get; internal set; } + + /// + /// Gets the embed's provider. + /// + [JsonProperty("provider", NullValueHandling = NullValueHandling.Ignore)] + public DiscordEmbedProvider? Provider { get; internal set; } + + /// + /// Gets the embed's author. + /// + [JsonProperty("author", NullValueHandling = NullValueHandling.Ignore)] + public DiscordEmbedAuthor? Author { get; internal set; } + + /// + /// Gets the embed's fields. + /// + [JsonProperty("fields", NullValueHandling = NullValueHandling.Ignore)] + public IReadOnlyList? Fields { get; internal set; } + + internal DiscordEmbed() { } +} diff --git a/DSharpPlus/Entities/Channel/Message/Embed/DiscordEmbedAuthor.cs b/DSharpPlus/Entities/Channel/Message/Embed/DiscordEmbedAuthor.cs index bbad4a9759..a8db0054d6 100644 --- a/DSharpPlus/Entities/Channel/Message/Embed/DiscordEmbedAuthor.cs +++ b/DSharpPlus/Entities/Channel/Message/Embed/DiscordEmbedAuthor.cs @@ -1,37 +1,37 @@ -using System; -using DSharpPlus.Net; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Gets the author of a discord embed. -/// -public sealed class DiscordEmbedAuthor -{ - /// - /// Gets the name of the author. - /// - [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] - public string? Name { get; set; } - - /// - /// Gets the url of the author. - /// - [JsonProperty("url", NullValueHandling = NullValueHandling.Ignore)] - public Uri? Url { get; set; } - - /// - /// Gets the url of the author's icon. - /// - [JsonProperty("icon_url", NullValueHandling = NullValueHandling.Ignore)] - public DiscordUri? IconUrl { get; set; } - - /// - /// Gets the proxied url of the author's icon. - /// - [JsonProperty("proxy_icon_url", NullValueHandling = NullValueHandling.Ignore)] - public DiscordUri? ProxyIconUrl { get; internal set; } - - internal DiscordEmbedAuthor() { } -} +using System; +using DSharpPlus.Net; +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Gets the author of a discord embed. +/// +public sealed class DiscordEmbedAuthor +{ + /// + /// Gets the name of the author. + /// + [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] + public string? Name { get; set; } + + /// + /// Gets the url of the author. + /// + [JsonProperty("url", NullValueHandling = NullValueHandling.Ignore)] + public Uri? Url { get; set; } + + /// + /// Gets the url of the author's icon. + /// + [JsonProperty("icon_url", NullValueHandling = NullValueHandling.Ignore)] + public DiscordUri? IconUrl { get; set; } + + /// + /// Gets the proxied url of the author's icon. + /// + [JsonProperty("proxy_icon_url", NullValueHandling = NullValueHandling.Ignore)] + public DiscordUri? ProxyIconUrl { get; internal set; } + + internal DiscordEmbedAuthor() { } +} diff --git a/DSharpPlus/Entities/Channel/Message/Embed/DiscordEmbedBuilder.cs b/DSharpPlus/Entities/Channel/Message/Embed/DiscordEmbedBuilder.cs index 73fb301bbf..a91db36afc 100644 --- a/DSharpPlus/Entities/Channel/Message/Embed/DiscordEmbedBuilder.cs +++ b/DSharpPlus/Entities/Channel/Message/Embed/DiscordEmbedBuilder.cs @@ -1,606 +1,606 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; -using DSharpPlus.Net; - -namespace DSharpPlus.Entities; - -/// -/// Constructs embeds. -/// -public sealed class DiscordEmbedBuilder -{ - /// - /// Gets or sets the embed's title. - /// - public string? Title - { - get => this.title; - set - { - if (value != null && value.Length > 256) - { - throw new ArgumentException("Title length cannot exceed 256 characters.", nameof(value)); - } - - this.title = value; - } - } - private string? title; - - /// - /// Gets or sets the embed's description. - /// - public string? Description - { - get => this.description; - set - { - if (value != null && value.Length > 4096) - { - throw new ArgumentException("Description length cannot exceed 4096 characters.", nameof(value)); - } - - this.description = value; - } - } - private string? description; - - /// - /// Gets or sets the url for the embed's title. - /// - public string? Url - { - get => this.url?.ToString(); - set => this.url = string.IsNullOrEmpty(value) ? null : new Uri(value); - } - private Uri? url; - - /// - /// Gets or sets the embed's color. - /// - public DiscordColor? Color { get; set; } - - /// - /// Gets or sets the embed's timestamp. - /// - public DateTimeOffset? Timestamp { get; set; } - - /// - /// Gets or sets the embed's image url. - /// - public string? ImageUrl - { - get => this.imageUri?.ToString(); - set => this.imageUri = string.IsNullOrEmpty(value) ? null : new DiscordUri(value); - } - private DiscordUri? imageUri; - - /// - /// Gets or sets the embed's author. - /// - public EmbedAuthor? Author { get; set; } - - /// - /// Gets or sets the embed's footer. - /// - public EmbedFooter? Footer { get; set; } - - /// - /// Gets or sets the embed's thumbnail. - /// - public EmbedThumbnail? Thumbnail { get; set; } - - /// - /// Gets the embed's fields. - /// - public IReadOnlyList Fields { get; } - private readonly List fields = []; - - /// - /// Constructs a new empty embed builder. - /// - public DiscordEmbedBuilder() => this.Fields = new ReadOnlyCollection(this.fields); - - /// - /// Constructs a new embed builder using another embed as prototype. - /// - /// Embed to use as prototype. - public DiscordEmbedBuilder(DiscordEmbed original) - : this() - { - this.Title = original.Title; - this.Description = original.Description; - this.Url = original.Url?.ToString(); - this.ImageUrl = original.Image?.Url?.ToString(); - this.Color = original.Color; - this.Timestamp = original.Timestamp; - - if (original.Thumbnail != null) - { - this.Thumbnail = new EmbedThumbnail - { - Url = original.Thumbnail.Url?.ToString(), - Height = original.Thumbnail.Height, - Width = original.Thumbnail.Width - }; - } - - if (original.Author != null) - { - this.Author = new EmbedAuthor - { - IconUrl = original.Author.IconUrl?.ToString(), - Name = original.Author.Name, - Url = original.Author.Url?.ToString() - }; - } - - if (original.Footer != null) - { - this.Footer = new EmbedFooter - { - IconUrl = original.Footer.IconUrl?.ToString(), - Text = original.Footer.Text - }; - } - - if (original.Fields?.Any() == true) - { - this.fields.AddRange(original.Fields); - } - - while (this.fields.Count > 25) - { - this.fields.RemoveAt(this.fields.Count - 1); - } - } - - /// - /// Sets the embed's title. - /// - /// Title to set. - /// This embed builder. - public DiscordEmbedBuilder WithTitle(string title) - { - this.Title = title; - return this; - } - - /// - /// Sets the embed's description. - /// - /// Description to set. - /// This embed builder. - public DiscordEmbedBuilder WithDescription(string description) - { - this.Description = description; - return this; - } - - /// - /// Sets the embed's title url. - /// - /// Title url to set. - /// This embed builder. - public DiscordEmbedBuilder WithUrl(string url) - { - this.Url = url; - return this; - } - - /// - /// Sets the embed's title url. - /// - /// Title url to set. - /// This embed builder. - public DiscordEmbedBuilder WithUrl(Uri url) - { - this.url = url; - return this; - } - - /// - /// Sets the embed's color. - /// - /// Embed color to set. - /// This embed builder. - public DiscordEmbedBuilder WithColor(DiscordColor color) - { - this.Color = color; - return this; - } - - /// - /// Sets the embed's timestamp. - /// - /// Timestamp to set. - /// This embed builder. - public DiscordEmbedBuilder WithTimestamp(DateTimeOffset? timestamp) - { - this.Timestamp = timestamp; - return this; - } - - /// - /// Sets the embed's timestamp. - /// - /// Timestamp to set. - /// This embed builder. - public DiscordEmbedBuilder WithTimestamp(DateTime? timestamp) - { - this.Timestamp = timestamp == null ? null : new DateTimeOffset(timestamp.Value); - return this; - } - - /// - /// Sets the embed's timestamp based on a snowflake. - /// - /// Snowflake to calculate timestamp from. - /// This embed builder. - public DiscordEmbedBuilder WithTimestamp(ulong snowflake) - { - this.Timestamp = new DateTimeOffset(2015, 1, 1, 0, 0, 0, TimeSpan.Zero).AddMilliseconds(snowflake >> 22); - return this; - } - - /// - /// Sets the embed's image url. - /// - /// Image url to set. - /// This embed builder. - public DiscordEmbedBuilder WithImageUrl(string url) - { - this.ImageUrl = url; - return this; - } - - /// - /// Sets the embed's image url. - /// - /// Image url to set. - /// This embed builder. - public DiscordEmbedBuilder WithImageUrl(Uri url) - { - this.imageUri = new DiscordUri(url); - return this; - } - - /// - /// Sets the embed's thumbnail. - /// - /// Thumbnail url to set. - /// The height of the thumbnail to set. - /// The width of the thumbnail to set. - /// This embed builder. - public DiscordEmbedBuilder WithThumbnail(string url, int height = 0, int width = 0) - { - this.Thumbnail = new EmbedThumbnail - { - Url = url, - Height = height, - Width = width - }; - - return this; - } - - /// - /// Sets the embed's thumbnail. - /// - /// Thumbnail url to set. - /// The height of the thumbnail to set. - /// The width of the thumbnail to set. - /// This embed builder. - public DiscordEmbedBuilder WithThumbnail(Uri url, int height = 0, int width = 0) - { - this.Thumbnail = new EmbedThumbnail - { - uri = new DiscordUri(url), - Height = height, - Width = width - }; - - return this; - } - - /// - /// Sets the embed's author. - /// - /// Author's name. - /// Author's url. - /// Author icon's url. - /// This embed builder. - public DiscordEmbedBuilder WithAuthor(string? name = null, string? url = null, string? iconUrl = null) - { - this.Author = string.IsNullOrEmpty(name) && string.IsNullOrEmpty(url) && string.IsNullOrEmpty(iconUrl) - ? null - : new EmbedAuthor - { - Name = name, - Url = url, - IconUrl = iconUrl - }; - return this; - } - - /// - /// Sets the embed's footer. - /// - /// Footer's text. - /// Footer icon's url. - /// This embed builder. - public DiscordEmbedBuilder WithFooter(string? text = null, string? iconUrl = null) - { - if (text is not null && text.Length > 2048) - { - throw new ArgumentException("Footer text length cannot exceed 2048 characters.", nameof(text)); - } - - this.Footer = string.IsNullOrEmpty(text) && string.IsNullOrEmpty(iconUrl) - ? null - : new EmbedFooter - { - Text = text, - IconUrl = iconUrl - }; - return this; - } - - /// - /// Adds a field to this embed. - /// - /// Name of the field to add. - /// Value of the field to add. - /// Whether the field is to be inline or not. - /// This embed builder. - public DiscordEmbedBuilder AddField(string name, string value, bool inline = false) - { - if (string.IsNullOrWhiteSpace(name)) - { - ArgumentNullException.ThrowIfNull(name); - - throw new ArgumentException("Name cannot be empty or whitespace.", nameof(name)); - } - - if (string.IsNullOrWhiteSpace(value)) - { - ArgumentNullException.ThrowIfNull(value); - - throw new ArgumentException("Value cannot be empty or whitespace.", nameof(value)); - } - - if (name.Length > 256) - { - throw new ArgumentException("Embed field name length cannot exceed 256 characters."); - } - - if (value.Length > 1024) - { - throw new ArgumentException("Embed field value length cannot exceed 1024 characters."); - } - - if (this.fields.Count >= 25) - { - throw new InvalidOperationException("Cannot add more than 25 fields."); - } - - this.fields.Add(new DiscordEmbedField - { - Inline = inline, - Name = name, - Value = value - }); - - return this; - } - - /// - /// Removes a field of the specified index from this embed. - /// - /// Index of the field to remove. - /// This embed builder. - public DiscordEmbedBuilder RemoveFieldAt(int index) - { - this.fields.RemoveAt(index); - return this; - } - - /// - /// Removes fields of the specified range from this embed. - /// - /// Index of the first field to remove. - /// Number of fields to remove. - /// This embed builder. - public DiscordEmbedBuilder RemoveFieldRange(int index, int count) - { - this.fields.RemoveRange(index, count); - return this; - } - - /// - /// Removes all fields from this embed. - /// - /// This embed builder. - public DiscordEmbedBuilder ClearFields() - { - this.fields.Clear(); - return this; - } - - /// - /// Constructs a new embed from data supplied to this builder. - /// - /// New discord embed. - public DiscordEmbed Build() - { - DiscordEmbed embed = new() - { - Title = this.title, - Description = this.description, - Url = this.url, - color = this.Color is not null ? Optional.FromValue(this.Color.Value.Value) : Optional.FromNoValue(), - Timestamp = this.Timestamp - }; - - if (this.Footer is not null) - { - embed.Footer = new DiscordEmbedFooter - { - Text = this.Footer.Text, - IconUrl = this.Footer.iconUri - }; - } - - if (this.Author is not null) - { - embed.Author = new DiscordEmbedAuthor - { - Name = this.Author.Name, - Url = this.Author.uri, - IconUrl = this.Author.iconUri - }; - } - - if (this.imageUri is not null) - { - embed.Image = new DiscordEmbedImage { Url = this.imageUri.Value }; - } - - if (this.Thumbnail is not null) - { - embed.Thumbnail = new DiscordEmbedThumbnail - { - Url = this.Thumbnail.uri, - Height = this.Thumbnail.Height, - Width = this.Thumbnail.Width - }; - } - - embed.Fields = new ReadOnlyCollection(new List(this.fields)); // copy the list, don't wrap it, prevents mutation - - return embed; - } - - /// - /// Implicitly converts this builder to an embed. - /// - /// Builder to convert. - public static implicit operator DiscordEmbed(DiscordEmbedBuilder builder) - => builder.Build(); - - /// - /// Represents an embed author. - /// - public class EmbedAuthor - { - /// - /// Gets or sets the name of the author. - /// - public string? Name - { - get => this.name; - set - { - if (value != null && value.Length > 256) - { - throw new ArgumentException("Author name length cannot exceed 256 characters.", nameof(value)); - } - - this.name = value; - } - } - private string? name; - - /// - /// Gets or sets the Url to which the author's link leads. - /// - public string? Url - { - get => this.uri?.ToString(); - set => this.uri = string.IsNullOrEmpty(value) ? null : new Uri(value); - } - internal Uri? uri; - - /// - /// Gets or sets the Author's icon url. - /// - public string? IconUrl - { - get => this.iconUri?.ToString(); - set => this.iconUri = string.IsNullOrEmpty(value) ? null : new DiscordUri(value); - } - internal DiscordUri? iconUri; - } - - /// - /// Represents an embed footer. - /// - public class EmbedFooter - { - /// - /// Gets or sets the text of the footer. - /// - public string? Text - { - get => this.text; - set - { - if (value != null && value.Length > 2048) - { - throw new ArgumentException("Footer text length cannot exceed 2048 characters.", nameof(value)); - } - - this.text = value; - } - } - private string? text; - - /// - /// Gets or sets the Url - /// - public string? IconUrl - { - get => this.iconUri?.ToString(); - set => this.iconUri = string.IsNullOrEmpty(value) ? null : new DiscordUri(value); - } - internal DiscordUri? iconUri; - } - - /// - /// Represents an embed thumbnail. - /// - public class EmbedThumbnail - { - /// - /// Gets or sets the thumbnail's image url. - /// - public string? Url - { - get => this.uri?.ToString(); - set => this.uri = string.IsNullOrEmpty(value) ? null : new DiscordUri(value); - } - internal DiscordUri? uri; - - /// - /// Gets or sets the thumbnail's height. - /// - public int Height - { - get => this.height; - set => this.height = value >= 0 ? value : 0; - } - private int height; - - /// - /// Gets or sets the thumbnail's width. - /// - public int Width - { - get => this.width; - set => this.width = value >= 0 ? value : 0; - } - private int width; - } -} +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using DSharpPlus.Net; + +namespace DSharpPlus.Entities; + +/// +/// Constructs embeds. +/// +public sealed class DiscordEmbedBuilder +{ + /// + /// Gets or sets the embed's title. + /// + public string? Title + { + get => this.title; + set + { + if (value != null && value.Length > 256) + { + throw new ArgumentException("Title length cannot exceed 256 characters.", nameof(value)); + } + + this.title = value; + } + } + private string? title; + + /// + /// Gets or sets the embed's description. + /// + public string? Description + { + get => this.description; + set + { + if (value != null && value.Length > 4096) + { + throw new ArgumentException("Description length cannot exceed 4096 characters.", nameof(value)); + } + + this.description = value; + } + } + private string? description; + + /// + /// Gets or sets the url for the embed's title. + /// + public string? Url + { + get => this.url?.ToString(); + set => this.url = string.IsNullOrEmpty(value) ? null : new Uri(value); + } + private Uri? url; + + /// + /// Gets or sets the embed's color. + /// + public DiscordColor? Color { get; set; } + + /// + /// Gets or sets the embed's timestamp. + /// + public DateTimeOffset? Timestamp { get; set; } + + /// + /// Gets or sets the embed's image url. + /// + public string? ImageUrl + { + get => this.imageUri?.ToString(); + set => this.imageUri = string.IsNullOrEmpty(value) ? null : new DiscordUri(value); + } + private DiscordUri? imageUri; + + /// + /// Gets or sets the embed's author. + /// + public EmbedAuthor? Author { get; set; } + + /// + /// Gets or sets the embed's footer. + /// + public EmbedFooter? Footer { get; set; } + + /// + /// Gets or sets the embed's thumbnail. + /// + public EmbedThumbnail? Thumbnail { get; set; } + + /// + /// Gets the embed's fields. + /// + public IReadOnlyList Fields { get; } + private readonly List fields = []; + + /// + /// Constructs a new empty embed builder. + /// + public DiscordEmbedBuilder() => this.Fields = new ReadOnlyCollection(this.fields); + + /// + /// Constructs a new embed builder using another embed as prototype. + /// + /// Embed to use as prototype. + public DiscordEmbedBuilder(DiscordEmbed original) + : this() + { + this.Title = original.Title; + this.Description = original.Description; + this.Url = original.Url?.ToString(); + this.ImageUrl = original.Image?.Url?.ToString(); + this.Color = original.Color; + this.Timestamp = original.Timestamp; + + if (original.Thumbnail != null) + { + this.Thumbnail = new EmbedThumbnail + { + Url = original.Thumbnail.Url?.ToString(), + Height = original.Thumbnail.Height, + Width = original.Thumbnail.Width + }; + } + + if (original.Author != null) + { + this.Author = new EmbedAuthor + { + IconUrl = original.Author.IconUrl?.ToString(), + Name = original.Author.Name, + Url = original.Author.Url?.ToString() + }; + } + + if (original.Footer != null) + { + this.Footer = new EmbedFooter + { + IconUrl = original.Footer.IconUrl?.ToString(), + Text = original.Footer.Text + }; + } + + if (original.Fields?.Any() == true) + { + this.fields.AddRange(original.Fields); + } + + while (this.fields.Count > 25) + { + this.fields.RemoveAt(this.fields.Count - 1); + } + } + + /// + /// Sets the embed's title. + /// + /// Title to set. + /// This embed builder. + public DiscordEmbedBuilder WithTitle(string title) + { + this.Title = title; + return this; + } + + /// + /// Sets the embed's description. + /// + /// Description to set. + /// This embed builder. + public DiscordEmbedBuilder WithDescription(string description) + { + this.Description = description; + return this; + } + + /// + /// Sets the embed's title url. + /// + /// Title url to set. + /// This embed builder. + public DiscordEmbedBuilder WithUrl(string url) + { + this.Url = url; + return this; + } + + /// + /// Sets the embed's title url. + /// + /// Title url to set. + /// This embed builder. + public DiscordEmbedBuilder WithUrl(Uri url) + { + this.url = url; + return this; + } + + /// + /// Sets the embed's color. + /// + /// Embed color to set. + /// This embed builder. + public DiscordEmbedBuilder WithColor(DiscordColor color) + { + this.Color = color; + return this; + } + + /// + /// Sets the embed's timestamp. + /// + /// Timestamp to set. + /// This embed builder. + public DiscordEmbedBuilder WithTimestamp(DateTimeOffset? timestamp) + { + this.Timestamp = timestamp; + return this; + } + + /// + /// Sets the embed's timestamp. + /// + /// Timestamp to set. + /// This embed builder. + public DiscordEmbedBuilder WithTimestamp(DateTime? timestamp) + { + this.Timestamp = timestamp == null ? null : new DateTimeOffset(timestamp.Value); + return this; + } + + /// + /// Sets the embed's timestamp based on a snowflake. + /// + /// Snowflake to calculate timestamp from. + /// This embed builder. + public DiscordEmbedBuilder WithTimestamp(ulong snowflake) + { + this.Timestamp = new DateTimeOffset(2015, 1, 1, 0, 0, 0, TimeSpan.Zero).AddMilliseconds(snowflake >> 22); + return this; + } + + /// + /// Sets the embed's image url. + /// + /// Image url to set. + /// This embed builder. + public DiscordEmbedBuilder WithImageUrl(string url) + { + this.ImageUrl = url; + return this; + } + + /// + /// Sets the embed's image url. + /// + /// Image url to set. + /// This embed builder. + public DiscordEmbedBuilder WithImageUrl(Uri url) + { + this.imageUri = new DiscordUri(url); + return this; + } + + /// + /// Sets the embed's thumbnail. + /// + /// Thumbnail url to set. + /// The height of the thumbnail to set. + /// The width of the thumbnail to set. + /// This embed builder. + public DiscordEmbedBuilder WithThumbnail(string url, int height = 0, int width = 0) + { + this.Thumbnail = new EmbedThumbnail + { + Url = url, + Height = height, + Width = width + }; + + return this; + } + + /// + /// Sets the embed's thumbnail. + /// + /// Thumbnail url to set. + /// The height of the thumbnail to set. + /// The width of the thumbnail to set. + /// This embed builder. + public DiscordEmbedBuilder WithThumbnail(Uri url, int height = 0, int width = 0) + { + this.Thumbnail = new EmbedThumbnail + { + uri = new DiscordUri(url), + Height = height, + Width = width + }; + + return this; + } + + /// + /// Sets the embed's author. + /// + /// Author's name. + /// Author's url. + /// Author icon's url. + /// This embed builder. + public DiscordEmbedBuilder WithAuthor(string? name = null, string? url = null, string? iconUrl = null) + { + this.Author = string.IsNullOrEmpty(name) && string.IsNullOrEmpty(url) && string.IsNullOrEmpty(iconUrl) + ? null + : new EmbedAuthor + { + Name = name, + Url = url, + IconUrl = iconUrl + }; + return this; + } + + /// + /// Sets the embed's footer. + /// + /// Footer's text. + /// Footer icon's url. + /// This embed builder. + public DiscordEmbedBuilder WithFooter(string? text = null, string? iconUrl = null) + { + if (text is not null && text.Length > 2048) + { + throw new ArgumentException("Footer text length cannot exceed 2048 characters.", nameof(text)); + } + + this.Footer = string.IsNullOrEmpty(text) && string.IsNullOrEmpty(iconUrl) + ? null + : new EmbedFooter + { + Text = text, + IconUrl = iconUrl + }; + return this; + } + + /// + /// Adds a field to this embed. + /// + /// Name of the field to add. + /// Value of the field to add. + /// Whether the field is to be inline or not. + /// This embed builder. + public DiscordEmbedBuilder AddField(string name, string value, bool inline = false) + { + if (string.IsNullOrWhiteSpace(name)) + { + ArgumentNullException.ThrowIfNull(name); + + throw new ArgumentException("Name cannot be empty or whitespace.", nameof(name)); + } + + if (string.IsNullOrWhiteSpace(value)) + { + ArgumentNullException.ThrowIfNull(value); + + throw new ArgumentException("Value cannot be empty or whitespace.", nameof(value)); + } + + if (name.Length > 256) + { + throw new ArgumentException("Embed field name length cannot exceed 256 characters."); + } + + if (value.Length > 1024) + { + throw new ArgumentException("Embed field value length cannot exceed 1024 characters."); + } + + if (this.fields.Count >= 25) + { + throw new InvalidOperationException("Cannot add more than 25 fields."); + } + + this.fields.Add(new DiscordEmbedField + { + Inline = inline, + Name = name, + Value = value + }); + + return this; + } + + /// + /// Removes a field of the specified index from this embed. + /// + /// Index of the field to remove. + /// This embed builder. + public DiscordEmbedBuilder RemoveFieldAt(int index) + { + this.fields.RemoveAt(index); + return this; + } + + /// + /// Removes fields of the specified range from this embed. + /// + /// Index of the first field to remove. + /// Number of fields to remove. + /// This embed builder. + public DiscordEmbedBuilder RemoveFieldRange(int index, int count) + { + this.fields.RemoveRange(index, count); + return this; + } + + /// + /// Removes all fields from this embed. + /// + /// This embed builder. + public DiscordEmbedBuilder ClearFields() + { + this.fields.Clear(); + return this; + } + + /// + /// Constructs a new embed from data supplied to this builder. + /// + /// New discord embed. + public DiscordEmbed Build() + { + DiscordEmbed embed = new() + { + Title = this.title, + Description = this.description, + Url = this.url, + color = this.Color is not null ? Optional.FromValue(this.Color.Value.Value) : Optional.FromNoValue(), + Timestamp = this.Timestamp + }; + + if (this.Footer is not null) + { + embed.Footer = new DiscordEmbedFooter + { + Text = this.Footer.Text, + IconUrl = this.Footer.iconUri + }; + } + + if (this.Author is not null) + { + embed.Author = new DiscordEmbedAuthor + { + Name = this.Author.Name, + Url = this.Author.uri, + IconUrl = this.Author.iconUri + }; + } + + if (this.imageUri is not null) + { + embed.Image = new DiscordEmbedImage { Url = this.imageUri.Value }; + } + + if (this.Thumbnail is not null) + { + embed.Thumbnail = new DiscordEmbedThumbnail + { + Url = this.Thumbnail.uri, + Height = this.Thumbnail.Height, + Width = this.Thumbnail.Width + }; + } + + embed.Fields = new ReadOnlyCollection(new List(this.fields)); // copy the list, don't wrap it, prevents mutation + + return embed; + } + + /// + /// Implicitly converts this builder to an embed. + /// + /// Builder to convert. + public static implicit operator DiscordEmbed(DiscordEmbedBuilder builder) + => builder.Build(); + + /// + /// Represents an embed author. + /// + public class EmbedAuthor + { + /// + /// Gets or sets the name of the author. + /// + public string? Name + { + get => this.name; + set + { + if (value != null && value.Length > 256) + { + throw new ArgumentException("Author name length cannot exceed 256 characters.", nameof(value)); + } + + this.name = value; + } + } + private string? name; + + /// + /// Gets or sets the Url to which the author's link leads. + /// + public string? Url + { + get => this.uri?.ToString(); + set => this.uri = string.IsNullOrEmpty(value) ? null : new Uri(value); + } + internal Uri? uri; + + /// + /// Gets or sets the Author's icon url. + /// + public string? IconUrl + { + get => this.iconUri?.ToString(); + set => this.iconUri = string.IsNullOrEmpty(value) ? null : new DiscordUri(value); + } + internal DiscordUri? iconUri; + } + + /// + /// Represents an embed footer. + /// + public class EmbedFooter + { + /// + /// Gets or sets the text of the footer. + /// + public string? Text + { + get => this.text; + set + { + if (value != null && value.Length > 2048) + { + throw new ArgumentException("Footer text length cannot exceed 2048 characters.", nameof(value)); + } + + this.text = value; + } + } + private string? text; + + /// + /// Gets or sets the Url + /// + public string? IconUrl + { + get => this.iconUri?.ToString(); + set => this.iconUri = string.IsNullOrEmpty(value) ? null : new DiscordUri(value); + } + internal DiscordUri? iconUri; + } + + /// + /// Represents an embed thumbnail. + /// + public class EmbedThumbnail + { + /// + /// Gets or sets the thumbnail's image url. + /// + public string? Url + { + get => this.uri?.ToString(); + set => this.uri = string.IsNullOrEmpty(value) ? null : new DiscordUri(value); + } + internal DiscordUri? uri; + + /// + /// Gets or sets the thumbnail's height. + /// + public int Height + { + get => this.height; + set => this.height = value >= 0 ? value : 0; + } + private int height; + + /// + /// Gets or sets the thumbnail's width. + /// + public int Width + { + get => this.width; + set => this.width = value >= 0 ? value : 0; + } + private int width; + } +} diff --git a/DSharpPlus/Entities/Channel/Message/Embed/DiscordEmbedField.cs b/DSharpPlus/Entities/Channel/Message/Embed/DiscordEmbedField.cs index 3f5fda88e6..9b70c22ed4 100644 --- a/DSharpPlus/Entities/Channel/Message/Embed/DiscordEmbedField.cs +++ b/DSharpPlus/Entities/Channel/Message/Embed/DiscordEmbedField.cs @@ -1,29 +1,29 @@ -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents a field inside a discord embed. -/// -public sealed class DiscordEmbedField -{ - /// - /// Gets the name of the field. - /// - [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] - public string? Name { get; set; } - - /// - /// Gets the value of the field. - /// - [JsonProperty("value", NullValueHandling = NullValueHandling.Ignore)] - public string? Value { get; set; } - - /// - /// Gets whether or not this field should display inline. - /// - [JsonProperty("inline", NullValueHandling = NullValueHandling.Ignore)] - public bool Inline { get; set; } - - internal DiscordEmbedField() { } -} +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Represents a field inside a discord embed. +/// +public sealed class DiscordEmbedField +{ + /// + /// Gets the name of the field. + /// + [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] + public string? Name { get; set; } + + /// + /// Gets the value of the field. + /// + [JsonProperty("value", NullValueHandling = NullValueHandling.Ignore)] + public string? Value { get; set; } + + /// + /// Gets whether or not this field should display inline. + /// + [JsonProperty("inline", NullValueHandling = NullValueHandling.Ignore)] + public bool Inline { get; set; } + + internal DiscordEmbedField() { } +} diff --git a/DSharpPlus/Entities/Channel/Message/Embed/DiscordEmbedFooter.cs b/DSharpPlus/Entities/Channel/Message/Embed/DiscordEmbedFooter.cs index 33aab4823e..56aa646aaa 100644 --- a/DSharpPlus/Entities/Channel/Message/Embed/DiscordEmbedFooter.cs +++ b/DSharpPlus/Entities/Channel/Message/Embed/DiscordEmbedFooter.cs @@ -1,30 +1,30 @@ -using DSharpPlus.Net; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents a footer in an embed. -/// -public sealed class DiscordEmbedFooter -{ - /// - /// Gets the footer's text. - /// - [JsonProperty("text", NullValueHandling = NullValueHandling.Ignore)] - public string? Text { get; internal set; } - - /// - /// Gets the url of the footer's icon. - /// - [JsonProperty("icon_url", NullValueHandling = NullValueHandling.Ignore)] - public DiscordUri? IconUrl { get; internal set; } - - /// - /// Gets the proxied url of the footer's icon. - /// - [JsonProperty("proxy_icon_url", NullValueHandling = NullValueHandling.Ignore)] - public DiscordUri? ProxyIconUrl { get; internal set; } - - internal DiscordEmbedFooter() { } -} +using DSharpPlus.Net; +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Represents a footer in an embed. +/// +public sealed class DiscordEmbedFooter +{ + /// + /// Gets the footer's text. + /// + [JsonProperty("text", NullValueHandling = NullValueHandling.Ignore)] + public string? Text { get; internal set; } + + /// + /// Gets the url of the footer's icon. + /// + [JsonProperty("icon_url", NullValueHandling = NullValueHandling.Ignore)] + public DiscordUri? IconUrl { get; internal set; } + + /// + /// Gets the proxied url of the footer's icon. + /// + [JsonProperty("proxy_icon_url", NullValueHandling = NullValueHandling.Ignore)] + public DiscordUri? ProxyIconUrl { get; internal set; } + + internal DiscordEmbedFooter() { } +} diff --git a/DSharpPlus/Entities/Channel/Message/Embed/DiscordEmbedImage.cs b/DSharpPlus/Entities/Channel/Message/Embed/DiscordEmbedImage.cs index 837abcbb86..7addc8ad72 100644 --- a/DSharpPlus/Entities/Channel/Message/Embed/DiscordEmbedImage.cs +++ b/DSharpPlus/Entities/Channel/Message/Embed/DiscordEmbedImage.cs @@ -1,36 +1,36 @@ -using DSharpPlus.Net; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents an image in an embed. -/// -public sealed class DiscordEmbedImage -{ - /// - /// Gets the source url of the image. - /// - [JsonProperty("url", NullValueHandling = NullValueHandling.Ignore)] - public DiscordUri? Url { get; internal set; } - - /// - /// Gets a proxied url of the image. - /// - [JsonProperty("proxy_url", NullValueHandling = NullValueHandling.Ignore)] - public DiscordUri? ProxyUrl { get; internal set; } - - /// - /// Gets the height of the image. - /// - [JsonProperty("height", NullValueHandling = NullValueHandling.Ignore)] - public int Height { get; internal set; } - - /// - /// Gets the width of the image. - /// - [JsonProperty("width", NullValueHandling = NullValueHandling.Ignore)] - public int Width { get; internal set; } - - internal DiscordEmbedImage() { } -} +using DSharpPlus.Net; +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Represents an image in an embed. +/// +public sealed class DiscordEmbedImage +{ + /// + /// Gets the source url of the image. + /// + [JsonProperty("url", NullValueHandling = NullValueHandling.Ignore)] + public DiscordUri? Url { get; internal set; } + + /// + /// Gets a proxied url of the image. + /// + [JsonProperty("proxy_url", NullValueHandling = NullValueHandling.Ignore)] + public DiscordUri? ProxyUrl { get; internal set; } + + /// + /// Gets the height of the image. + /// + [JsonProperty("height", NullValueHandling = NullValueHandling.Ignore)] + public int Height { get; internal set; } + + /// + /// Gets the width of the image. + /// + [JsonProperty("width", NullValueHandling = NullValueHandling.Ignore)] + public int Width { get; internal set; } + + internal DiscordEmbedImage() { } +} diff --git a/DSharpPlus/Entities/Channel/Message/Embed/DiscordEmbedProvider.cs b/DSharpPlus/Entities/Channel/Message/Embed/DiscordEmbedProvider.cs index 89de526e43..199347b492 100644 --- a/DSharpPlus/Entities/Channel/Message/Embed/DiscordEmbedProvider.cs +++ b/DSharpPlus/Entities/Channel/Message/Embed/DiscordEmbedProvider.cs @@ -1,24 +1,24 @@ -using System; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents an embed provider. -/// -public sealed class DiscordEmbedProvider -{ - /// - /// Gets the name of the provider. - /// - [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] - public string? Name { get; internal set; } - - /// - /// Gets the url of the provider. - /// - [JsonProperty("url", NullValueHandling = NullValueHandling.Ignore)] - public Uri? Url { get; internal set; } - - internal DiscordEmbedProvider() { } -} +using System; +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Represents an embed provider. +/// +public sealed class DiscordEmbedProvider +{ + /// + /// Gets the name of the provider. + /// + [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] + public string? Name { get; internal set; } + + /// + /// Gets the url of the provider. + /// + [JsonProperty("url", NullValueHandling = NullValueHandling.Ignore)] + public Uri? Url { get; internal set; } + + internal DiscordEmbedProvider() { } +} diff --git a/DSharpPlus/Entities/Channel/Message/Embed/DiscordEmbedThumbnail.cs b/DSharpPlus/Entities/Channel/Message/Embed/DiscordEmbedThumbnail.cs index 923ffcc27e..35ded0162f 100644 --- a/DSharpPlus/Entities/Channel/Message/Embed/DiscordEmbedThumbnail.cs +++ b/DSharpPlus/Entities/Channel/Message/Embed/DiscordEmbedThumbnail.cs @@ -1,36 +1,36 @@ -using DSharpPlus.Net; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents a thumbnail in an embed. -/// -public sealed class DiscordEmbedThumbnail -{ - /// - /// Gets the source url of the thumbnail (only https). - /// - [JsonProperty("url", NullValueHandling = NullValueHandling.Ignore)] - public DiscordUri? Url { get; internal set; } - - /// - /// Gets a proxied url of the thumbnail. - /// - [JsonProperty("proxy_url", NullValueHandling = NullValueHandling.Ignore)] - public DiscordUri? ProxyUrl { get; internal set; } - - /// - /// Gets the height of the thumbnail. - /// - [JsonProperty("height", NullValueHandling = NullValueHandling.Ignore)] - public int Height { get; internal set; } - - /// - /// Gets the width of the thumbnail. - /// - [JsonProperty("width", NullValueHandling = NullValueHandling.Ignore)] - public int Width { get; internal set; } - - internal DiscordEmbedThumbnail() { } -} +using DSharpPlus.Net; +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Represents a thumbnail in an embed. +/// +public sealed class DiscordEmbedThumbnail +{ + /// + /// Gets the source url of the thumbnail (only https). + /// + [JsonProperty("url", NullValueHandling = NullValueHandling.Ignore)] + public DiscordUri? Url { get; internal set; } + + /// + /// Gets a proxied url of the thumbnail. + /// + [JsonProperty("proxy_url", NullValueHandling = NullValueHandling.Ignore)] + public DiscordUri? ProxyUrl { get; internal set; } + + /// + /// Gets the height of the thumbnail. + /// + [JsonProperty("height", NullValueHandling = NullValueHandling.Ignore)] + public int Height { get; internal set; } + + /// + /// Gets the width of the thumbnail. + /// + [JsonProperty("width", NullValueHandling = NullValueHandling.Ignore)] + public int Width { get; internal set; } + + internal DiscordEmbedThumbnail() { } +} diff --git a/DSharpPlus/Entities/Channel/Message/Embed/DiscordEmbedVideo.cs b/DSharpPlus/Entities/Channel/Message/Embed/DiscordEmbedVideo.cs index 8caaf05ef4..d0b56d324d 100644 --- a/DSharpPlus/Entities/Channel/Message/Embed/DiscordEmbedVideo.cs +++ b/DSharpPlus/Entities/Channel/Message/Embed/DiscordEmbedVideo.cs @@ -1,30 +1,30 @@ -using System; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents a video inside an embed. -/// -public sealed class DiscordEmbedVideo -{ - /// - /// Gets the source url of the video. - /// - [JsonProperty("url", NullValueHandling = NullValueHandling.Ignore)] - public Uri? Url { get; internal set; } - - /// - /// Gets the height of the video. - /// - [JsonProperty("height", NullValueHandling = NullValueHandling.Ignore)] - public int Height { get; internal set; } - - /// - /// Gets the width of the video. - /// - [JsonProperty("width", NullValueHandling = NullValueHandling.Ignore)] - public int Width { get; internal set; } - - internal DiscordEmbedVideo() { } -} +using System; +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Represents a video inside an embed. +/// +public sealed class DiscordEmbedVideo +{ + /// + /// Gets the source url of the video. + /// + [JsonProperty("url", NullValueHandling = NullValueHandling.Ignore)] + public Uri? Url { get; internal set; } + + /// + /// Gets the height of the video. + /// + [JsonProperty("height", NullValueHandling = NullValueHandling.Ignore)] + public int Height { get; internal set; } + + /// + /// Gets the width of the video. + /// + [JsonProperty("width", NullValueHandling = NullValueHandling.Ignore)] + public int Width { get; internal set; } + + internal DiscordEmbedVideo() { } +} diff --git a/DSharpPlus/Entities/Channel/Message/MentionType.cs b/DSharpPlus/Entities/Channel/Message/MentionType.cs index 0736c52033..460da112e4 100644 --- a/DSharpPlus/Entities/Channel/Message/MentionType.cs +++ b/DSharpPlus/Entities/Channel/Message/MentionType.cs @@ -1,33 +1,33 @@ -namespace DSharpPlus.Entities; - - -/// -/// Type of mention being made -/// -public enum DiscordMentionType -{ - /// - /// No mention (wtf?) - /// - None = 0, - - /// - /// Mentioned Username - /// - Username = 1, - - /// - /// Mentioned Nickname - /// - Nickname = 2, - - /// - /// Mentioned Channel - /// - Channel = 4, - - /// - /// Mentioned Role - /// - Role = 8 -} +namespace DSharpPlus.Entities; + + +/// +/// Type of mention being made +/// +public enum DiscordMentionType +{ + /// + /// No mention (wtf?) + /// + None = 0, + + /// + /// Mentioned Username + /// + Username = 1, + + /// + /// Mentioned Nickname + /// + Nickname = 2, + + /// + /// Mentioned Channel + /// + Channel = 4, + + /// + /// Mentioned Role + /// + Role = 8 +} diff --git a/DSharpPlus/Entities/Channel/Message/Mentions.cs b/DSharpPlus/Entities/Channel/Message/Mentions.cs index 4464571880..7eecfa5329 100644 --- a/DSharpPlus/Entities/Channel/Message/Mentions.cs +++ b/DSharpPlus/Entities/Channel/Message/Mentions.cs @@ -1,104 +1,104 @@ -using System.Collections.Generic; - -namespace DSharpPlus.Entities; - -/// -/// Interface for mentionables -/// -public interface IMention { } - -/// -/// Allows a reply to ping the user being replied to. -/// -public readonly struct RepliedUserMention : IMention -{ - //This is pointless because new RepliedUserMention() will work, but it is here for consistency with the other mentionables. - /// - /// Mention the user being replied to. Alias to constructor. - /// - public static readonly RepliedUserMention All = new(); -} - -/// -/// Allows @everyone and @here pings to mention in the message. -/// -public readonly struct EveryoneMention : IMention -{ - //This is pointless because new EveryoneMention() will work, but it is here for consistency with the other mentionables. - /// - /// Allow the mentioning of @everyone and @here. Alias to constructor. - /// - public static readonly EveryoneMention All = new(); -} - -/// -/// Allows @user pings to mention in the message. -/// -/// -/// Allows the specific user to be mentioned -/// -/// -public readonly struct UserMention(ulong id) : IMention -{ - /// - /// Allow mentioning of all users. Alias to constructor. - /// - public static readonly UserMention All = new(); - - /// - /// Optional Id of the user that is allowed to be mentioned. If null, then all user mentions will be allowed. - /// - public ulong? Id { get; } = id; - - /// - /// Allows the specific user to be mentioned - /// - /// - public UserMention(DiscordUser user) : this(user.Id) { } - - public static implicit operator UserMention(DiscordUser user) => new(user.Id); -} - -/// -/// Allows @role pings to mention in the message. -/// -/// -/// Allows the specific id to be mentioned -/// -/// -public readonly struct RoleMention(ulong id) : IMention -{ - /// - /// Allow the mentioning of all roles. Alias to constructor. - /// - public static readonly RoleMention All = new(); - - /// - /// Optional Id of the role that is allowed to be mentioned. If null, then all role mentions will be allowed. - /// - public ulong? Id { get; } = id; - - /// - /// Allows the specific role to be mentioned - /// - /// - public RoleMention(DiscordRole role) : this(role.Id) { } - - public static implicit operator RoleMention(DiscordRole role) => new(role.Id); -} - -/// -/// Contains static instances of common mention patterns. -/// -public static class Mentions -{ - /// - /// All possible mentions - @everyone + @here, users, and roles. - /// - public static IEnumerable All { get; } = [EveryoneMention.All, UserMention.All, RoleMention.All]; - - /// - /// No mentions allowed. - /// - public static IEnumerable None { get; } = []; -} +using System.Collections.Generic; + +namespace DSharpPlus.Entities; + +/// +/// Interface for mentionables +/// +public interface IMention { } + +/// +/// Allows a reply to ping the user being replied to. +/// +public readonly struct RepliedUserMention : IMention +{ + //This is pointless because new RepliedUserMention() will work, but it is here for consistency with the other mentionables. + /// + /// Mention the user being replied to. Alias to constructor. + /// + public static readonly RepliedUserMention All = new(); +} + +/// +/// Allows @everyone and @here pings to mention in the message. +/// +public readonly struct EveryoneMention : IMention +{ + //This is pointless because new EveryoneMention() will work, but it is here for consistency with the other mentionables. + /// + /// Allow the mentioning of @everyone and @here. Alias to constructor. + /// + public static readonly EveryoneMention All = new(); +} + +/// +/// Allows @user pings to mention in the message. +/// +/// +/// Allows the specific user to be mentioned +/// +/// +public readonly struct UserMention(ulong id) : IMention +{ + /// + /// Allow mentioning of all users. Alias to constructor. + /// + public static readonly UserMention All = new(); + + /// + /// Optional Id of the user that is allowed to be mentioned. If null, then all user mentions will be allowed. + /// + public ulong? Id { get; } = id; + + /// + /// Allows the specific user to be mentioned + /// + /// + public UserMention(DiscordUser user) : this(user.Id) { } + + public static implicit operator UserMention(DiscordUser user) => new(user.Id); +} + +/// +/// Allows @role pings to mention in the message. +/// +/// +/// Allows the specific id to be mentioned +/// +/// +public readonly struct RoleMention(ulong id) : IMention +{ + /// + /// Allow the mentioning of all roles. Alias to constructor. + /// + public static readonly RoleMention All = new(); + + /// + /// Optional Id of the role that is allowed to be mentioned. If null, then all role mentions will be allowed. + /// + public ulong? Id { get; } = id; + + /// + /// Allows the specific role to be mentioned + /// + /// + public RoleMention(DiscordRole role) : this(role.Id) { } + + public static implicit operator RoleMention(DiscordRole role) => new(role.Id); +} + +/// +/// Contains static instances of common mention patterns. +/// +public static class Mentions +{ + /// + /// All possible mentions - @everyone + @here, users, and roles. + /// + public static IEnumerable All { get; } = [EveryoneMention.All, UserMention.All, RoleMention.All]; + + /// + /// No mentions allowed. + /// + public static IEnumerable None { get; } = []; +} diff --git a/DSharpPlus/Entities/Channel/Message/Poll/DiscordPoll.cs b/DSharpPlus/Entities/Channel/Message/Poll/DiscordPoll.cs index aca8d5e595..ea3c910943 100644 --- a/DSharpPlus/Entities/Channel/Message/Poll/DiscordPoll.cs +++ b/DSharpPlus/Entities/Channel/Message/Poll/DiscordPoll.cs @@ -1,40 +1,40 @@ -using System; -using System.Collections.Generic; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -public sealed class DiscordPoll -{ - /// - /// Gets the question for this poll. Only text is supported. - /// - [JsonProperty("question")] - public DiscordPollMedia Question { get; internal set; } - - /// - /// Gets the answers available in the poll. - /// - [JsonProperty("answers")] - public IReadOnlyList Answers { get; internal set; } - - /// - /// Gets the expiry date for this poll. - /// - [JsonProperty("expiry")] - public DateTimeOffset? Expiry { get; internal set; } - - /// - /// Whether the poll allows for multiple answers. - /// - [JsonProperty("allow_multiselect")] - public bool AllowMultisect { get; internal set; } - - /// - /// Gets the layout type for this poll. Defaults to . - /// - [JsonProperty("layout_type", NullValueHandling = NullValueHandling.Ignore)] - public DiscordPollLayoutType Layout { get; internal set; } - - internal DiscordPoll() { } -} +using System; +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +public sealed class DiscordPoll +{ + /// + /// Gets the question for this poll. Only text is supported. + /// + [JsonProperty("question")] + public DiscordPollMedia Question { get; internal set; } + + /// + /// Gets the answers available in the poll. + /// + [JsonProperty("answers")] + public IReadOnlyList Answers { get; internal set; } + + /// + /// Gets the expiry date for this poll. + /// + [JsonProperty("expiry")] + public DateTimeOffset? Expiry { get; internal set; } + + /// + /// Whether the poll allows for multiple answers. + /// + [JsonProperty("allow_multiselect")] + public bool AllowMultisect { get; internal set; } + + /// + /// Gets the layout type for this poll. Defaults to . + /// + [JsonProperty("layout_type", NullValueHandling = NullValueHandling.Ignore)] + public DiscordPollLayoutType Layout { get; internal set; } + + internal DiscordPoll() { } +} diff --git a/DSharpPlus/Entities/Channel/Message/Poll/DiscordPollAnswer.cs b/DSharpPlus/Entities/Channel/Message/Poll/DiscordPollAnswer.cs index 862ce12d8c..bb2ceafa03 100644 --- a/DSharpPlus/Entities/Channel/Message/Poll/DiscordPollAnswer.cs +++ b/DSharpPlus/Entities/Channel/Message/Poll/DiscordPollAnswer.cs @@ -1,21 +1,21 @@ -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents an answer to a poll. -/// -public class DiscordPollAnswer -{ - /// - /// Gets the ID of the answer. - /// - [JsonProperty("answer_id")] - public int AnswerId { get; internal set; } - - /// - /// Gets the data for the answer. - /// - [JsonProperty("poll_media")] - public DiscordPollMedia AnswerData { get; internal set; } -} +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Represents an answer to a poll. +/// +public class DiscordPollAnswer +{ + /// + /// Gets the ID of the answer. + /// + [JsonProperty("answer_id")] + public int AnswerId { get; internal set; } + + /// + /// Gets the data for the answer. + /// + [JsonProperty("poll_media")] + public DiscordPollMedia AnswerData { get; internal set; } +} diff --git a/DSharpPlus/Entities/Channel/Message/Poll/DiscordPollAnswerCount.cs b/DSharpPlus/Entities/Channel/Message/Poll/DiscordPollAnswerCount.cs index 361ea039a1..aa6e54c16f 100644 --- a/DSharpPlus/Entities/Channel/Message/Poll/DiscordPollAnswerCount.cs +++ b/DSharpPlus/Entities/Channel/Message/Poll/DiscordPollAnswerCount.cs @@ -1,35 +1,35 @@ -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Gets a count of a poll answer. -/// -public sealed class DiscordPollAnswerCount -{ - /// - /// Gets the ID of this answer. - /// - // Hello source code reader! I have no idea why Discord chose to do this in lieu - // of using a dictionary. A dictionary would allow you to more easily map PollAnswer -> PollResult - // but instead, you must loop over, check the ID, then check the ID of the current poll *answer* - // to then build your dictionary. - Velvet - [JsonProperty("answer_id")] - public int AnswerId { get; internal set; } - - /// - /// Gets a (potentially approximate) count of how many users voted for this answer. - /// - /// - /// This count isn't guaranteed to be precise unless is true. - /// - public int Count { get; internal set; } - - /// - /// Gets whether the current user voted for this answer. - /// - [JsonProperty("me_voted")] - public bool SelfVoted { get; internal set; } - - internal DiscordPollAnswerCount() { } -} +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Gets a count of a poll answer. +/// +public sealed class DiscordPollAnswerCount +{ + /// + /// Gets the ID of this answer. + /// + // Hello source code reader! I have no idea why Discord chose to do this in lieu + // of using a dictionary. A dictionary would allow you to more easily map PollAnswer -> PollResult + // but instead, you must loop over, check the ID, then check the ID of the current poll *answer* + // to then build your dictionary. - Velvet + [JsonProperty("answer_id")] + public int AnswerId { get; internal set; } + + /// + /// Gets a (potentially approximate) count of how many users voted for this answer. + /// + /// + /// This count isn't guaranteed to be precise unless is true. + /// + public int Count { get; internal set; } + + /// + /// Gets whether the current user voted for this answer. + /// + [JsonProperty("me_voted")] + public bool SelfVoted { get; internal set; } + + internal DiscordPollAnswerCount() { } +} diff --git a/DSharpPlus/Entities/Channel/Message/Poll/DiscordPollBuilder.cs b/DSharpPlus/Entities/Channel/Message/Poll/DiscordPollBuilder.cs index 5c14a45a98..857fe165e5 100644 --- a/DSharpPlus/Entities/Channel/Message/Poll/DiscordPollBuilder.cs +++ b/DSharpPlus/Entities/Channel/Message/Poll/DiscordPollBuilder.cs @@ -1,115 +1,115 @@ -using System; -using System.Collections.Generic; -using DSharpPlus.Net.Abstractions; - -namespace DSharpPlus.Entities; - -/// -/// Represents a builder for s. -/// -public class DiscordPollBuilder -{ - /// - /// Gets or sets the question for this poll. - /// - public string Question { get; set; } - - /// - /// Gets or sets whether this poll is multiple choice. - /// - public bool IsMultipleChoice { get; set; } - - /// - /// Gets the options for this poll. - /// - public IReadOnlyList Options => this.options; - private readonly List options = []; - - /// - /// Gets or sets the duration for this poll in hours. - /// - public int Duration { get; set; } = 1; - - /// - /// Sets the question for this poll. - /// - /// The question for the poll. - /// The modified builder to chain calls with. - public DiscordPollBuilder WithQuestion(string question) - { - this.Question = question; - return this; - } - - /// - /// Adds an option to this poll. - /// - /// The text for the option. Null may be passed if is passed instead. - /// An optional emoji for the poll. - /// The modified builder to chain calls with. - public DiscordPollBuilder AddOption(string text, DiscordComponentEmoji? emoji = null) - { - if (emoji is null) - { - ArgumentNullException.ThrowIfNullOrWhiteSpace(text); - } - - this.options.Add(new DiscordPollMedia { Text = text, Emoji = emoji }); - return this; - } - - /// - /// Sets whether this poll is multiple choice. - /// - /// Whether the builder is multiple-choice. Defaults to true - /// The modified builder to chain calls with. - public DiscordPollBuilder AsMultipleChoice(bool isMultiChoice = true) - { - this.IsMultipleChoice = isMultiChoice; - return this; - } - - /// - /// Sets the expiry date for this poll. - /// - /// How many hours the poll should last. - /// The modified builder to chain calls with. - /// Thrown if is in the past or more than 7 days in the future. - public DiscordPollBuilder WithDuration(int hours) - { - if (hours < 1) - { - throw new InvalidOperationException("Duration must be at least 1 hour."); - } - - if (hours > 24 * 7) - { - throw new InvalidOperationException("Duration must be less then 7 days/168 hours."); - } - - this.Duration = hours; - return this; - } - - /// - /// Builds the poll. - /// - /// A to build the create request. - /// Thrown if the poll has less than two options. - internal PollCreatePayload BuildInternal() => this.options.Count < 2 - ? throw new InvalidOperationException("A poll must have at least two options.") - : new PollCreatePayload(this); - - public DiscordPollBuilder() { } - - public DiscordPollBuilder(DiscordPoll poll) - { - WithQuestion(poll.Question.Text); - AsMultipleChoice(poll.AllowMultisect); - - foreach (DiscordPollAnswer option in poll.Answers) - { - AddOption(option.AnswerData.Text, option.AnswerData.Emoji); - } - } -} +using System; +using System.Collections.Generic; +using DSharpPlus.Net.Abstractions; + +namespace DSharpPlus.Entities; + +/// +/// Represents a builder for s. +/// +public class DiscordPollBuilder +{ + /// + /// Gets or sets the question for this poll. + /// + public string Question { get; set; } + + /// + /// Gets or sets whether this poll is multiple choice. + /// + public bool IsMultipleChoice { get; set; } + + /// + /// Gets the options for this poll. + /// + public IReadOnlyList Options => this.options; + private readonly List options = []; + + /// + /// Gets or sets the duration for this poll in hours. + /// + public int Duration { get; set; } = 1; + + /// + /// Sets the question for this poll. + /// + /// The question for the poll. + /// The modified builder to chain calls with. + public DiscordPollBuilder WithQuestion(string question) + { + this.Question = question; + return this; + } + + /// + /// Adds an option to this poll. + /// + /// The text for the option. Null may be passed if is passed instead. + /// An optional emoji for the poll. + /// The modified builder to chain calls with. + public DiscordPollBuilder AddOption(string text, DiscordComponentEmoji? emoji = null) + { + if (emoji is null) + { + ArgumentNullException.ThrowIfNullOrWhiteSpace(text); + } + + this.options.Add(new DiscordPollMedia { Text = text, Emoji = emoji }); + return this; + } + + /// + /// Sets whether this poll is multiple choice. + /// + /// Whether the builder is multiple-choice. Defaults to true + /// The modified builder to chain calls with. + public DiscordPollBuilder AsMultipleChoice(bool isMultiChoice = true) + { + this.IsMultipleChoice = isMultiChoice; + return this; + } + + /// + /// Sets the expiry date for this poll. + /// + /// How many hours the poll should last. + /// The modified builder to chain calls with. + /// Thrown if is in the past or more than 7 days in the future. + public DiscordPollBuilder WithDuration(int hours) + { + if (hours < 1) + { + throw new InvalidOperationException("Duration must be at least 1 hour."); + } + + if (hours > 24 * 7) + { + throw new InvalidOperationException("Duration must be less then 7 days/168 hours."); + } + + this.Duration = hours; + return this; + } + + /// + /// Builds the poll. + /// + /// A to build the create request. + /// Thrown if the poll has less than two options. + internal PollCreatePayload BuildInternal() => this.options.Count < 2 + ? throw new InvalidOperationException("A poll must have at least two options.") + : new PollCreatePayload(this); + + public DiscordPollBuilder() { } + + public DiscordPollBuilder(DiscordPoll poll) + { + WithQuestion(poll.Question.Text); + AsMultipleChoice(poll.AllowMultisect); + + foreach (DiscordPollAnswer option in poll.Answers) + { + AddOption(option.AnswerData.Text, option.AnswerData.Emoji); + } + } +} diff --git a/DSharpPlus/Entities/Channel/Message/Poll/DiscordPollLayoutType.cs b/DSharpPlus/Entities/Channel/Message/Poll/DiscordPollLayoutType.cs index b2f3239ae5..9a43a464e1 100644 --- a/DSharpPlus/Entities/Channel/Message/Poll/DiscordPollLayoutType.cs +++ b/DSharpPlus/Entities/Channel/Message/Poll/DiscordPollLayoutType.cs @@ -1,13 +1,13 @@ -namespace DSharpPlus.Entities; - - -/// -/// Represents the layout type of . -/// -public enum DiscordPollLayoutType -{ - /// - /// "The, uhm, default layout type." - Discord. - /// - Default = 1, -} +namespace DSharpPlus.Entities; + + +/// +/// Represents the layout type of . +/// +public enum DiscordPollLayoutType +{ + /// + /// "The, uhm, default layout type." - Discord. + /// + Default = 1, +} diff --git a/DSharpPlus/Entities/Channel/Message/Poll/DiscordPollMedia.cs b/DSharpPlus/Entities/Channel/Message/Poll/DiscordPollMedia.cs index 25409908ca..f700d0a4b3 100644 --- a/DSharpPlus/Entities/Channel/Message/Poll/DiscordPollMedia.cs +++ b/DSharpPlus/Entities/Channel/Message/Poll/DiscordPollMedia.cs @@ -1,28 +1,28 @@ -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents media for a poll. It is the backplane for poll options. -/// -public sealed class DiscordPollMedia -{ - /// - /// Gets the text for the field. - /// - /// - /// For questions, the maximum length of this is 300 characters.
- /// For answers, the maximum is 55. This is subject to change from Discord, however.

- /// Despite nullability, this field should always be non-null. This is also subject to change from Discord. - ///
- [JsonProperty("text", NullValueHandling = NullValueHandling.Ignore)] - public string? Text { get; internal set; } - - /// - /// Gets the emoji for the field, if any. - /// - [JsonProperty("emoji", NullValueHandling = NullValueHandling.Ignore)] - public DiscordComponentEmoji? Emoji { get; internal set; } - - internal DiscordPollMedia() { } -} +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Represents media for a poll. It is the backplane for poll options. +/// +public sealed class DiscordPollMedia +{ + /// + /// Gets the text for the field. + /// + /// + /// For questions, the maximum length of this is 300 characters.
+ /// For answers, the maximum is 55. This is subject to change from Discord, however.

+ /// Despite nullability, this field should always be non-null. This is also subject to change from Discord. + ///
+ [JsonProperty("text", NullValueHandling = NullValueHandling.Ignore)] + public string? Text { get; internal set; } + + /// + /// Gets the emoji for the field, if any. + /// + [JsonProperty("emoji", NullValueHandling = NullValueHandling.Ignore)] + public DiscordComponentEmoji? Emoji { get; internal set; } + + internal DiscordPollMedia() { } +} diff --git a/DSharpPlus/Entities/Channel/Message/Poll/DiscordPollResult.cs b/DSharpPlus/Entities/Channel/Message/Poll/DiscordPollResult.cs index 412b3d1c38..51fa85fd68 100644 --- a/DSharpPlus/Entities/Channel/Message/Poll/DiscordPollResult.cs +++ b/DSharpPlus/Entities/Channel/Message/Poll/DiscordPollResult.cs @@ -1,19 +1,19 @@ -using System.Collections.Generic; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -public sealed class DiscordPollResult -{ - /// - /// Gets whether the poll answers have been precisely tallied. - /// - [JsonProperty("is_finalized")] - public bool IsFinalized { get; internal set; } - - /// - /// Gets the results of the poll. - /// - [JsonProperty("answer_counts")] - public IReadOnlyList Results { get; internal set; } -} +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +public sealed class DiscordPollResult +{ + /// + /// Gets whether the poll answers have been precisely tallied. + /// + [JsonProperty("is_finalized")] + public bool IsFinalized { get; internal set; } + + /// + /// Gets the results of the poll. + /// + [JsonProperty("answer_counts")] + public IReadOnlyList Results { get; internal set; } +} diff --git a/DSharpPlus/Entities/Channel/Message/Poll/DiscordPollVoteUpdate.cs b/DSharpPlus/Entities/Channel/Message/Poll/DiscordPollVoteUpdate.cs index a89faff476..7e776896b0 100644 --- a/DSharpPlus/Entities/Channel/Message/Poll/DiscordPollVoteUpdate.cs +++ b/DSharpPlus/Entities/Channel/Message/Poll/DiscordPollVoteUpdate.cs @@ -1,64 +1,64 @@ -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents an update for a poll vote. -/// -public class DiscordPollVoteUpdate -{ - /// - /// Gets or sets a client for this vote update. - /// - internal DiscordClient client; - - /// - /// Gets whether this vote was added or removed. true if it was added, false if it was removed. - /// - [JsonIgnore] - public bool WasAdded { get; internal set; } - - /// - /// Gets the user that added or removed a vote. - /// - public DiscordUser User => this.client.GetCachedOrEmptyUserInternal(this.UserId); - - [JsonIgnore] - public DiscordChannel Channel => this.client.InternalGetCachedChannel(this.ChannelId, this.GuildId); - - /// - /// Gets the message that the poll is attached to. - /// - /// - /// This property attempts to pull the associated message from cache, which relies on a cache provider - /// being enabled in the client. If no cache provider is enabled, this property will always return . - /// - // Should this pull from cache as an auto-property? Perhaps having a hard-set message pulled from cache further up - // instead. - [JsonIgnore] - public DiscordMessage? Message - => this.client.MessageCache?.TryGet(this.MessageId, out DiscordMessage? msg) ?? false ? msg : null; - - /// - /// Gets the guild this poll was sent in, if applicable. - /// - public DiscordGuild? Guild - => this.GuildId.HasValue ? this.client.InternalGetCachedGuild(this.GuildId.Value) : null; - - [JsonProperty("user_id")] - internal ulong UserId { get; set; } - - [JsonProperty("channel_id")] - internal ulong ChannelId { get; set; } - - [JsonProperty("message_id")] - internal ulong MessageId { get; set; } - - [JsonProperty("guild_id", NullValueHandling = NullValueHandling.Ignore)] - internal ulong? GuildId { get; set; } - - [JsonProperty("answer_id")] - internal int AnswerId { get; set; } - - internal DiscordPollVoteUpdate() { } -} +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Represents an update for a poll vote. +/// +public class DiscordPollVoteUpdate +{ + /// + /// Gets or sets a client for this vote update. + /// + internal DiscordClient client; + + /// + /// Gets whether this vote was added or removed. true if it was added, false if it was removed. + /// + [JsonIgnore] + public bool WasAdded { get; internal set; } + + /// + /// Gets the user that added or removed a vote. + /// + public DiscordUser User => this.client.GetCachedOrEmptyUserInternal(this.UserId); + + [JsonIgnore] + public DiscordChannel Channel => this.client.InternalGetCachedChannel(this.ChannelId, this.GuildId); + + /// + /// Gets the message that the poll is attached to. + /// + /// + /// This property attempts to pull the associated message from cache, which relies on a cache provider + /// being enabled in the client. If no cache provider is enabled, this property will always return . + /// + // Should this pull from cache as an auto-property? Perhaps having a hard-set message pulled from cache further up + // instead. + [JsonIgnore] + public DiscordMessage? Message + => this.client.MessageCache?.TryGet(this.MessageId, out DiscordMessage? msg) ?? false ? msg : null; + + /// + /// Gets the guild this poll was sent in, if applicable. + /// + public DiscordGuild? Guild + => this.GuildId.HasValue ? this.client.InternalGetCachedGuild(this.GuildId.Value) : null; + + [JsonProperty("user_id")] + internal ulong UserId { get; set; } + + [JsonProperty("channel_id")] + internal ulong ChannelId { get; set; } + + [JsonProperty("message_id")] + internal ulong MessageId { get; set; } + + [JsonProperty("guild_id", NullValueHandling = NullValueHandling.Ignore)] + internal ulong? GuildId { get; set; } + + [JsonProperty("answer_id")] + internal int AnswerId { get; set; } + + internal DiscordPollVoteUpdate() { } +} diff --git a/DSharpPlus/Entities/Channel/Overwrite/DiscordOverwrite.cs b/DSharpPlus/Entities/Channel/Overwrite/DiscordOverwrite.cs index a1666229db..acfbe00e07 100644 --- a/DSharpPlus/Entities/Channel/Overwrite/DiscordOverwrite.cs +++ b/DSharpPlus/Entities/Channel/Overwrite/DiscordOverwrite.cs @@ -1,90 +1,90 @@ -using System; -using System.Threading.Tasks; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents a permission overwrite for a channel. -/// -public class DiscordOverwrite : SnowflakeObject -{ - /// - /// Gets the type of the overwrite. Either "role" or "member". - /// - [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] - public DiscordOverwriteType Type { get; internal set; } - - /// - /// Gets the allowed permission set. - /// - [JsonProperty("allow", NullValueHandling = NullValueHandling.Ignore)] - public DiscordPermissions Allowed { get; internal set; } - - /// - /// Gets the denied permission set. - /// - [JsonProperty("deny", NullValueHandling = NullValueHandling.Ignore)] - public DiscordPermissions Denied { get; internal set; } - - [JsonIgnore] - internal ulong channelId; - - /// - /// Deletes this channel overwrite. - /// - /// Reason as to why this overwrite gets deleted. - /// - public async Task DeleteAsync(string? reason = null) => await this.Discord.ApiClient.DeleteChannelPermissionAsync(this.channelId, this.Id, reason); - - /// - /// Updates this channel overwrite. - /// - /// Permissions that are allowed. - /// Permissions that are denied. - /// Reason as to why you made this change. - /// - /// Thrown when the client does not have the permission. - /// Thrown when the overwrite does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public async Task UpdateAsync(DiscordPermissions? allow = null, DiscordPermissions? deny = null, string? reason = null) - => await this.Discord.ApiClient.EditChannelPermissionsAsync(this.channelId, this.Id, allow ?? this.Allowed, deny ?? this.Denied, this.Type.ToString().ToLowerInvariant(), reason); - - /// - /// Gets the DiscordMember that is affected by this overwrite. - /// - /// The DiscordMember that is affected by this overwrite - /// Thrown when the client does not have the permission. - /// Thrown when the overwrite does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public async Task GetMemberAsync() => this.Type != DiscordOverwriteType.Member - ? throw new ArgumentException(nameof(this.Type), "This overwrite is for a role, not a member.") - : await (await this.Discord.ApiClient.GetChannelAsync(this.channelId)).Guild.GetMemberAsync(this.Id); - - /// - /// Gets the DiscordRole that is affected by this overwrite. - /// - /// The DiscordRole that is affected by this overwrite - /// Thrown when the role does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public async Task GetRoleAsync() => this.Type != DiscordOverwriteType.Role - ? throw new ArgumentException(nameof(this.Type), "This overwrite is for a member, not a role.") - : await (await this.Discord.ApiClient.GetChannelAsync(this.channelId)).Guild.GetRoleAsync(this.Id); - - internal DiscordOverwrite() { } - - /// - /// Checks whether given permissions are allowed, denied, or not set. - /// - /// Permissions to check. - /// Whether given permissions are allowed, denied, or not set. - public DiscordPermissionLevel CheckPermission(DiscordPermissions permissions) - { - return this.Allowed.HasAllPermissions(permissions) - ? DiscordPermissionLevel.Allowed - : this.Denied.HasAllPermissions(permissions) ? DiscordPermissionLevel.Denied : DiscordPermissionLevel.Unset; - } -} +using System; +using System.Threading.Tasks; +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Represents a permission overwrite for a channel. +/// +public class DiscordOverwrite : SnowflakeObject +{ + /// + /// Gets the type of the overwrite. Either "role" or "member". + /// + [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] + public DiscordOverwriteType Type { get; internal set; } + + /// + /// Gets the allowed permission set. + /// + [JsonProperty("allow", NullValueHandling = NullValueHandling.Ignore)] + public DiscordPermissions Allowed { get; internal set; } + + /// + /// Gets the denied permission set. + /// + [JsonProperty("deny", NullValueHandling = NullValueHandling.Ignore)] + public DiscordPermissions Denied { get; internal set; } + + [JsonIgnore] + internal ulong channelId; + + /// + /// Deletes this channel overwrite. + /// + /// Reason as to why this overwrite gets deleted. + /// + public async Task DeleteAsync(string? reason = null) => await this.Discord.ApiClient.DeleteChannelPermissionAsync(this.channelId, this.Id, reason); + + /// + /// Updates this channel overwrite. + /// + /// Permissions that are allowed. + /// Permissions that are denied. + /// Reason as to why you made this change. + /// + /// Thrown when the client does not have the permission. + /// Thrown when the overwrite does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task UpdateAsync(DiscordPermissions? allow = null, DiscordPermissions? deny = null, string? reason = null) + => await this.Discord.ApiClient.EditChannelPermissionsAsync(this.channelId, this.Id, allow ?? this.Allowed, deny ?? this.Denied, this.Type.ToString().ToLowerInvariant(), reason); + + /// + /// Gets the DiscordMember that is affected by this overwrite. + /// + /// The DiscordMember that is affected by this overwrite + /// Thrown when the client does not have the permission. + /// Thrown when the overwrite does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task GetMemberAsync() => this.Type != DiscordOverwriteType.Member + ? throw new ArgumentException(nameof(this.Type), "This overwrite is for a role, not a member.") + : await (await this.Discord.ApiClient.GetChannelAsync(this.channelId)).Guild.GetMemberAsync(this.Id); + + /// + /// Gets the DiscordRole that is affected by this overwrite. + /// + /// The DiscordRole that is affected by this overwrite + /// Thrown when the role does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task GetRoleAsync() => this.Type != DiscordOverwriteType.Role + ? throw new ArgumentException(nameof(this.Type), "This overwrite is for a member, not a role.") + : await (await this.Discord.ApiClient.GetChannelAsync(this.channelId)).Guild.GetRoleAsync(this.Id); + + internal DiscordOverwrite() { } + + /// + /// Checks whether given permissions are allowed, denied, or not set. + /// + /// Permissions to check. + /// Whether given permissions are allowed, denied, or not set. + public DiscordPermissionLevel CheckPermission(DiscordPermissions permissions) + { + return this.Allowed.HasAllPermissions(permissions) + ? DiscordPermissionLevel.Allowed + : this.Denied.HasAllPermissions(permissions) ? DiscordPermissionLevel.Denied : DiscordPermissionLevel.Unset; + } +} diff --git a/DSharpPlus/Entities/Channel/Overwrite/DiscordOverwriteBuilder.cs b/DSharpPlus/Entities/Channel/Overwrite/DiscordOverwriteBuilder.cs index 637e6a650c..69d75b4fb1 100644 --- a/DSharpPlus/Entities/Channel/Overwrite/DiscordOverwriteBuilder.cs +++ b/DSharpPlus/Entities/Channel/Overwrite/DiscordOverwriteBuilder.cs @@ -1,131 +1,131 @@ -using System; -using System.Threading.Tasks; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents a Discord permission overwrite builder. -/// -public sealed record DiscordOverwriteBuilder -{ - /// - /// Gets or sets the allowed permissions for this overwrite. - /// - public DiscordPermissions Allowed { get; set; } - - /// - /// Gets or sets the denied permissions for this overwrite. - /// - public DiscordPermissions Denied { get; set; } - - /// - /// The id of the target for this overwrite. - /// - public ulong TargetId { get; set; } - - /// - /// Gets the type of this overwrite's target. - /// - public DiscordOverwriteType Type { get; set; } - - /// - /// Creates a new Discord permission overwrite builder. This class can be used to construct permission overwrites for guild channels, used when creating channels. - /// - public DiscordOverwriteBuilder() { } - - /// - /// Creates a new Discord permission overwrite builder for a member. This class can be used to construct permission overwrites for guild channels, used when creating channels. - /// - public DiscordOverwriteBuilder(DiscordMember member) - { - this.TargetId = member.Id; - this.Type = DiscordOverwriteType.Member; - } - - /// - /// Creates a new Discord permission overwrite builder for a role. This class can be used to construct permission overwrites for guild channels, used when creating channels. - /// - public DiscordOverwriteBuilder(DiscordRole role) - { - this.TargetId = role.Id; - this.Type = DiscordOverwriteType.Role; - } - - /// - /// Allows a permission for this overwrite. - /// - /// Permission or permission set to allow for this overwrite. - /// This builder. - public DiscordOverwriteBuilder Allow(DiscordPermissions permission) - { - this.Allowed |= permission; - return this; - } - - /// - /// Denies a permission for this overwrite. - /// - /// Permission or permission set to deny for this overwrite. - /// This builder. - public DiscordOverwriteBuilder Deny(DiscordPermissions permission) - { - this.Denied |= permission; - return this; - } - - /// - /// Attempts to get the entity representing the target of this overwrite. - /// - /// The server to which the target belongs. - /// Entity representing the target of this overwrite, or null if the target id is not set. - public async ValueTask GetTargetAsync(DiscordGuild guild) => this.Type switch - { - DiscordOverwriteType.Member => await guild.GetMemberAsync(this.TargetId), - DiscordOverwriteType.Role => await guild.GetRoleAsync(this.TargetId), - _ => null - }; - - /// - /// Populates this builder with data from another overwrite object. - /// - /// Overwrite from which data will be used. - /// This builder. - public static DiscordOverwriteBuilder From(DiscordOverwrite other) => new() - { - Allowed = other.Allowed, - Denied = other.Denied, - TargetId = other.Id, - Type = other.Type - }; - - /// - /// Builds this DiscordOverwrite. - /// - /// Use this object for creation of new overwrites. - internal DiscordRestOverwrite Build() - { - return this.TargetId is 0 ? throw new InvalidOperationException("The target id must be set.") : new() - { - Allow = this.Allowed, - Deny = this.Denied, - Id = this.TargetId, - Type = this.Type, - }; - } -} - -internal struct DiscordRestOverwrite -{ - [JsonProperty("allow", NullValueHandling = NullValueHandling.Ignore)] - internal DiscordPermissions Allow { get; set; } - - [JsonProperty("deny", NullValueHandling = NullValueHandling.Ignore)] - internal DiscordPermissions Deny { get; set; } - - [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)] - internal ulong Id { get; set; } - - [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] - internal DiscordOverwriteType Type { get; set; } -} +using System; +using System.Threading.Tasks; +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Represents a Discord permission overwrite builder. +/// +public sealed record DiscordOverwriteBuilder +{ + /// + /// Gets or sets the allowed permissions for this overwrite. + /// + public DiscordPermissions Allowed { get; set; } + + /// + /// Gets or sets the denied permissions for this overwrite. + /// + public DiscordPermissions Denied { get; set; } + + /// + /// The id of the target for this overwrite. + /// + public ulong TargetId { get; set; } + + /// + /// Gets the type of this overwrite's target. + /// + public DiscordOverwriteType Type { get; set; } + + /// + /// Creates a new Discord permission overwrite builder. This class can be used to construct permission overwrites for guild channels, used when creating channels. + /// + public DiscordOverwriteBuilder() { } + + /// + /// Creates a new Discord permission overwrite builder for a member. This class can be used to construct permission overwrites for guild channels, used when creating channels. + /// + public DiscordOverwriteBuilder(DiscordMember member) + { + this.TargetId = member.Id; + this.Type = DiscordOverwriteType.Member; + } + + /// + /// Creates a new Discord permission overwrite builder for a role. This class can be used to construct permission overwrites for guild channels, used when creating channels. + /// + public DiscordOverwriteBuilder(DiscordRole role) + { + this.TargetId = role.Id; + this.Type = DiscordOverwriteType.Role; + } + + /// + /// Allows a permission for this overwrite. + /// + /// Permission or permission set to allow for this overwrite. + /// This builder. + public DiscordOverwriteBuilder Allow(DiscordPermissions permission) + { + this.Allowed |= permission; + return this; + } + + /// + /// Denies a permission for this overwrite. + /// + /// Permission or permission set to deny for this overwrite. + /// This builder. + public DiscordOverwriteBuilder Deny(DiscordPermissions permission) + { + this.Denied |= permission; + return this; + } + + /// + /// Attempts to get the entity representing the target of this overwrite. + /// + /// The server to which the target belongs. + /// Entity representing the target of this overwrite, or null if the target id is not set. + public async ValueTask GetTargetAsync(DiscordGuild guild) => this.Type switch + { + DiscordOverwriteType.Member => await guild.GetMemberAsync(this.TargetId), + DiscordOverwriteType.Role => await guild.GetRoleAsync(this.TargetId), + _ => null + }; + + /// + /// Populates this builder with data from another overwrite object. + /// + /// Overwrite from which data will be used. + /// This builder. + public static DiscordOverwriteBuilder From(DiscordOverwrite other) => new() + { + Allowed = other.Allowed, + Denied = other.Denied, + TargetId = other.Id, + Type = other.Type + }; + + /// + /// Builds this DiscordOverwrite. + /// + /// Use this object for creation of new overwrites. + internal DiscordRestOverwrite Build() + { + return this.TargetId is 0 ? throw new InvalidOperationException("The target id must be set.") : new() + { + Allow = this.Allowed, + Deny = this.Denied, + Id = this.TargetId, + Type = this.Type, + }; + } +} + +internal struct DiscordRestOverwrite +{ + [JsonProperty("allow", NullValueHandling = NullValueHandling.Ignore)] + internal DiscordPermissions Allow { get; set; } + + [JsonProperty("deny", NullValueHandling = NullValueHandling.Ignore)] + internal DiscordPermissions Deny { get; set; } + + [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)] + internal ulong Id { get; set; } + + [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] + internal DiscordOverwriteType Type { get; set; } +} diff --git a/DSharpPlus/Entities/Channel/Stage/DiscordStageInstance.cs b/DSharpPlus/Entities/Channel/Stage/DiscordStageInstance.cs index f1697e0242..39305a23ec 100644 --- a/DSharpPlus/Entities/Channel/Stage/DiscordStageInstance.cs +++ b/DSharpPlus/Entities/Channel/Stage/DiscordStageInstance.cs @@ -1,79 +1,79 @@ -using System; -using System.Threading.Tasks; -using DSharpPlus.Exceptions; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents a discord stage instance. -/// -public sealed class DiscordStageInstance : SnowflakeObject -{ - /// - /// Gets the guild this stage instance is in. - /// - [JsonIgnore] - public DiscordGuild Guild - => this.Discord.Guilds.TryGetValue(this.GuildId, out DiscordGuild? guild) ? guild : null; - - /// - /// Gets the id of the guild this stage instance is in. - /// - [JsonProperty("guild_id")] - public ulong GuildId { get; internal set; } - - /// - /// Gets the channel this stage instance is in. - /// - [JsonIgnore] - public DiscordChannel Channel - => (this.Discord as DiscordClient)?.InternalGetCachedChannel(this.ChannelId, this.GuildId) ?? null; - - /// - /// Gets the id of the channel this stage instance is in. - /// - [JsonProperty("channel_id")] - public ulong ChannelId { get; internal set; } - - /// - /// Gets the topic of this stage instance. - /// - [JsonProperty("topic")] - public string Topic { get; internal set; } - - /// - /// Gets the privacy level of this stage instance. - /// - [JsonProperty("privacy_level")] - public DiscordStagePrivacyLevel PrivacyLevel { get; internal set; } - - /// - /// Gets whether or not stage discovery is disabled. - /// - [JsonProperty("discoverable_disabled")] - public bool DiscoverableDisabled { get; internal set; } - - /// - /// Become speaker of current stage. - /// - /// - /// Thrown when the client does not have the permission - public async Task BecomeSpeakerAsync() - => await this.Discord.ApiClient.BecomeStageInstanceSpeakerAsync(this.GuildId, this.Id, null); - - /// - /// Request to become a speaker in the stage instance. - /// - /// - /// Thrown when the client does not have the permission - public async Task SendSpeakerRequestAsync() => await this.Discord.ApiClient.BecomeStageInstanceSpeakerAsync(this.GuildId, this.Id, null, DateTime.Now); - - /// - /// Invite a member to become a speaker in the state instance. - /// - /// The member to invite to speak on stage. - /// - /// Thrown when the client does not have the permission - public async Task InviteToSpeakAsync(DiscordMember member) => await this.Discord.ApiClient.BecomeStageInstanceSpeakerAsync(this.GuildId, this.Id, member.Id, null, suppress: false); -} +using System; +using System.Threading.Tasks; +using DSharpPlus.Exceptions; +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Represents a discord stage instance. +/// +public sealed class DiscordStageInstance : SnowflakeObject +{ + /// + /// Gets the guild this stage instance is in. + /// + [JsonIgnore] + public DiscordGuild Guild + => this.Discord.Guilds.TryGetValue(this.GuildId, out DiscordGuild? guild) ? guild : null; + + /// + /// Gets the id of the guild this stage instance is in. + /// + [JsonProperty("guild_id")] + public ulong GuildId { get; internal set; } + + /// + /// Gets the channel this stage instance is in. + /// + [JsonIgnore] + public DiscordChannel Channel + => (this.Discord as DiscordClient)?.InternalGetCachedChannel(this.ChannelId, this.GuildId) ?? null; + + /// + /// Gets the id of the channel this stage instance is in. + /// + [JsonProperty("channel_id")] + public ulong ChannelId { get; internal set; } + + /// + /// Gets the topic of this stage instance. + /// + [JsonProperty("topic")] + public string Topic { get; internal set; } + + /// + /// Gets the privacy level of this stage instance. + /// + [JsonProperty("privacy_level")] + public DiscordStagePrivacyLevel PrivacyLevel { get; internal set; } + + /// + /// Gets whether or not stage discovery is disabled. + /// + [JsonProperty("discoverable_disabled")] + public bool DiscoverableDisabled { get; internal set; } + + /// + /// Become speaker of current stage. + /// + /// + /// Thrown when the client does not have the permission + public async Task BecomeSpeakerAsync() + => await this.Discord.ApiClient.BecomeStageInstanceSpeakerAsync(this.GuildId, this.Id, null); + + /// + /// Request to become a speaker in the stage instance. + /// + /// + /// Thrown when the client does not have the permission + public async Task SendSpeakerRequestAsync() => await this.Discord.ApiClient.BecomeStageInstanceSpeakerAsync(this.GuildId, this.Id, null, DateTime.Now); + + /// + /// Invite a member to become a speaker in the state instance. + /// + /// The member to invite to speak on stage. + /// + /// Thrown when the client does not have the permission + public async Task InviteToSpeakAsync(DiscordMember member) => await this.Discord.ApiClient.BecomeStageInstanceSpeakerAsync(this.GuildId, this.Id, member.Id, null, suppress: false); +} diff --git a/DSharpPlus/Entities/Channel/Stage/DiscordStagePrivacyLevel.cs b/DSharpPlus/Entities/Channel/Stage/DiscordStagePrivacyLevel.cs index 1b3e0cd3a0..bffe9d140c 100644 --- a/DSharpPlus/Entities/Channel/Stage/DiscordStagePrivacyLevel.cs +++ b/DSharpPlus/Entities/Channel/Stage/DiscordStagePrivacyLevel.cs @@ -1,18 +1,18 @@ -namespace DSharpPlus.Entities; - - -/// -/// Represents a stage instance's privacy level. -/// -public enum DiscordStagePrivacyLevel -{ - /// - /// Indicates that the stage instance is publicly visible. - /// - Public = 1, - - /// - /// Indicates that the stage instance is only visible to guild members. - /// - GuildOnly -} +namespace DSharpPlus.Entities; + + +/// +/// Represents a stage instance's privacy level. +/// +public enum DiscordStagePrivacyLevel +{ + /// + /// Indicates that the stage instance is publicly visible. + /// + Public = 1, + + /// + /// Indicates that the stage instance is only visible to guild members. + /// + GuildOnly +} diff --git a/DSharpPlus/Entities/Channel/Thread/DiscordThreadChannel.cs b/DSharpPlus/Entities/Channel/Thread/DiscordThreadChannel.cs index 48231e71d8..830eed369a 100644 --- a/DSharpPlus/Entities/Channel/Thread/DiscordThreadChannel.cs +++ b/DSharpPlus/Entities/Channel/Thread/DiscordThreadChannel.cs @@ -1,196 +1,196 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using DSharpPlus.Exceptions; -using DSharpPlus.Net.Models; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents a Discord thread in a channel. -/// -public class DiscordThreadChannel : DiscordChannel -{ - /// - /// Gets the ID of this thread's creator. - /// - [JsonProperty("owner_id", NullValueHandling = NullValueHandling.Ignore)] - public ulong CreatorId { get; internal set; } - - /// - /// Gets the approximate count of messages in a thread, capped to 50. - /// - [JsonProperty("message_count", NullValueHandling = NullValueHandling.Ignore)] - public int? MessageCount { get; internal set; } - - /// - /// Gets the approximate count of members in a thread, capped to 50. - /// - [JsonProperty("member_count", NullValueHandling = NullValueHandling.Ignore)] - public int? MemberCount { get; internal set; } - - /// - /// Represents the current member for this thread. This will have a value if the user has joined the thread. - /// - [JsonProperty("member", NullValueHandling = NullValueHandling.Ignore)] - public DiscordThreadChannelMember CurrentMember { get; internal set; } - - /// - /// Gets the approximate count of members in a thread, up to 50. - /// - [JsonProperty("thread_metadata", NullValueHandling = NullValueHandling.Ignore)] - public DiscordThreadChannelMetadata ThreadMetadata { get; internal set; } - - /// - /// Gets whether this thread has been newly created. This property is not populated when fetched by REST. - /// - [JsonProperty("newly_created", NullValueHandling = NullValueHandling.Ignore)] - public bool IsNew { get; internal set; } - - /// - /// Gets the tags applied to this forum post. - /// - // Performant? No. Ideally, you're not using this property often. -#pragma warning disable IDE0046 // we don't want doubly nested ternaries here - public IReadOnlyList AppliedTags - { - get - { - // discord sends null if this thread never had tags applied, which means it has no tags. return empty. - if (this.appliedTagIds is null) - { - return []; - } - - return this.Parent is DiscordForumChannel parent - ? parent.AvailableTags.Where(pt => this.appliedTagIds.Contains(pt.Id)).ToArray() - : []; - } - } -#pragma warning restore IDE0046 - - /// - /// Gets the IDs of the tags applied to this forum post. - /// - public IReadOnlyList AppliedTagIds => this.appliedTagIds; - -#pragma warning disable CS0649 // Field is never assigned to, and will always have its default value null - // Justification: Used by JSON.NET - [JsonProperty("applied_tags")] - private readonly List appliedTagIds; -#pragma warning restore CS0649 - - #region Methods - - /// - /// Makes the current user join the thread. - /// - /// Thrown when Discord is unable to process the request. - public async Task JoinThreadAsync() - => await this.Discord.ApiClient.JoinThreadAsync(this.Id); - - /// - /// Makes the current user leave the thread. - /// - /// Thrown when Discord is unable to process the request. - public async Task LeaveThreadAsync() - => await this.Discord.ApiClient.LeaveThreadAsync(this.Id); - - /// - /// Returns a full list of the thread members in this thread. - /// Requires the intent specified in - /// - /// A collection of all threads members in this thread. - /// Thrown when Discord is unable to process the request. - public async Task> ListJoinedMembersAsync() - => await this.Discord.ApiClient.ListThreadMembersAsync(this.Id); - - /// - /// Adds the given DiscordMember to this thread. Requires an not archived thread and send message permissions. - /// - /// The member to add to the thread. - /// Thrown when the client does not have the . - /// Thrown when Discord is unable to process the request. - public async Task AddThreadMemberAsync(DiscordMember member) - { - if (this.ThreadMetadata.IsArchived) - { - throw new InvalidOperationException("You cannot add members to an archived thread."); - } - - await this.Discord.ApiClient.AddThreadMemberAsync(this.Id, member.Id); - } - - /// - /// Removes the given DiscordMember from this thread. Requires an not archived thread and send message permissions. - /// - /// The member to remove from the thread. - /// Thrown when the client does not have the permission, or is not the creator of the thread if it is private. - /// Thrown when Discord is unable to process the request. - public async Task RemoveThreadMemberAsync(DiscordMember member) - { - if (this.ThreadMetadata.IsArchived) - { - throw new InvalidOperationException("You cannot remove members from an archived thread."); - } - - await this.Discord.ApiClient.RemoveThreadMemberAsync(this.Id, member.Id); - } - - /// - /// Modifies the current thread. - /// - /// Action to perform on this thread - /// Thrown when the client does not have the permission. - /// Thrown when the channel does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public async Task ModifyAsync(Action action) - { - ThreadChannelEditModel mdl = new(); - action(mdl); - await this.Discord.ApiClient.ModifyThreadChannelAsync(this.Id, mdl.Name, mdl.Position, mdl.Topic, mdl.Nsfw, - mdl.Parent.HasValue ? mdl.Parent.Value?.Id : default(Optional), mdl.Bitrate, mdl.Userlimit, mdl.PerUserRateLimit, mdl.RtcRegion.IfPresent(r => r?.Id), - mdl.QualityMode, mdl.Type, mdl.PermissionOverwrites, mdl.IsArchived, mdl.AutoArchiveDuration, mdl.Locked, mdl.AppliedTags, mdl.IsInvitable, mdl.AuditLogReason); - - // We set these *after* the rest request so that Discord can validate the properties. This is useful if the requirements ever change. - if (!string.IsNullOrWhiteSpace(mdl.Name)) - { - this.Name = mdl.Name; - } - - if (mdl.PerUserRateLimit.HasValue) - { - this.PerUserRateLimit = mdl.PerUserRateLimit.Value; - } - - if (mdl.IsArchived.HasValue) - { - this.ThreadMetadata.IsArchived = mdl.IsArchived.Value; - } - - if (mdl.AutoArchiveDuration.HasValue) - { - this.ThreadMetadata.AutoArchiveDuration = mdl.AutoArchiveDuration.Value; - } - - if (mdl.Locked.HasValue) - { - this.ThreadMetadata.IsLocked = mdl.Locked.Value; - } - } - - /// - /// Returns a thread member object for the specified user if they are a member of the thread, returns a 404 response otherwise. - /// - /// The guild member to retrieve. - /// Thrown when a GuildMember has not joined the channel thread. - public async Task GetThreadMemberAsync(DiscordMember member) - => await this.Discord.ApiClient.GetThreadMemberAsync(this.Id, member.Id); - - #endregion - - internal DiscordThreadChannel() { } -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using DSharpPlus.Exceptions; +using DSharpPlus.Net.Models; +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Represents a Discord thread in a channel. +/// +public class DiscordThreadChannel : DiscordChannel +{ + /// + /// Gets the ID of this thread's creator. + /// + [JsonProperty("owner_id", NullValueHandling = NullValueHandling.Ignore)] + public ulong CreatorId { get; internal set; } + + /// + /// Gets the approximate count of messages in a thread, capped to 50. + /// + [JsonProperty("message_count", NullValueHandling = NullValueHandling.Ignore)] + public int? MessageCount { get; internal set; } + + /// + /// Gets the approximate count of members in a thread, capped to 50. + /// + [JsonProperty("member_count", NullValueHandling = NullValueHandling.Ignore)] + public int? MemberCount { get; internal set; } + + /// + /// Represents the current member for this thread. This will have a value if the user has joined the thread. + /// + [JsonProperty("member", NullValueHandling = NullValueHandling.Ignore)] + public DiscordThreadChannelMember CurrentMember { get; internal set; } + + /// + /// Gets the approximate count of members in a thread, up to 50. + /// + [JsonProperty("thread_metadata", NullValueHandling = NullValueHandling.Ignore)] + public DiscordThreadChannelMetadata ThreadMetadata { get; internal set; } + + /// + /// Gets whether this thread has been newly created. This property is not populated when fetched by REST. + /// + [JsonProperty("newly_created", NullValueHandling = NullValueHandling.Ignore)] + public bool IsNew { get; internal set; } + + /// + /// Gets the tags applied to this forum post. + /// + // Performant? No. Ideally, you're not using this property often. +#pragma warning disable IDE0046 // we don't want doubly nested ternaries here + public IReadOnlyList AppliedTags + { + get + { + // discord sends null if this thread never had tags applied, which means it has no tags. return empty. + if (this.appliedTagIds is null) + { + return []; + } + + return this.Parent is DiscordForumChannel parent + ? parent.AvailableTags.Where(pt => this.appliedTagIds.Contains(pt.Id)).ToArray() + : []; + } + } +#pragma warning restore IDE0046 + + /// + /// Gets the IDs of the tags applied to this forum post. + /// + public IReadOnlyList AppliedTagIds => this.appliedTagIds; + +#pragma warning disable CS0649 // Field is never assigned to, and will always have its default value null + // Justification: Used by JSON.NET + [JsonProperty("applied_tags")] + private readonly List appliedTagIds; +#pragma warning restore CS0649 + + #region Methods + + /// + /// Makes the current user join the thread. + /// + /// Thrown when Discord is unable to process the request. + public async Task JoinThreadAsync() + => await this.Discord.ApiClient.JoinThreadAsync(this.Id); + + /// + /// Makes the current user leave the thread. + /// + /// Thrown when Discord is unable to process the request. + public async Task LeaveThreadAsync() + => await this.Discord.ApiClient.LeaveThreadAsync(this.Id); + + /// + /// Returns a full list of the thread members in this thread. + /// Requires the intent specified in + /// + /// A collection of all threads members in this thread. + /// Thrown when Discord is unable to process the request. + public async Task> ListJoinedMembersAsync() + => await this.Discord.ApiClient.ListThreadMembersAsync(this.Id); + + /// + /// Adds the given DiscordMember to this thread. Requires an not archived thread and send message permissions. + /// + /// The member to add to the thread. + /// Thrown when the client does not have the . + /// Thrown when Discord is unable to process the request. + public async Task AddThreadMemberAsync(DiscordMember member) + { + if (this.ThreadMetadata.IsArchived) + { + throw new InvalidOperationException("You cannot add members to an archived thread."); + } + + await this.Discord.ApiClient.AddThreadMemberAsync(this.Id, member.Id); + } + + /// + /// Removes the given DiscordMember from this thread. Requires an not archived thread and send message permissions. + /// + /// The member to remove from the thread. + /// Thrown when the client does not have the permission, or is not the creator of the thread if it is private. + /// Thrown when Discord is unable to process the request. + public async Task RemoveThreadMemberAsync(DiscordMember member) + { + if (this.ThreadMetadata.IsArchived) + { + throw new InvalidOperationException("You cannot remove members from an archived thread."); + } + + await this.Discord.ApiClient.RemoveThreadMemberAsync(this.Id, member.Id); + } + + /// + /// Modifies the current thread. + /// + /// Action to perform on this thread + /// Thrown when the client does not have the permission. + /// Thrown when the channel does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task ModifyAsync(Action action) + { + ThreadChannelEditModel mdl = new(); + action(mdl); + await this.Discord.ApiClient.ModifyThreadChannelAsync(this.Id, mdl.Name, mdl.Position, mdl.Topic, mdl.Nsfw, + mdl.Parent.HasValue ? mdl.Parent.Value?.Id : default(Optional), mdl.Bitrate, mdl.Userlimit, mdl.PerUserRateLimit, mdl.RtcRegion.IfPresent(r => r?.Id), + mdl.QualityMode, mdl.Type, mdl.PermissionOverwrites, mdl.IsArchived, mdl.AutoArchiveDuration, mdl.Locked, mdl.AppliedTags, mdl.IsInvitable, mdl.AuditLogReason); + + // We set these *after* the rest request so that Discord can validate the properties. This is useful if the requirements ever change. + if (!string.IsNullOrWhiteSpace(mdl.Name)) + { + this.Name = mdl.Name; + } + + if (mdl.PerUserRateLimit.HasValue) + { + this.PerUserRateLimit = mdl.PerUserRateLimit.Value; + } + + if (mdl.IsArchived.HasValue) + { + this.ThreadMetadata.IsArchived = mdl.IsArchived.Value; + } + + if (mdl.AutoArchiveDuration.HasValue) + { + this.ThreadMetadata.AutoArchiveDuration = mdl.AutoArchiveDuration.Value; + } + + if (mdl.Locked.HasValue) + { + this.ThreadMetadata.IsLocked = mdl.Locked.Value; + } + } + + /// + /// Returns a thread member object for the specified user if they are a member of the thread, returns a 404 response otherwise. + /// + /// The guild member to retrieve. + /// Thrown when a GuildMember has not joined the channel thread. + public async Task GetThreadMemberAsync(DiscordMember member) + => await this.Discord.ApiClient.GetThreadMemberAsync(this.Id, member.Id); + + #endregion + + internal DiscordThreadChannel() { } +} diff --git a/DSharpPlus/Entities/Channel/Thread/DiscordThreadChannelMember.cs b/DSharpPlus/Entities/Channel/Thread/DiscordThreadChannelMember.cs index 513c9a0930..feff2e1ed0 100644 --- a/DSharpPlus/Entities/Channel/Thread/DiscordThreadChannelMember.cs +++ b/DSharpPlus/Entities/Channel/Thread/DiscordThreadChannelMember.cs @@ -1,104 +1,104 @@ -using System; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -public class DiscordThreadChannelMember -{ - /// - /// Gets ID of the thread. - /// - [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)] - public ulong ThreadId { get; set; } - - /// - /// Gets ID of the user. - /// - [JsonProperty("user_id", NullValueHandling = NullValueHandling.Ignore)] - public ulong Id { get; set; } - - /// - /// Gets timestamp when the user joined the thread. - /// - [JsonProperty("join_timestamp", NullValueHandling = NullValueHandling.Ignore)] - public DateTimeOffset? JoinTimeStamp { get; internal set; } - - [JsonProperty("flags", NullValueHandling = NullValueHandling.Ignore)] - internal int UserFlags { get; set; } - - /// - /// Gets the DiscordMember that represents this ThreadMember. Can be a skeleton object. - /// - [JsonIgnore] - public DiscordMember Member - => this.Guild != null ? this.Guild.members.TryGetValue(this.Id, out DiscordMember? member) ? member : new DiscordMember { Id = this.Id, guild_id = this.guild_id, Discord = this.Discord } : null; - - /// - /// Gets the category that contains this channel. For threads, gets the channel this thread was created in. - /// - [JsonIgnore] - public DiscordChannel Thread - => this.Guild != null ? this.Guild.threads.TryGetValue(this.ThreadId, out DiscordThreadChannel? thread) ? thread : null : null; - - /// - /// Gets the guild to which this channel belongs. - /// - [JsonIgnore] - public DiscordGuild Guild - => this.Discord.Guilds.TryGetValue(this.guild_id, out DiscordGuild? guild) ? guild : null; - - [JsonIgnore] - internal ulong guild_id; - - /// - /// Gets the client instance this object is tied to. - /// - [JsonIgnore] - internal BaseDiscordClient Discord { get; set; } - - internal DiscordThreadChannelMember() { } - - /// - /// 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) => Equals(obj as DiscordThreadChannelMember); - - /// - /// Checks whether this is equal to another . - /// - /// to compare to. - /// Whether the is equal to this . - public bool Equals(DiscordThreadChannelMember e) => e is not null && (ReferenceEquals(this, e) || (this.Id == e.Id && this.ThreadId == e.ThreadId)); - - /// - /// Gets the hash code for this . - /// - /// The hash code for this . - public override int GetHashCode() => HashCode.Combine(this.Id, this.ThreadId); - - /// - /// 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 ==(DiscordThreadChannelMember e1, DiscordThreadChannelMember e2) - { - object? o1 = e1; - object? o2 = e2; - - return (o1 != null || o2 == null) && (o1 == null || o2 != null) -&& ((o1 == null && o2 == null) || (e1.Id == e2.Id && e1.ThreadId == e2.ThreadId)); - } - - /// - /// 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 !=(DiscordThreadChannelMember e1, DiscordThreadChannelMember e2) - => !(e1 == e2); -} +using System; +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +public class DiscordThreadChannelMember +{ + /// + /// Gets ID of the thread. + /// + [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)] + public ulong ThreadId { get; set; } + + /// + /// Gets ID of the user. + /// + [JsonProperty("user_id", NullValueHandling = NullValueHandling.Ignore)] + public ulong Id { get; set; } + + /// + /// Gets timestamp when the user joined the thread. + /// + [JsonProperty("join_timestamp", NullValueHandling = NullValueHandling.Ignore)] + public DateTimeOffset? JoinTimeStamp { get; internal set; } + + [JsonProperty("flags", NullValueHandling = NullValueHandling.Ignore)] + internal int UserFlags { get; set; } + + /// + /// Gets the DiscordMember that represents this ThreadMember. Can be a skeleton object. + /// + [JsonIgnore] + public DiscordMember Member + => this.Guild != null ? this.Guild.members.TryGetValue(this.Id, out DiscordMember? member) ? member : new DiscordMember { Id = this.Id, guild_id = this.guild_id, Discord = this.Discord } : null; + + /// + /// Gets the category that contains this channel. For threads, gets the channel this thread was created in. + /// + [JsonIgnore] + public DiscordChannel Thread + => this.Guild != null ? this.Guild.threads.TryGetValue(this.ThreadId, out DiscordThreadChannel? thread) ? thread : null : null; + + /// + /// Gets the guild to which this channel belongs. + /// + [JsonIgnore] + public DiscordGuild Guild + => this.Discord.Guilds.TryGetValue(this.guild_id, out DiscordGuild? guild) ? guild : null; + + [JsonIgnore] + internal ulong guild_id; + + /// + /// Gets the client instance this object is tied to. + /// + [JsonIgnore] + internal BaseDiscordClient Discord { get; set; } + + internal DiscordThreadChannelMember() { } + + /// + /// 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) => Equals(obj as DiscordThreadChannelMember); + + /// + /// Checks whether this is equal to another . + /// + /// to compare to. + /// Whether the is equal to this . + public bool Equals(DiscordThreadChannelMember e) => e is not null && (ReferenceEquals(this, e) || (this.Id == e.Id && this.ThreadId == e.ThreadId)); + + /// + /// Gets the hash code for this . + /// + /// The hash code for this . + public override int GetHashCode() => HashCode.Combine(this.Id, this.ThreadId); + + /// + /// 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 ==(DiscordThreadChannelMember e1, DiscordThreadChannelMember e2) + { + object? o1 = e1; + object? o2 = e2; + + return (o1 != null || o2 == null) && (o1 == null || o2 != null) +&& ((o1 == null && o2 == null) || (e1.Id == e2.Id && e1.ThreadId == e2.ThreadId)); + } + + /// + /// 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 !=(DiscordThreadChannelMember e1, DiscordThreadChannelMember e2) + => !(e1 == e2); +} diff --git a/DSharpPlus/Entities/Channel/Thread/DiscordThreadChannelMetadata.cs b/DSharpPlus/Entities/Channel/Thread/DiscordThreadChannelMetadata.cs index e941038723..743bc2b79d 100644 --- a/DSharpPlus/Entities/Channel/Thread/DiscordThreadChannelMetadata.cs +++ b/DSharpPlus/Entities/Channel/Thread/DiscordThreadChannelMetadata.cs @@ -1,50 +1,50 @@ -using System; -using System.Globalization; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -public class DiscordThreadChannelMetadata -{ - /// - /// Gets whether this thread is archived or not. - /// - [JsonProperty("archived", NullValueHandling = NullValueHandling.Ignore)] - public bool IsArchived { get; internal set; } - - /// - /// Gets the duration in minutes to automatically archive the thread after recent activity. Can be set to: 60, 1440, 4320, 10080. - /// - [JsonProperty("auto_archive_duration", NullValueHandling = NullValueHandling.Ignore)] - public DiscordAutoArchiveDuration AutoArchiveDuration { get; internal set; } - - /// - /// Gets the time timestamp for when the thread's archive status was last changed. - /// - [JsonProperty("archive_timestamp", NullValueHandling = NullValueHandling.Ignore)] - public DateTimeOffset? ArchiveTimestamp { get; internal set; } - - /// - /// Gets whether this thread is locked or not. - /// - [JsonProperty("locked", NullValueHandling = NullValueHandling.Ignore)] - public bool? IsLocked { get; internal set; } - - /// - /// whether non-moderators can add other non-moderators to a thread. Only available on private threads - /// - [JsonProperty("invitable", NullValueHandling = NullValueHandling.Ignore)] - public bool? IsInvitable { get; internal set; } - - /// - /// Gets the time this thread was created. Only populated for threads created after 2022-01-09 (YYYY-MM-DD). - /// - public DateTimeOffset? CreationTimestamp - => !string.IsNullOrWhiteSpace(this.CreateTimestampRaw) && DateTimeOffset.TryParse(this.CreateTimestampRaw, CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTimeOffset dto) ? - dto : null; - - [JsonProperty("create_timestamp", NullValueHandling = NullValueHandling.Ignore)] - internal string CreateTimestampRaw { get; set; } - - internal DiscordThreadChannelMetadata() { } -} +using System; +using System.Globalization; +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +public class DiscordThreadChannelMetadata +{ + /// + /// Gets whether this thread is archived or not. + /// + [JsonProperty("archived", NullValueHandling = NullValueHandling.Ignore)] + public bool IsArchived { get; internal set; } + + /// + /// Gets the duration in minutes to automatically archive the thread after recent activity. Can be set to: 60, 1440, 4320, 10080. + /// + [JsonProperty("auto_archive_duration", NullValueHandling = NullValueHandling.Ignore)] + public DiscordAutoArchiveDuration AutoArchiveDuration { get; internal set; } + + /// + /// Gets the time timestamp for when the thread's archive status was last changed. + /// + [JsonProperty("archive_timestamp", NullValueHandling = NullValueHandling.Ignore)] + public DateTimeOffset? ArchiveTimestamp { get; internal set; } + + /// + /// Gets whether this thread is locked or not. + /// + [JsonProperty("locked", NullValueHandling = NullValueHandling.Ignore)] + public bool? IsLocked { get; internal set; } + + /// + /// whether non-moderators can add other non-moderators to a thread. Only available on private threads + /// + [JsonProperty("invitable", NullValueHandling = NullValueHandling.Ignore)] + public bool? IsInvitable { get; internal set; } + + /// + /// Gets the time this thread was created. Only populated for threads created after 2022-01-09 (YYYY-MM-DD). + /// + public DateTimeOffset? CreationTimestamp + => !string.IsNullOrWhiteSpace(this.CreateTimestampRaw) && DateTimeOffset.TryParse(this.CreateTimestampRaw, CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTimeOffset dto) ? + dto : null; + + [JsonProperty("create_timestamp", NullValueHandling = NullValueHandling.Ignore)] + internal string CreateTimestampRaw { get; set; } + + internal DiscordThreadChannelMetadata() { } +} diff --git a/DSharpPlus/Entities/Channel/Thread/Forum/DefaultReaction.cs b/DSharpPlus/Entities/Channel/Thread/Forum/DefaultReaction.cs index 45bf698b67..ad715d6225 100644 --- a/DSharpPlus/Entities/Channel/Thread/Forum/DefaultReaction.cs +++ b/DSharpPlus/Entities/Channel/Thread/Forum/DefaultReaction.cs @@ -1,30 +1,30 @@ -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents an emoji used for reacting to a forum post. -/// -public sealed class DefaultReaction -{ - /// - /// The ID of the emoji, if applicable. - /// - [JsonProperty("emoji_id")] - public ulong? EmojiId { get; internal set; } - - /// - /// The unicode emoji, if applicable. - /// - [JsonProperty("emoji_name")] - public string? EmojiName { get; internal set; } - - /// - /// Creates a DefaultReaction object from an emoji. - /// - /// The . - /// Create object. - public static DefaultReaction FromEmoji(DiscordEmoji emoji) => emoji.Id == 0 - ? new DefaultReaction { EmojiName = emoji.Name } - : new DefaultReaction { EmojiId = emoji.Id }; -} +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Represents an emoji used for reacting to a forum post. +/// +public sealed class DefaultReaction +{ + /// + /// The ID of the emoji, if applicable. + /// + [JsonProperty("emoji_id")] + public ulong? EmojiId { get; internal set; } + + /// + /// The unicode emoji, if applicable. + /// + [JsonProperty("emoji_name")] + public string? EmojiName { get; internal set; } + + /// + /// Creates a DefaultReaction object from an emoji. + /// + /// The . + /// Create object. + public static DefaultReaction FromEmoji(DiscordEmoji emoji) => emoji.Id == 0 + ? new DefaultReaction { EmojiName = emoji.Name } + : new DefaultReaction { EmojiId = emoji.Id }; +} diff --git a/DSharpPlus/Entities/Channel/Thread/Forum/DefaultSortOrder.cs b/DSharpPlus/Entities/Channel/Thread/Forum/DefaultSortOrder.cs index f09dd2671e..55c68a111d 100644 --- a/DSharpPlus/Entities/Channel/Thread/Forum/DefaultSortOrder.cs +++ b/DSharpPlus/Entities/Channel/Thread/Forum/DefaultSortOrder.cs @@ -1,17 +1,17 @@ -namespace DSharpPlus.Entities; - - -/// -/// The sort order for forum channels. -/// -public enum DiscordDefaultSortOrder -{ - /// - /// Sorts posts by the latest message in the thread. - /// - LatestActivity, - /// - /// Sorts posts by the creation of the post itself. - /// - CreationDate -} +namespace DSharpPlus.Entities; + + +/// +/// The sort order for forum channels. +/// +public enum DiscordDefaultSortOrder +{ + /// + /// Sorts posts by the latest message in the thread. + /// + LatestActivity, + /// + /// Sorts posts by the creation of the post itself. + /// + CreationDate +} diff --git a/DSharpPlus/Entities/Channel/Thread/Forum/DiscordDefaultForumLayout.cs b/DSharpPlus/Entities/Channel/Thread/Forum/DiscordDefaultForumLayout.cs index fd76793787..5123f73ed9 100644 --- a/DSharpPlus/Entities/Channel/Thread/Forum/DiscordDefaultForumLayout.cs +++ b/DSharpPlus/Entities/Channel/Thread/Forum/DiscordDefaultForumLayout.cs @@ -1,23 +1,23 @@ -namespace DSharpPlus.Entities; - - -/// -/// The layout type for forum channels. -/// -public enum DiscordDefaultForumLayout -{ - /// - /// The channel doesn't have a set layout. - /// - Unset, - - /// - /// Posts will be displayed in a list format. - /// - ListView, - - /// - /// Posts will be displayed in a grid format that prioritizes image previews over the forum's content. - /// - GalleryView -} +namespace DSharpPlus.Entities; + + +/// +/// The layout type for forum channels. +/// +public enum DiscordDefaultForumLayout +{ + /// + /// The channel doesn't have a set layout. + /// + Unset, + + /// + /// Posts will be displayed in a list format. + /// + ListView, + + /// + /// Posts will be displayed in a grid format that prioritizes image previews over the forum's content. + /// + GalleryView +} diff --git a/DSharpPlus/Entities/Channel/Thread/Forum/DiscordForumChannel.cs b/DSharpPlus/Entities/Channel/Thread/Forum/DiscordForumChannel.cs index e36ea3a552..85b69f747e 100644 --- a/DSharpPlus/Entities/Channel/Thread/Forum/DiscordForumChannel.cs +++ b/DSharpPlus/Entities/Channel/Thread/Forum/DiscordForumChannel.cs @@ -1,66 +1,66 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents either a forum channel or a post in the forum. -/// -public sealed class DiscordForumChannel : DiscordChannel -{ - [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] - public override DiscordChannelType Type => DiscordChannelType.GuildForum; - - /// - /// Gets the topic of the forum. This doubles as the guidelines for the forum. - /// - [JsonProperty("topic")] - public new string Topic { get; internal set; } - - /// - /// Gets the default ratelimit per user for the forum. This is applied to all posts upon creation. - /// - [JsonProperty("default_thread_rate_limit_per_user")] - public int? DefaultPerUserRateLimit { get; internal set; } - - /// - /// Gets the available tags for the forum. - /// - public IReadOnlyList AvailableTags => this.availableTags; - -#pragma warning disable CS0649 // Field is never assigned to, and will always have its default value null - // Justification: Used by JSON.NET - [JsonProperty("available_tags")] - private readonly List availableTags; -#pragma warning restore CS0649 - - /// - /// The default reaction shown on posts when they are created. - /// - [JsonProperty("default_reaction_emoji", NullValueHandling = NullValueHandling.Ignore)] - public DefaultReaction? DefaultReaction { get; internal set; } - - /// - /// The default sort order of posts in the forum. - /// - [JsonProperty("default_sort_order", NullValueHandling = NullValueHandling.Ignore)] - public DiscordDefaultSortOrder? DefaultSortOrder { get; internal set; } - - /// - /// The default layout of posts in the forum. Defaults to - /// - [JsonProperty("default_forum_layout", NullValueHandling = NullValueHandling.Ignore)] - public DiscordDefaultForumLayout? DefaultLayout { get; internal set; } - - /// - /// Creates a forum post. - /// - /// The builder to create the forum post with. - /// The starter (the created thread, and the initial message) from creating the post. - public async Task CreateForumPostAsync(ForumPostBuilder builder) - => await this.Discord.ApiClient.CreateForumPostAsync(this.Id, builder.Name, builder.Message, builder.AutoArchiveDuration, builder.SlowMode, builder.AppliedTags.Select(t => t.Id)); - - internal DiscordForumChannel() { } -} +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Represents either a forum channel or a post in the forum. +/// +public sealed class DiscordForumChannel : DiscordChannel +{ + [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] + public override DiscordChannelType Type => DiscordChannelType.GuildForum; + + /// + /// Gets the topic of the forum. This doubles as the guidelines for the forum. + /// + [JsonProperty("topic")] + public new string Topic { get; internal set; } + + /// + /// Gets the default ratelimit per user for the forum. This is applied to all posts upon creation. + /// + [JsonProperty("default_thread_rate_limit_per_user")] + public int? DefaultPerUserRateLimit { get; internal set; } + + /// + /// Gets the available tags for the forum. + /// + public IReadOnlyList AvailableTags => this.availableTags; + +#pragma warning disable CS0649 // Field is never assigned to, and will always have its default value null + // Justification: Used by JSON.NET + [JsonProperty("available_tags")] + private readonly List availableTags; +#pragma warning restore CS0649 + + /// + /// The default reaction shown on posts when they are created. + /// + [JsonProperty("default_reaction_emoji", NullValueHandling = NullValueHandling.Ignore)] + public DefaultReaction? DefaultReaction { get; internal set; } + + /// + /// The default sort order of posts in the forum. + /// + [JsonProperty("default_sort_order", NullValueHandling = NullValueHandling.Ignore)] + public DiscordDefaultSortOrder? DefaultSortOrder { get; internal set; } + + /// + /// The default layout of posts in the forum. Defaults to + /// + [JsonProperty("default_forum_layout", NullValueHandling = NullValueHandling.Ignore)] + public DiscordDefaultForumLayout? DefaultLayout { get; internal set; } + + /// + /// Creates a forum post. + /// + /// The builder to create the forum post with. + /// The starter (the created thread, and the initial message) from creating the post. + public async Task CreateForumPostAsync(ForumPostBuilder builder) + => await this.Discord.ApiClient.CreateForumPostAsync(this.Id, builder.Name, builder.Message, builder.AutoArchiveDuration, builder.SlowMode, builder.AppliedTags.Select(t => t.Id)); + + internal DiscordForumChannel() { } +} diff --git a/DSharpPlus/Entities/Channel/Thread/Forum/DiscordForumPostStarter.cs b/DSharpPlus/Entities/Channel/Thread/Forum/DiscordForumPostStarter.cs index cd303623bf..0cc0a3f9b1 100644 --- a/DSharpPlus/Entities/Channel/Thread/Forum/DiscordForumPostStarter.cs +++ b/DSharpPlus/Entities/Channel/Thread/Forum/DiscordForumPostStarter.cs @@ -1,25 +1,25 @@ -namespace DSharpPlus.Entities; - - -/// -/// Represents the return of creating a forum post. -/// -public sealed class DiscordForumPostStarter -{ - /// - /// The channel of the forum post. - /// - public DiscordThreadChannel Channel { get; internal set; } - /// - /// The message of the forum post. - /// - public DiscordMessage Message { get; internal set; } - - internal DiscordForumPostStarter() { } - - internal DiscordForumPostStarter(DiscordThreadChannel chn, DiscordMessage msg) - { - this.Channel = chn; - this.Message = msg; - } -} +namespace DSharpPlus.Entities; + + +/// +/// Represents the return of creating a forum post. +/// +public sealed class DiscordForumPostStarter +{ + /// + /// The channel of the forum post. + /// + public DiscordThreadChannel Channel { get; internal set; } + /// + /// The message of the forum post. + /// + public DiscordMessage Message { get; internal set; } + + internal DiscordForumPostStarter() { } + + internal DiscordForumPostStarter(DiscordThreadChannel chn, DiscordMessage msg) + { + this.Channel = chn; + this.Message = msg; + } +} diff --git a/DSharpPlus/Entities/Channel/Thread/Forum/DiscordForumTag.cs b/DSharpPlus/Entities/Channel/Thread/Forum/DiscordForumTag.cs index f819659bbd..24dc1c4fd8 100644 --- a/DSharpPlus/Entities/Channel/Thread/Forum/DiscordForumTag.cs +++ b/DSharpPlus/Entities/Channel/Thread/Forum/DiscordForumTag.cs @@ -1,112 +1,112 @@ -using System.Diagnostics.CodeAnalysis; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -public sealed class DiscordForumTag : SnowflakeObject -{ - /// - /// Gets the name of this tag. - /// - [JsonProperty("name")] - public string Name { get; internal set; } - - /// - /// Gets whether this tag is moderated. Moderated tags can only be applied by users with the permission. - /// - [JsonProperty("moderated")] - public bool Moderated { get; internal set; } - - /// - /// Gets the Id of the emoji for this tag, if applicable. - /// - [JsonProperty("emoji_id")] - public ulong? EmojiId { get; internal set; } - - /// - /// Gets the unicode emoji for this tag, if applicable. - /// - [JsonProperty("emoji_name")] - public string EmojiName { get; internal set; } -} - -public class DiscordForumTagBuilder -{ - [JsonProperty("name"), SuppressMessage("Code Quality", "IDE0052:Remove unread private members", Justification = "This is used by JSON.NET.")] - private string name; - - [JsonProperty("moderated"), SuppressMessage("Code Quality", "IDE0052:Remove unread private members", Justification = "This is used by JSON.NET.")] - private bool moderated; - - [JsonProperty("emoji_id"), SuppressMessage("Code Quality", "IDE0052:Remove unread private members", Justification = "This is used by JSON.NET.")] - private ulong? emojiId; - - [JsonProperty("emoji_name"), SuppressMessage("Code Quality", "IDE0052:Remove unread private members", Justification = "This is used by JSON.NET.")] - private string emojiName; - - public static DiscordForumTagBuilder FromTag(DiscordForumTag tag) - { - DiscordForumTagBuilder builder = new() - { - name = tag.Name, - moderated = tag.Moderated, - emojiId = tag.EmojiId, - emojiName = tag.EmojiName - }; - return builder; - } - - /// - /// Sets the name of this tag. - /// - /// The name of the tag. - /// The builder to chain calls with. - public DiscordForumTagBuilder WithName(string name) - { - this.name = name; - return this; - } - - /// - /// Sets this tag to be moderated (as in, it can only be set by users with the permission). - /// - /// Whether the tag is moderated. - /// The builder to chain calls with. - public DiscordForumTagBuilder IsModerated(bool moderated = true) - { - this.moderated = moderated; - return this; - } - - /// - /// Sets the emoji ID for this tag (which will overwrite the emoji name). - /// - /// - /// The builder to chain calls with. - public DiscordForumTagBuilder WithEmojiId(ulong? emojiId) - { - this.emojiId = emojiId; - this.emojiName = null; - return this; - } - - /// - /// Sets the emoji for this tag. - /// - /// The emoji to use. - /// The builder to chain calls with. - public DiscordForumTagBuilder WithEmoji(DiscordEmoji emoji) - { - this.emojiId = emoji.Id; - this.emojiName = emoji.Name; - return this; - } - - /// The builder to chain calls with. - public DiscordForumTagBuilder WithEmojiName(string emojiName) - { - this.emojiId = null; - this.emojiName = emojiName; - return this; - } -} +using System.Diagnostics.CodeAnalysis; +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +public sealed class DiscordForumTag : SnowflakeObject +{ + /// + /// Gets the name of this tag. + /// + [JsonProperty("name")] + public string Name { get; internal set; } + + /// + /// Gets whether this tag is moderated. Moderated tags can only be applied by users with the permission. + /// + [JsonProperty("moderated")] + public bool Moderated { get; internal set; } + + /// + /// Gets the Id of the emoji for this tag, if applicable. + /// + [JsonProperty("emoji_id")] + public ulong? EmojiId { get; internal set; } + + /// + /// Gets the unicode emoji for this tag, if applicable. + /// + [JsonProperty("emoji_name")] + public string EmojiName { get; internal set; } +} + +public class DiscordForumTagBuilder +{ + [JsonProperty("name"), SuppressMessage("Code Quality", "IDE0052:Remove unread private members", Justification = "This is used by JSON.NET.")] + private string name; + + [JsonProperty("moderated"), SuppressMessage("Code Quality", "IDE0052:Remove unread private members", Justification = "This is used by JSON.NET.")] + private bool moderated; + + [JsonProperty("emoji_id"), SuppressMessage("Code Quality", "IDE0052:Remove unread private members", Justification = "This is used by JSON.NET.")] + private ulong? emojiId; + + [JsonProperty("emoji_name"), SuppressMessage("Code Quality", "IDE0052:Remove unread private members", Justification = "This is used by JSON.NET.")] + private string emojiName; + + public static DiscordForumTagBuilder FromTag(DiscordForumTag tag) + { + DiscordForumTagBuilder builder = new() + { + name = tag.Name, + moderated = tag.Moderated, + emojiId = tag.EmojiId, + emojiName = tag.EmojiName + }; + return builder; + } + + /// + /// Sets the name of this tag. + /// + /// The name of the tag. + /// The builder to chain calls with. + public DiscordForumTagBuilder WithName(string name) + { + this.name = name; + return this; + } + + /// + /// Sets this tag to be moderated (as in, it can only be set by users with the permission). + /// + /// Whether the tag is moderated. + /// The builder to chain calls with. + public DiscordForumTagBuilder IsModerated(bool moderated = true) + { + this.moderated = moderated; + return this; + } + + /// + /// Sets the emoji ID for this tag (which will overwrite the emoji name). + /// + /// + /// The builder to chain calls with. + public DiscordForumTagBuilder WithEmojiId(ulong? emojiId) + { + this.emojiId = emojiId; + this.emojiName = null; + return this; + } + + /// + /// Sets the emoji for this tag. + /// + /// The emoji to use. + /// The builder to chain calls with. + public DiscordForumTagBuilder WithEmoji(DiscordEmoji emoji) + { + this.emojiId = emoji.Id; + this.emojiName = emoji.Name; + return this; + } + + /// The builder to chain calls with. + public DiscordForumTagBuilder WithEmojiName(string emojiName) + { + this.emojiId = null; + this.emojiName = emojiName; + return this; + } +} diff --git a/DSharpPlus/Entities/Channel/Thread/Forum/ForumPostBuilder.cs b/DSharpPlus/Entities/Channel/Thread/Forum/ForumPostBuilder.cs index 3ac62d9bef..8bb40e47ad 100644 --- a/DSharpPlus/Entities/Channel/Thread/Forum/ForumPostBuilder.cs +++ b/DSharpPlus/Entities/Channel/Thread/Forum/ForumPostBuilder.cs @@ -1,128 +1,128 @@ -using System; -using System.Collections.Generic; - -namespace DSharpPlus.Entities; - -/// -/// A builder to create a forum post. -/// -public class ForumPostBuilder -{ - /// - /// The name (or title) of the post. - /// - public string Name { get; set; } - - /// - /// The time (in seconds) that users must wait between messages. - /// - public int? SlowMode { get; set; } - - /// - /// The message to initiate the forum post with. - /// - public DiscordMessageBuilder Message { get; set; } - - /// - /// The tags to apply to this post. - /// - public IReadOnlyList AppliedTags { get; } - - /// - /// When to automatically archive the post. - /// - public DiscordAutoArchiveDuration? AutoArchiveDuration { get; set; } - - /// - /// Creates a new forum post builder. - /// - public ForumPostBuilder() => this.AppliedTags = new List(); - - /// - /// Sets the name (or title) of the post. - /// - /// The name of the post. - /// The builder to chain calls with - public ForumPostBuilder WithName(string name) - { - this.Name = name; - return this; - } - - /// - /// Sets slowmode for the post. - /// - /// The time in seconds to apply - /// - public ForumPostBuilder WithSlowMode(int slowMode) - { - this.SlowMode = slowMode; - return this; - } - - /// - /// Sets slow mode for the post. - /// - /// The slowmode delay to set. - /// The builder to chain calls with. - public ForumPostBuilder WithSlowMode(TimeSpan slowMode) - { - this.SlowMode = (int)slowMode.TotalSeconds; - return this; - } - - /// - /// Sets the message to initiate the forum post with. - /// - /// The message to start the post with. - /// The builder to chain calls with. - public ForumPostBuilder WithMessage(DiscordMessageBuilder message) - { - this.Message = message; - return this; - } - - /// - /// Sets the auto archive duration for the post. - /// - /// The duration in which the post will automatically archive - /// The builder to chain calls with - public ForumPostBuilder WithAutoArchiveDuration(DiscordAutoArchiveDuration autoArchiveDuration) - { - this.AutoArchiveDuration = autoArchiveDuration; - return this; - } - - /// - /// Adds a tag to the post. - /// - /// The tag to add. - /// The builder to chain calls with. - public ForumPostBuilder AddTag(DiscordForumTag tag) - { - ((List)this.AppliedTags).Add(tag); - return this; - } - - /// - /// Adds several tags to the post. - /// - /// The tags to add. - /// The builder to chain calls with. - public ForumPostBuilder AddTags(IEnumerable tags) - { - ((List)this.AppliedTags).AddRange(tags); - return this; - } - - /// - /// Removes a tag from the post. - /// - /// - /// - public ForumPostBuilder RemoveTag(DiscordForumTag tag) - { - ((List)this.AppliedTags).Remove(tag); - return this; - } -} +using System; +using System.Collections.Generic; + +namespace DSharpPlus.Entities; + +/// +/// A builder to create a forum post. +/// +public class ForumPostBuilder +{ + /// + /// The name (or title) of the post. + /// + public string Name { get; set; } + + /// + /// The time (in seconds) that users must wait between messages. + /// + public int? SlowMode { get; set; } + + /// + /// The message to initiate the forum post with. + /// + public DiscordMessageBuilder Message { get; set; } + + /// + /// The tags to apply to this post. + /// + public IReadOnlyList AppliedTags { get; } + + /// + /// When to automatically archive the post. + /// + public DiscordAutoArchiveDuration? AutoArchiveDuration { get; set; } + + /// + /// Creates a new forum post builder. + /// + public ForumPostBuilder() => this.AppliedTags = new List(); + + /// + /// Sets the name (or title) of the post. + /// + /// The name of the post. + /// The builder to chain calls with + public ForumPostBuilder WithName(string name) + { + this.Name = name; + return this; + } + + /// + /// Sets slowmode for the post. + /// + /// The time in seconds to apply + /// + public ForumPostBuilder WithSlowMode(int slowMode) + { + this.SlowMode = slowMode; + return this; + } + + /// + /// Sets slow mode for the post. + /// + /// The slowmode delay to set. + /// The builder to chain calls with. + public ForumPostBuilder WithSlowMode(TimeSpan slowMode) + { + this.SlowMode = (int)slowMode.TotalSeconds; + return this; + } + + /// + /// Sets the message to initiate the forum post with. + /// + /// The message to start the post with. + /// The builder to chain calls with. + public ForumPostBuilder WithMessage(DiscordMessageBuilder message) + { + this.Message = message; + return this; + } + + /// + /// Sets the auto archive duration for the post. + /// + /// The duration in which the post will automatically archive + /// The builder to chain calls with + public ForumPostBuilder WithAutoArchiveDuration(DiscordAutoArchiveDuration autoArchiveDuration) + { + this.AutoArchiveDuration = autoArchiveDuration; + return this; + } + + /// + /// Adds a tag to the post. + /// + /// The tag to add. + /// The builder to chain calls with. + public ForumPostBuilder AddTag(DiscordForumTag tag) + { + ((List)this.AppliedTags).Add(tag); + return this; + } + + /// + /// Adds several tags to the post. + /// + /// The tags to add. + /// The builder to chain calls with. + public ForumPostBuilder AddTags(IEnumerable tags) + { + ((List)this.AppliedTags).AddRange(tags); + return this; + } + + /// + /// Removes a tag from the post. + /// + /// + /// + public ForumPostBuilder RemoveTag(DiscordForumTag tag) + { + ((List)this.AppliedTags).Remove(tag); + return this; + } +} diff --git a/DSharpPlus/Entities/Channel/Thread/ThreadQueryResult.cs b/DSharpPlus/Entities/Channel/Thread/ThreadQueryResult.cs index 146920a726..7e9cca973c 100644 --- a/DSharpPlus/Entities/Channel/Thread/ThreadQueryResult.cs +++ b/DSharpPlus/Entities/Channel/Thread/ThreadQueryResult.cs @@ -1,23 +1,23 @@ -using System.Collections.Generic; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -public class ThreadQueryResult -{ - /// - /// Gets whether additional calls will yield more threads. - /// - [JsonProperty("has_more", NullValueHandling = NullValueHandling.Ignore)] - public bool HasMore { get; internal set; } - - /// - /// Gets the list of threads returned by the query. Generally ordered by in descending order. - /// - [JsonProperty("threads", NullValueHandling = NullValueHandling.Ignore)] - public IReadOnlyList Threads { get; internal set; } - - [JsonProperty("members", NullValueHandling = NullValueHandling.Ignore)] - internal IReadOnlyList Members { get; set; } - -} +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +public class ThreadQueryResult +{ + /// + /// Gets whether additional calls will yield more threads. + /// + [JsonProperty("has_more", NullValueHandling = NullValueHandling.Ignore)] + public bool HasMore { get; internal set; } + + /// + /// Gets the list of threads returned by the query. Generally ordered by in descending order. + /// + [JsonProperty("threads", NullValueHandling = NullValueHandling.Ignore)] + public IReadOnlyList Threads { get; internal set; } + + [JsonProperty("members", NullValueHandling = NullValueHandling.Ignore)] + internal IReadOnlyList Members { get; set; } + +} diff --git a/DSharpPlus/Entities/Color/DiscordColor.Colors.cs b/DSharpPlus/Entities/Color/DiscordColor.Colors.cs index f594e743a8..385958e9ae 100644 --- a/DSharpPlus/Entities/Color/DiscordColor.Colors.cs +++ b/DSharpPlus/Entities/Color/DiscordColor.Colors.cs @@ -1,252 +1,252 @@ -namespace DSharpPlus.Entities; - - -public readonly partial struct DiscordColor -{ - #region Black and White - /// - /// Represents no color, or integer 0; - /// - public static DiscordColor None { get; } = new DiscordColor(0); - - /// - /// A near-black color. Due to API limitations, the color is #010101, rather than #000000, as the latter is treated as no color. - /// - public static DiscordColor Black { get; } = new DiscordColor(0x010101); - - /// - /// White, or #FFFFFF. - /// - public static DiscordColor White { get; } = new DiscordColor(0xFFFFFF); - - /// - /// Gray, or #808080. - /// - public static DiscordColor Gray { get; } = new DiscordColor(0x808080); - - /// - /// Dark gray, or #A9A9A9. - /// - public static DiscordColor DarkGray { get; } = new DiscordColor(0xA9A9A9); - - /// - /// Light gray, or #808080. - /// - public static DiscordColor LightGray { get; } = new DiscordColor(0xD3D3D3); - - // dev-approved - /// - /// Very dark gray, or #666666. - /// - public static DiscordColor VeryDarkGray { get; } = new DiscordColor(0x666666); - #endregion - - #region Discord branding colors - // https://discord.com/branding - - /// - /// Discord Blurple, or #7289DA. - /// - public static DiscordColor Blurple { get; } = new DiscordColor(0x7289DA); - - /// - /// Discord Grayple, or #99AAB5. - /// - public static DiscordColor Grayple { get; } = new DiscordColor(0x99AAB5); - - /// - /// Discord Dark, But Not Black, or #2C2F33. - /// - public static DiscordColor DarkButNotBlack { get; } = new DiscordColor(0x2C2F33); - - /// - /// Discord Not QuiteBlack, or #23272A. - /// - public static DiscordColor NotQuiteBlack { get; } = new DiscordColor(0x23272A); - #endregion - - #region Other colors - /// - /// Red, or #FF0000. - /// - public static DiscordColor Red { get; } = new DiscordColor(0xFF0000); - - /// - /// Dark red, or #7F0000. - /// - public static DiscordColor DarkRed { get; } = new DiscordColor(0x7F0000); - - /// - /// Green, or #00FF00. - /// - public static DiscordColor Green { get; } = new DiscordColor(0x00FF00); - - /// - /// Dark green, or #007F00. - /// - public static DiscordColor DarkGreen { get; } = new DiscordColor(0x007F00); - - /// - /// Blue, or #0000FF. - /// - public static DiscordColor Blue { get; } = new DiscordColor(0x0000FF); - - /// - /// Dark blue, or #00007F. - /// - public static DiscordColor DarkBlue { get; } = new DiscordColor(0x00007F); - - /// - /// Yellow, or #FFFF00. - /// - public static DiscordColor Yellow { get; } = new DiscordColor(0xFFFF00); - - /// - /// Cyan, or #00FFFF. - /// - public static DiscordColor Cyan { get; } = new DiscordColor(0x00FFFF); - - /// - /// Magenta, or #FF00FF. - /// - public static DiscordColor Magenta { get; } = new DiscordColor(0xFF00FF); - - /// - /// Teal, or #008080. - /// - public static DiscordColor Teal { get; } = new DiscordColor(0x008080); - - // meme - /// - /// Aquamarine, or #00FFBF. - /// - public static DiscordColor Aquamarine { get; } = new DiscordColor(0x00FFBF); - - /// - /// Gold, or #FFD700. - /// - public static DiscordColor Gold { get; } = new DiscordColor(0xFFD700); - - // To be fair, you have to have a very high IQ to understand Goldenrod. - // The tones are extremely subtle, and without a solid grasp of artistic - // theory most of the beauty will go over a typical painter's head. - // There's also the flower's nihilistic style, which is deftly woven - // into its characterization - it's pollinated by the Bombus cryptarum - // bumblebee, for instance. The fans understand this stuff; they have - // the intellectual capacity to truly appreciate the depth of this - // flower, to realize that it's not just a color - it says something - // deep about LIFE. As a consequence people who dislike Goldenrod truly - // ARE idiots - of course they wouldn't appreciate, for instance, the - // beauty in the bumblebee species' complex presence in the British Isles, - // which is cryptically explained by Turgenev's Russian epic Fathers and - // Sons I'm blushing right now just imagining one of those addlepated - // simpletons scratching their heads in confusion as nature's genius - // unfolds itself on their computer screens. What fools... how I pity them. - // 😂 And yes by the way, I DO have a goldenrod tattoo. And no, you cannot - // see it. It's for the ladies' eyes only- And even they have to - // demonstrate that they're within 5 IQ points of my own (preferably lower) beforehand. - /// - /// Goldenrod, or #DAA520. - /// - public static DiscordColor Goldenrod { get; } = new DiscordColor(0xDAA520); - - // emzi's favourite - /// - /// Azure, or #007FFF. - /// - public static DiscordColor Azure { get; } = new DiscordColor(0x007FFF); - - /// - /// Rose, or #FF007F. - /// - public static DiscordColor Rose { get; } = new DiscordColor(0xFF007F); - - /// - /// Spring green, or #00FF7F. - /// - public static DiscordColor SpringGreen { get; } = new DiscordColor(0x00FF7F); - - /// - /// Chartreuse, or #7FFF00. - /// - public static DiscordColor Chartreuse { get; } = new DiscordColor(0x7FFF00); - - /// - /// Orange, or #FFA500. - /// - public static DiscordColor Orange { get; } = new DiscordColor(0xFFA500); - - /// - /// Purple, or #800080. - /// - public static DiscordColor Purple { get; } = new DiscordColor(0x800080); - - /// - /// Violet, or #EE82EE. - /// - public static DiscordColor Violet { get; } = new DiscordColor(0xEE82EE); - - /// - /// Brown, or #A52A2A. - /// - public static DiscordColor Brown { get; } = new DiscordColor(0xA52A2A); - - // meme - /// - /// Hot pink, or #FF69B4 - /// - public static DiscordColor HotPink { get; } = new DiscordColor(0xFF69B4); - - /// - /// Lilac, or #C8A2C8. - /// - public static DiscordColor Lilac { get; } = new DiscordColor(0xC8A2C8); - - /// - /// Cornflower blue, or #6495ED. - /// - public static DiscordColor CornflowerBlue { get; } = new DiscordColor(0x6495ED); - - /// - /// Midnight blue, or #191970. - /// - public static DiscordColor MidnightBlue { get; } = new DiscordColor(0x191970); - - /// - /// Wheat, or #F5DEB3. - /// - public static DiscordColor Wheat { get; } = new DiscordColor(0xF5DEB3); - - /// - /// Indian red, or #CD5C5C. - /// - public static DiscordColor IndianRed { get; } = new DiscordColor(0xCD5C5C); - - /// - /// Turquoise, or #30D5C8. - /// - public static DiscordColor Turquoise { get; } = new DiscordColor(0x30D5C8); - - /// - /// Sap green, or #507D2A. - /// - public static DiscordColor SapGreen { get; } = new DiscordColor(0x507D2A); - - // meme, specifically bob ross - /// - /// Phthalo blue, or #000F89. - /// - public static DiscordColor PhthaloBlue { get; } = new DiscordColor(0x000F89); - - // meme, specifically bob ross - /// - /// Phthalo green, or #123524. - /// - public static DiscordColor PhthaloGreen { get; } = new DiscordColor(0x123524); - - /// - /// Sienna, or #882D17. - /// - public static DiscordColor Sienna { get; } = new DiscordColor(0x882D17); - #endregion -} +namespace DSharpPlus.Entities; + + +public readonly partial struct DiscordColor +{ + #region Black and White + /// + /// Represents no color, or integer 0; + /// + public static DiscordColor None { get; } = new DiscordColor(0); + + /// + /// A near-black color. Due to API limitations, the color is #010101, rather than #000000, as the latter is treated as no color. + /// + public static DiscordColor Black { get; } = new DiscordColor(0x010101); + + /// + /// White, or #FFFFFF. + /// + public static DiscordColor White { get; } = new DiscordColor(0xFFFFFF); + + /// + /// Gray, or #808080. + /// + public static DiscordColor Gray { get; } = new DiscordColor(0x808080); + + /// + /// Dark gray, or #A9A9A9. + /// + public static DiscordColor DarkGray { get; } = new DiscordColor(0xA9A9A9); + + /// + /// Light gray, or #808080. + /// + public static DiscordColor LightGray { get; } = new DiscordColor(0xD3D3D3); + + // dev-approved + /// + /// Very dark gray, or #666666. + /// + public static DiscordColor VeryDarkGray { get; } = new DiscordColor(0x666666); + #endregion + + #region Discord branding colors + // https://discord.com/branding + + /// + /// Discord Blurple, or #7289DA. + /// + public static DiscordColor Blurple { get; } = new DiscordColor(0x7289DA); + + /// + /// Discord Grayple, or #99AAB5. + /// + public static DiscordColor Grayple { get; } = new DiscordColor(0x99AAB5); + + /// + /// Discord Dark, But Not Black, or #2C2F33. + /// + public static DiscordColor DarkButNotBlack { get; } = new DiscordColor(0x2C2F33); + + /// + /// Discord Not QuiteBlack, or #23272A. + /// + public static DiscordColor NotQuiteBlack { get; } = new DiscordColor(0x23272A); + #endregion + + #region Other colors + /// + /// Red, or #FF0000. + /// + public static DiscordColor Red { get; } = new DiscordColor(0xFF0000); + + /// + /// Dark red, or #7F0000. + /// + public static DiscordColor DarkRed { get; } = new DiscordColor(0x7F0000); + + /// + /// Green, or #00FF00. + /// + public static DiscordColor Green { get; } = new DiscordColor(0x00FF00); + + /// + /// Dark green, or #007F00. + /// + public static DiscordColor DarkGreen { get; } = new DiscordColor(0x007F00); + + /// + /// Blue, or #0000FF. + /// + public static DiscordColor Blue { get; } = new DiscordColor(0x0000FF); + + /// + /// Dark blue, or #00007F. + /// + public static DiscordColor DarkBlue { get; } = new DiscordColor(0x00007F); + + /// + /// Yellow, or #FFFF00. + /// + public static DiscordColor Yellow { get; } = new DiscordColor(0xFFFF00); + + /// + /// Cyan, or #00FFFF. + /// + public static DiscordColor Cyan { get; } = new DiscordColor(0x00FFFF); + + /// + /// Magenta, or #FF00FF. + /// + public static DiscordColor Magenta { get; } = new DiscordColor(0xFF00FF); + + /// + /// Teal, or #008080. + /// + public static DiscordColor Teal { get; } = new DiscordColor(0x008080); + + // meme + /// + /// Aquamarine, or #00FFBF. + /// + public static DiscordColor Aquamarine { get; } = new DiscordColor(0x00FFBF); + + /// + /// Gold, or #FFD700. + /// + public static DiscordColor Gold { get; } = new DiscordColor(0xFFD700); + + // To be fair, you have to have a very high IQ to understand Goldenrod. + // The tones are extremely subtle, and without a solid grasp of artistic + // theory most of the beauty will go over a typical painter's head. + // There's also the flower's nihilistic style, which is deftly woven + // into its characterization - it's pollinated by the Bombus cryptarum + // bumblebee, for instance. The fans understand this stuff; they have + // the intellectual capacity to truly appreciate the depth of this + // flower, to realize that it's not just a color - it says something + // deep about LIFE. As a consequence people who dislike Goldenrod truly + // ARE idiots - of course they wouldn't appreciate, for instance, the + // beauty in the bumblebee species' complex presence in the British Isles, + // which is cryptically explained by Turgenev's Russian epic Fathers and + // Sons I'm blushing right now just imagining one of those addlepated + // simpletons scratching their heads in confusion as nature's genius + // unfolds itself on their computer screens. What fools... how I pity them. + // 😂 And yes by the way, I DO have a goldenrod tattoo. And no, you cannot + // see it. It's for the ladies' eyes only- And even they have to + // demonstrate that they're within 5 IQ points of my own (preferably lower) beforehand. + /// + /// Goldenrod, or #DAA520. + /// + public static DiscordColor Goldenrod { get; } = new DiscordColor(0xDAA520); + + // emzi's favourite + /// + /// Azure, or #007FFF. + /// + public static DiscordColor Azure { get; } = new DiscordColor(0x007FFF); + + /// + /// Rose, or #FF007F. + /// + public static DiscordColor Rose { get; } = new DiscordColor(0xFF007F); + + /// + /// Spring green, or #00FF7F. + /// + public static DiscordColor SpringGreen { get; } = new DiscordColor(0x00FF7F); + + /// + /// Chartreuse, or #7FFF00. + /// + public static DiscordColor Chartreuse { get; } = new DiscordColor(0x7FFF00); + + /// + /// Orange, or #FFA500. + /// + public static DiscordColor Orange { get; } = new DiscordColor(0xFFA500); + + /// + /// Purple, or #800080. + /// + public static DiscordColor Purple { get; } = new DiscordColor(0x800080); + + /// + /// Violet, or #EE82EE. + /// + public static DiscordColor Violet { get; } = new DiscordColor(0xEE82EE); + + /// + /// Brown, or #A52A2A. + /// + public static DiscordColor Brown { get; } = new DiscordColor(0xA52A2A); + + // meme + /// + /// Hot pink, or #FF69B4 + /// + public static DiscordColor HotPink { get; } = new DiscordColor(0xFF69B4); + + /// + /// Lilac, or #C8A2C8. + /// + public static DiscordColor Lilac { get; } = new DiscordColor(0xC8A2C8); + + /// + /// Cornflower blue, or #6495ED. + /// + public static DiscordColor CornflowerBlue { get; } = new DiscordColor(0x6495ED); + + /// + /// Midnight blue, or #191970. + /// + public static DiscordColor MidnightBlue { get; } = new DiscordColor(0x191970); + + /// + /// Wheat, or #F5DEB3. + /// + public static DiscordColor Wheat { get; } = new DiscordColor(0xF5DEB3); + + /// + /// Indian red, or #CD5C5C. + /// + public static DiscordColor IndianRed { get; } = new DiscordColor(0xCD5C5C); + + /// + /// Turquoise, or #30D5C8. + /// + public static DiscordColor Turquoise { get; } = new DiscordColor(0x30D5C8); + + /// + /// Sap green, or #507D2A. + /// + public static DiscordColor SapGreen { get; } = new DiscordColor(0x507D2A); + + // meme, specifically bob ross + /// + /// Phthalo blue, or #000F89. + /// + public static DiscordColor PhthaloBlue { get; } = new DiscordColor(0x000F89); + + // meme, specifically bob ross + /// + /// Phthalo green, or #123524. + /// + public static DiscordColor PhthaloGreen { get; } = new DiscordColor(0x123524); + + /// + /// Sienna, or #882D17. + /// + public static DiscordColor Sienna { get; } = new DiscordColor(0x882D17); + #endregion +} diff --git a/DSharpPlus/Entities/Color/DiscordColor.cs b/DSharpPlus/Entities/Color/DiscordColor.cs index 9af02e71c9..8488af6f53 100644 --- a/DSharpPlus/Entities/Color/DiscordColor.cs +++ b/DSharpPlus/Entities/Color/DiscordColor.cs @@ -1,121 +1,121 @@ -using System; -using System.Globalization; -using System.Linq; - -namespace DSharpPlus.Entities; - -/// -/// Represents a color used in Discord API. -/// -public partial struct DiscordColor -{ - private static readonly char[] hexAlphabet = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F']; - - /// - /// Gets the integer representation of this color. - /// - public int Value { get; } - - /// - /// Gets the red component of this color as an 8-bit integer. - /// - public readonly byte R - => (byte)((this.Value >> 16) & 0xFF); - - /// - /// Gets the green component of this color as an 8-bit integer. - /// - public readonly byte G - => (byte)((this.Value >> 8) & 0xFF); - - /// - /// Gets the blue component of this color as an 8-bit integer. - /// - public readonly byte B - => (byte)(this.Value & 0xFF); - - /// - /// Creates a new color with specified value. - /// - /// Value of the color. - public DiscordColor(int color) => this.Value = color; - - /// - /// Creates a new color with specified values for red, green, and blue components. - /// - /// Value of the red component. - /// Value of the green component. - /// Value of the blue component. - public DiscordColor(byte r, byte g, byte b) => this.Value = (r << 16) | (g << 8) | b; - - /// - /// Creates a new color with specified values for red, green, and blue components. - /// - /// Value of the red component. - /// Value of the green component. - /// Value of the blue component. - public DiscordColor(float r, float g, float b) - { - if (r is < 0 or > 1) - { - throw new ArgumentOutOfRangeException(nameof(r), "Value must be between 0 and 1."); - } - else if (g is < 0 or > 1) - { - throw new ArgumentOutOfRangeException(nameof(g), "Value must be between 0 and 1."); - } - else if (b is < 0 or > 1) - { - throw new ArgumentOutOfRangeException(nameof(b), "Value must be between 0 and 1."); - } - - byte rb = (byte)(r * 255); - byte gb = (byte)(g * 255); - byte bb = (byte)(b * 255); - - this.Value = (rb << 16) | (gb << 8) | bb; - } - - /// - /// Creates a new color from specified string representation. - /// - /// String representation of the color. Must be 6 hexadecimal characters, optionally with # prefix. - public DiscordColor(string color) - { - if (string.IsNullOrWhiteSpace(color)) - { - throw new ArgumentNullException(nameof(color), "Null or empty values are not allowed!"); - } - - if (color.Length is not 6 and not 7) - { - throw new ArgumentException("Color must be 6 or 7 characters in length.", nameof(color)); - } - - color = color.ToUpper(); - if (color.Length == 7 && color[0] != '#') - { - throw new ArgumentException("7-character colors must begin with #.", nameof(color)); - } - else if (color.Length == 7) - { - color = color[1..]; - } - - if (color.Any(xc => !hexAlphabet.Contains(xc))) - { - throw new ArgumentException("Colors must consist of hexadecimal characters only.", nameof(color)); - } - - this.Value = int.Parse(color, NumberStyles.HexNumber, CultureInfo.InvariantCulture); - } - - /// - /// Gets a string representation of this color. - /// - /// String representation of this color. - public override readonly string ToString() => $"#{this.Value:X6}"; - - public static implicit operator DiscordColor(int value) - => new(value); -} +using System; +using System.Globalization; +using System.Linq; + +namespace DSharpPlus.Entities; + +/// +/// Represents a color used in Discord API. +/// +public partial struct DiscordColor +{ + private static readonly char[] hexAlphabet = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F']; + + /// + /// Gets the integer representation of this color. + /// + public int Value { get; } + + /// + /// Gets the red component of this color as an 8-bit integer. + /// + public readonly byte R + => (byte)((this.Value >> 16) & 0xFF); + + /// + /// Gets the green component of this color as an 8-bit integer. + /// + public readonly byte G + => (byte)((this.Value >> 8) & 0xFF); + + /// + /// Gets the blue component of this color as an 8-bit integer. + /// + public readonly byte B + => (byte)(this.Value & 0xFF); + + /// + /// Creates a new color with specified value. + /// + /// Value of the color. + public DiscordColor(int color) => this.Value = color; + + /// + /// Creates a new color with specified values for red, green, and blue components. + /// + /// Value of the red component. + /// Value of the green component. + /// Value of the blue component. + public DiscordColor(byte r, byte g, byte b) => this.Value = (r << 16) | (g << 8) | b; + + /// + /// Creates a new color with specified values for red, green, and blue components. + /// + /// Value of the red component. + /// Value of the green component. + /// Value of the blue component. + public DiscordColor(float r, float g, float b) + { + if (r is < 0 or > 1) + { + throw new ArgumentOutOfRangeException(nameof(r), "Value must be between 0 and 1."); + } + else if (g is < 0 or > 1) + { + throw new ArgumentOutOfRangeException(nameof(g), "Value must be between 0 and 1."); + } + else if (b is < 0 or > 1) + { + throw new ArgumentOutOfRangeException(nameof(b), "Value must be between 0 and 1."); + } + + byte rb = (byte)(r * 255); + byte gb = (byte)(g * 255); + byte bb = (byte)(b * 255); + + this.Value = (rb << 16) | (gb << 8) | bb; + } + + /// + /// Creates a new color from specified string representation. + /// + /// String representation of the color. Must be 6 hexadecimal characters, optionally with # prefix. + public DiscordColor(string color) + { + if (string.IsNullOrWhiteSpace(color)) + { + throw new ArgumentNullException(nameof(color), "Null or empty values are not allowed!"); + } + + if (color.Length is not 6 and not 7) + { + throw new ArgumentException("Color must be 6 or 7 characters in length.", nameof(color)); + } + + color = color.ToUpper(); + if (color.Length == 7 && color[0] != '#') + { + throw new ArgumentException("7-character colors must begin with #.", nameof(color)); + } + else if (color.Length == 7) + { + color = color[1..]; + } + + if (color.Any(xc => !hexAlphabet.Contains(xc))) + { + throw new ArgumentException("Colors must consist of hexadecimal characters only.", nameof(color)); + } + + this.Value = int.Parse(color, NumberStyles.HexNumber, CultureInfo.InvariantCulture); + } + + /// + /// Gets a string representation of this color. + /// + /// String representation of this color. + public override readonly string ToString() => $"#{this.Value:X6}"; + + public static implicit operator DiscordColor(int value) + => new(value); +} diff --git a/DSharpPlus/Entities/DiscordConnection.cs b/DSharpPlus/Entities/DiscordConnection.cs index c71b947e1a..64337789b7 100644 --- a/DSharpPlus/Entities/DiscordConnection.cs +++ b/DSharpPlus/Entities/DiscordConnection.cs @@ -1,72 +1,72 @@ -using System.Collections.Generic; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Gets a Discord connection to a 3rd party service. -/// -public class DiscordConnection -{ - /// - /// Gets the id of the connection account - /// - [JsonProperty("id")] - public string Id { get; internal set; } - - /// - /// Gets the username of the connection account. - /// - [JsonProperty("name")] - public string Name { get; set; } - - /// - /// Gets the service of the connection (twitch, youtube, steam, twitter, facebook, spotify, leagueoflegends, reddit) - /// - [JsonProperty("type")] - public string Type { get; set; } - - /// - /// Gets whether the connection is revoked. - /// - [JsonProperty("revoked")] - public bool IsRevoked { get; internal set; } - - /// - /// Gets a collection of partial server integrations. - /// - [JsonProperty("integrations", NullValueHandling = NullValueHandling.Ignore)] - public IReadOnlyList Integrations { get; internal set; } - - /// - /// Gets the connection is verified or not. - /// - [JsonProperty("verified", NullValueHandling = NullValueHandling.Ignore)] - public bool? Verified { get; set; } - - /// - /// Gets the connection will show activity or not. - /// - [JsonProperty("show_activity", NullValueHandling = NullValueHandling.Ignore)] - public bool? ShowActivity { get; set; } - - /// - /// Gets the connection will sync friends or not. - /// - [JsonProperty("friend_sync", NullValueHandling = NullValueHandling.Ignore)] - public bool? FriendSync { get; set; } - - /// - /// Gets the visibility of the connection. - /// - [JsonProperty("visibility", NullValueHandling = NullValueHandling.Ignore)] - public long? Visibility { get; set; } - - /// - /// Gets the client instance this object is tied to. - /// - [JsonIgnore] - internal BaseDiscordClient Discord { get; set; } - - internal DiscordConnection() { } -} +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Gets a Discord connection to a 3rd party service. +/// +public class DiscordConnection +{ + /// + /// Gets the id of the connection account + /// + [JsonProperty("id")] + public string Id { get; internal set; } + + /// + /// Gets the username of the connection account. + /// + [JsonProperty("name")] + public string Name { get; set; } + + /// + /// Gets the service of the connection (twitch, youtube, steam, twitter, facebook, spotify, leagueoflegends, reddit) + /// + [JsonProperty("type")] + public string Type { get; set; } + + /// + /// Gets whether the connection is revoked. + /// + [JsonProperty("revoked")] + public bool IsRevoked { get; internal set; } + + /// + /// Gets a collection of partial server integrations. + /// + [JsonProperty("integrations", NullValueHandling = NullValueHandling.Ignore)] + public IReadOnlyList Integrations { get; internal set; } + + /// + /// Gets the connection is verified or not. + /// + [JsonProperty("verified", NullValueHandling = NullValueHandling.Ignore)] + public bool? Verified { get; set; } + + /// + /// Gets the connection will show activity or not. + /// + [JsonProperty("show_activity", NullValueHandling = NullValueHandling.Ignore)] + public bool? ShowActivity { get; set; } + + /// + /// Gets the connection will sync friends or not. + /// + [JsonProperty("friend_sync", NullValueHandling = NullValueHandling.Ignore)] + public bool? FriendSync { get; set; } + + /// + /// Gets the visibility of the connection. + /// + [JsonProperty("visibility", NullValueHandling = NullValueHandling.Ignore)] + public long? Visibility { get; set; } + + /// + /// Gets the client instance this object is tied to. + /// + [JsonIgnore] + internal BaseDiscordClient Discord { get; set; } + + internal DiscordConnection() { } +} diff --git a/DSharpPlus/Entities/DiscordPermissions.cs b/DSharpPlus/Entities/DiscordPermissions.cs index 3a845a90f4..58dd03e089 100644 --- a/DSharpPlus/Entities/DiscordPermissions.cs +++ b/DSharpPlus/Entities/DiscordPermissions.cs @@ -1,356 +1,356 @@ -#pragma warning disable IDE0040 - -using System; -using System.Buffers.Binary; -using System.Collections.Generic; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using System.Numerics; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Runtime.Intrinsics; -using System.Text; - -using CommunityToolkit.HighPerformance.Helpers; - -using DSharpPlus.Net.Serialization; - -using Newtonsoft.Json; - -using HashCode = CommunityToolkit.HighPerformance.Helpers.HashCode; - -namespace DSharpPlus.Entities; - -/// -/// Represents a set of Discord permissions. -/// -[JsonConverter(typeof(DiscordPermissionsAsStringJsonConverter))] -public readonly partial struct DiscordPermissions - : IEquatable -{ - // only change ContainerWidth here, the other two constants are automatically updated for internal uses - // for ContainerWidth, 1 width == 128 bits. - private const int ContainerWidth = 1; - private const int ContainerElementCount = ContainerWidth * 4; - private const int ContainerByteCount = ContainerWidth * 16; - - private static readonly string[] permissionNames = CreatePermissionNameArray(); - private static readonly int highestDefinedValue = (int)DiscordPermissionExtensions.GetValues()[^1]; - - private readonly DiscordPermissionContainer data; - - /// - /// Creates a new instance of this type from exactly the specified permission. - /// - public DiscordPermissions(DiscordPermission permission) - => this.data.SetFlag((int)permission, true); - - /// - /// Creates a new instance of this type from the specified permissions. - /// - [OverloadResolutionPriority(1)] - public DiscordPermissions(params ReadOnlySpan permissions) - { - foreach (DiscordPermission permission in permissions) - { - this.data.SetFlag((int)permission, true); - } - } - - /// - /// Creates a new instance of this type from the specified permissions. - /// - [OverloadResolutionPriority(0)] - public DiscordPermissions(params IReadOnlyList permissions) - { - foreach (DiscordPermission permission in permissions) - { - this.data.SetFlag((int)permission, true); - } - } - - /// - /// Creates a new instance of this type from the specified big integer. This assumes that the data is unsigned. - /// - public DiscordPermissions(BigInteger permissionSet) - { - Span buffer = MemoryMarshal.Cast(MemoryMarshal.CreateSpan(ref this.data[0], ContainerElementCount)); - - if (!permissionSet.TryWriteBytes(buffer, out _, isUnsigned: true)) - { - // we don't want to fail in release mode, which would break perfectly working code because the library - // hasn't been updated to support a new permission or because Discord is testing in prod again. - // seeing this assertion in dev should be an indication to expand this type. - Debug.Assert(false, "The amount of permissions DSharpPlus can represent has been exceeded."); - } - } - - /// - /// Creates a new instance of this type from the specified raw data. This assumes that the data is unsigned. - /// - public DiscordPermissions(scoped ReadOnlySpan raw) - { - Span buffer = MemoryMarshal.Cast(MemoryMarshal.CreateSpan(ref this.data[0], ContainerElementCount * 4)); - - if (!raw.TryCopyTo(buffer)) - { - // we don't want to fail in release mode, which would break perfectly working code because the library - // hasn't been updated to support a new permission or because Discord is testing in prod again. - // seeing this assertion in dev should be an indication to expand this type. - Debug.Assert(false, "The amount of permissions DSharpPlus can represent has been exceeded."); - } - } - - /// - /// A copy constructor that sets an arbitrary amount of flags to their respective values. - /// - private DiscordPermissions - ( - scoped ReadOnlySpan raw, - ReadOnlySpan setPermissions, - ReadOnlySpan removePermissions - ) - : this(raw) - { - foreach (DiscordPermission permission in setPermissions) - { - this.data.SetFlag((int)permission, true); - } - - foreach (DiscordPermission permission in removePermissions) - { - this.data.SetFlag((int)permission, false); - } - } - - /// - /// A copy constructor that sets one specific flag to the specified value. - /// - private DiscordPermissions(DiscordPermissions original, int index, bool flag) - { - this.data = original.data; - this.data.SetFlag(index, flag); - } - - public static implicit operator DiscordPermissions(DiscordPermission initial) => new(initial); - - /// - /// Returns an empty Discord permission set. - /// - public static DiscordPermissions None => default; - - /// - /// Returns a full Discord permission set with all flags set to true. - /// - public static DiscordPermissions AllBitsSet - { - get - { - Span result = stackalloc byte[ContainerByteCount]; - - for (int i = 0; i < ContainerElementCount; i += 16) - { - Vector128.StoreUnsafe(Vector128.AllBitsSet, ref result[i]); - } - - return new(result); - } - } - - /// - /// Returns a Discord permission set with all documented permissions set to true. - /// - public static DiscordPermissions All { get; } = new(DiscordPermissionExtensions.GetValues()); - - [UnscopedRef] - private ReadOnlySpan AsSpan - => MemoryMarshal.Cast((ReadOnlySpan)this.data); - - private bool GetFlag(int index) - => this.data.HasFlag(index); - - /// - /// Determines whether this Discord permission set is equal to the provided object. - /// - public override bool Equals([NotNullWhen(true)] object? obj) - => obj is DiscordPermissions permissions && Equals(permissions); - - /// - /// Determines whether this Discord permission set is equal to the provided Discord permission set. - /// - public bool Equals(DiscordPermissions other) - => ((ReadOnlySpan)this.data).SequenceEqual(other.data); - - /// - /// Returns a string representation of this permission set. - /// - public override string ToString() => ToString("a placeholder format string that doesn't do anything"); - - /// - /// Returns a string representation of this permission set, according to the provided format string. - /// - /// - /// Specifies the format in which the string should be created. Currently supported formats are:
- /// - raw: This prints the raw, byte-wise backing data of this instance.
- /// - name: This prints each flag by name, separated by commas.
- /// - anything else will print the integer value contained in this instance. - /// - public string ToString(string format) - { - if (format == "raw") - { - StringBuilder builder = new("DiscordPermissions - raw value:"); - - foreach (byte b in this.AsSpan) - { - _ = builder.Append(' '); - _ = builder.Append(b.ToString("x2", CultureInfo.InvariantCulture)); - } - - return builder.ToString(); - } - else if (format == "name") - { - int pop = 0; - - for (int i = 0; i < ContainerElementCount; i += 4) - { - pop += BitOperations.PopCount(this.data[i]); - pop += BitOperations.PopCount(this.data[i + 1]); - pop += BitOperations.PopCount(this.data[i + 2]); - pop += BitOperations.PopCount(this.data[i + 3]); - } - - if (pop == 0) - { - return "None"; - } - - Span names = new string[pop]; - DiscordPermissionEnumerator enumerator = new(this.data); - - for (int i = 0; i < pop; i++) - { - _ = enumerator.MoveNext(); - int flag = (int)enumerator.Current; - names[i] = flag <= highestDefinedValue ? permissionNames[flag] : flag.ToString(CultureInfo.InvariantCulture); - } - - return string.Join(", ", names); - } - else if (format.StartsWith("name:")) - { - string trimmedFormat = format[5..]; - - if (string.IsNullOrWhiteSpace(trimmedFormat) || !trimmedFormat.Contains("{permission}")) - { - ThrowFormatException(format); - } - - StringBuilder builder = new(); - - foreach (DiscordPermission permission in EnumeratePermissions()) - { - int flag = (int)permission; - string permissionName = flag <= highestDefinedValue ? permissionNames[flag] : flag.ToString(CultureInfo.InvariantCulture); - - _ = builder.Append(trimmedFormat.Replace("{permission}", permissionName)); - } - - return builder.ToString(); - } - else - { - Span buffer = stackalloc byte[ContainerElementCount * 4]; - this.AsSpan.CopyTo(buffer); - - if (!BitConverter.IsLittleEndian) - { - Span bigEndianWorkaround = MemoryMarshal.Cast(buffer); - BinaryPrimitives.ReverseEndianness(bigEndianWorkaround, bigEndianWorkaround); - } - - return new BigInteger(buffer, true, false).ToString(CultureInfo.InvariantCulture); - } - } - - /// - /// Calculates a hash code for this Discord permission set. The hash code is only guaranteed to be consistent - /// within a process, and sharing this data across process boundaries is dangerous. - /// - public override int GetHashCode() - => HashCode.Combine(this.data); - - /// - /// Provides an enumeration of all permissions specified by this set. - /// - public DiscordPermissionEnumerable EnumeratePermissions() - => new(this.data); - - public static bool operator ==(DiscordPermissions left, DiscordPermissions right) => left.Equals(right); - public static bool operator !=(DiscordPermissions left, DiscordPermissions right) => !(left == right); - - private static string[] CreatePermissionNameArray() - { - int highest = (int)DiscordPermissionExtensions.GetValues()[^1]; - string[] names = new string[highest + 1]; - - for (int i = 0; i <= highest; i++) - { - names[i] = ((DiscordPermission)i).ToStringFast(true); - } - - return names; - } - - [DoesNotReturn] - [DebuggerHidden] - [StackTraceHidden] - private static void ThrowFormatException(string format) - => throw new FormatException($"The format string \"{format}\" was empty or malformed: it must contain an instruction to print a permission."); - - // we will be using an inline array from the start here so that further increases in the bit width - // only require increasing this number instead of switching to a new backing implementation strategy. - // if Discord changes the way permissions are represented in the API, this will obviously have to change. - // - // this should always be backed by a 32-bit integer, to make our life easier around popcnt and BitHelper. - // - /// - /// Represents a container for the backing storage of Discord permissions. - /// - [InlineArray(ContainerElementCount)] - internal struct DiscordPermissionContainer - { - public uint value; - - /// - /// Sets a specified flag to the specific value. This function fails in debug mode if the flag was out of range. - /// - public void SetFlag(int index, bool value) - { - int fieldIndex = index >> 5; - - Debug.Assert(fieldIndex < ContainerElementCount); - - int bitIndex = index & 0x1F; - ref uint segment = ref this[fieldIndex]; - BitHelper.SetFlag(ref segment, bitIndex, value); - } - - /// - /// Returns the value of a specified flag. This function fails in debug mode if the flag was out of range. - /// - public readonly bool HasFlag(int index) - { - int fieldIndex = index >> 5; - - Debug.Assert(fieldIndex < ContainerElementCount); - - int bitIndex = index & 0x1F; - uint segment = this[fieldIndex]; - return BitHelper.HasFlag(segment, bitIndex); - } - } -} - +#pragma warning disable IDE0040 + +using System; +using System.Buffers.Binary; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using System.Text; + +using CommunityToolkit.HighPerformance.Helpers; + +using DSharpPlus.Net.Serialization; + +using Newtonsoft.Json; + +using HashCode = CommunityToolkit.HighPerformance.Helpers.HashCode; + +namespace DSharpPlus.Entities; + +/// +/// Represents a set of Discord permissions. +/// +[JsonConverter(typeof(DiscordPermissionsAsStringJsonConverter))] +public readonly partial struct DiscordPermissions + : IEquatable +{ + // only change ContainerWidth here, the other two constants are automatically updated for internal uses + // for ContainerWidth, 1 width == 128 bits. + private const int ContainerWidth = 1; + private const int ContainerElementCount = ContainerWidth * 4; + private const int ContainerByteCount = ContainerWidth * 16; + + private static readonly string[] permissionNames = CreatePermissionNameArray(); + private static readonly int highestDefinedValue = (int)DiscordPermissionExtensions.GetValues()[^1]; + + private readonly DiscordPermissionContainer data; + + /// + /// Creates a new instance of this type from exactly the specified permission. + /// + public DiscordPermissions(DiscordPermission permission) + => this.data.SetFlag((int)permission, true); + + /// + /// Creates a new instance of this type from the specified permissions. + /// + [OverloadResolutionPriority(1)] + public DiscordPermissions(params ReadOnlySpan permissions) + { + foreach (DiscordPermission permission in permissions) + { + this.data.SetFlag((int)permission, true); + } + } + + /// + /// Creates a new instance of this type from the specified permissions. + /// + [OverloadResolutionPriority(0)] + public DiscordPermissions(params IReadOnlyList permissions) + { + foreach (DiscordPermission permission in permissions) + { + this.data.SetFlag((int)permission, true); + } + } + + /// + /// Creates a new instance of this type from the specified big integer. This assumes that the data is unsigned. + /// + public DiscordPermissions(BigInteger permissionSet) + { + Span buffer = MemoryMarshal.Cast(MemoryMarshal.CreateSpan(ref this.data[0], ContainerElementCount)); + + if (!permissionSet.TryWriteBytes(buffer, out _, isUnsigned: true)) + { + // we don't want to fail in release mode, which would break perfectly working code because the library + // hasn't been updated to support a new permission or because Discord is testing in prod again. + // seeing this assertion in dev should be an indication to expand this type. + Debug.Assert(false, "The amount of permissions DSharpPlus can represent has been exceeded."); + } + } + + /// + /// Creates a new instance of this type from the specified raw data. This assumes that the data is unsigned. + /// + public DiscordPermissions(scoped ReadOnlySpan raw) + { + Span buffer = MemoryMarshal.Cast(MemoryMarshal.CreateSpan(ref this.data[0], ContainerElementCount * 4)); + + if (!raw.TryCopyTo(buffer)) + { + // we don't want to fail in release mode, which would break perfectly working code because the library + // hasn't been updated to support a new permission or because Discord is testing in prod again. + // seeing this assertion in dev should be an indication to expand this type. + Debug.Assert(false, "The amount of permissions DSharpPlus can represent has been exceeded."); + } + } + + /// + /// A copy constructor that sets an arbitrary amount of flags to their respective values. + /// + private DiscordPermissions + ( + scoped ReadOnlySpan raw, + ReadOnlySpan setPermissions, + ReadOnlySpan removePermissions + ) + : this(raw) + { + foreach (DiscordPermission permission in setPermissions) + { + this.data.SetFlag((int)permission, true); + } + + foreach (DiscordPermission permission in removePermissions) + { + this.data.SetFlag((int)permission, false); + } + } + + /// + /// A copy constructor that sets one specific flag to the specified value. + /// + private DiscordPermissions(DiscordPermissions original, int index, bool flag) + { + this.data = original.data; + this.data.SetFlag(index, flag); + } + + public static implicit operator DiscordPermissions(DiscordPermission initial) => new(initial); + + /// + /// Returns an empty Discord permission set. + /// + public static DiscordPermissions None => default; + + /// + /// Returns a full Discord permission set with all flags set to true. + /// + public static DiscordPermissions AllBitsSet + { + get + { + Span result = stackalloc byte[ContainerByteCount]; + + for (int i = 0; i < ContainerElementCount; i += 16) + { + Vector128.StoreUnsafe(Vector128.AllBitsSet, ref result[i]); + } + + return new(result); + } + } + + /// + /// Returns a Discord permission set with all documented permissions set to true. + /// + public static DiscordPermissions All { get; } = new(DiscordPermissionExtensions.GetValues()); + + [UnscopedRef] + private ReadOnlySpan AsSpan + => MemoryMarshal.Cast((ReadOnlySpan)this.data); + + private bool GetFlag(int index) + => this.data.HasFlag(index); + + /// + /// Determines whether this Discord permission set is equal to the provided object. + /// + public override bool Equals([NotNullWhen(true)] object? obj) + => obj is DiscordPermissions permissions && Equals(permissions); + + /// + /// Determines whether this Discord permission set is equal to the provided Discord permission set. + /// + public bool Equals(DiscordPermissions other) + => ((ReadOnlySpan)this.data).SequenceEqual(other.data); + + /// + /// Returns a string representation of this permission set. + /// + public override string ToString() => ToString("a placeholder format string that doesn't do anything"); + + /// + /// Returns a string representation of this permission set, according to the provided format string. + /// + /// + /// Specifies the format in which the string should be created. Currently supported formats are:
+ /// - raw: This prints the raw, byte-wise backing data of this instance.
+ /// - name: This prints each flag by name, separated by commas.
+ /// - anything else will print the integer value contained in this instance. + /// + public string ToString(string format) + { + if (format == "raw") + { + StringBuilder builder = new("DiscordPermissions - raw value:"); + + foreach (byte b in this.AsSpan) + { + _ = builder.Append(' '); + _ = builder.Append(b.ToString("x2", CultureInfo.InvariantCulture)); + } + + return builder.ToString(); + } + else if (format == "name") + { + int pop = 0; + + for (int i = 0; i < ContainerElementCount; i += 4) + { + pop += BitOperations.PopCount(this.data[i]); + pop += BitOperations.PopCount(this.data[i + 1]); + pop += BitOperations.PopCount(this.data[i + 2]); + pop += BitOperations.PopCount(this.data[i + 3]); + } + + if (pop == 0) + { + return "None"; + } + + Span names = new string[pop]; + DiscordPermissionEnumerator enumerator = new(this.data); + + for (int i = 0; i < pop; i++) + { + _ = enumerator.MoveNext(); + int flag = (int)enumerator.Current; + names[i] = flag <= highestDefinedValue ? permissionNames[flag] : flag.ToString(CultureInfo.InvariantCulture); + } + + return string.Join(", ", names); + } + else if (format.StartsWith("name:")) + { + string trimmedFormat = format[5..]; + + if (string.IsNullOrWhiteSpace(trimmedFormat) || !trimmedFormat.Contains("{permission}")) + { + ThrowFormatException(format); + } + + StringBuilder builder = new(); + + foreach (DiscordPermission permission in EnumeratePermissions()) + { + int flag = (int)permission; + string permissionName = flag <= highestDefinedValue ? permissionNames[flag] : flag.ToString(CultureInfo.InvariantCulture); + + _ = builder.Append(trimmedFormat.Replace("{permission}", permissionName)); + } + + return builder.ToString(); + } + else + { + Span buffer = stackalloc byte[ContainerElementCount * 4]; + this.AsSpan.CopyTo(buffer); + + if (!BitConverter.IsLittleEndian) + { + Span bigEndianWorkaround = MemoryMarshal.Cast(buffer); + BinaryPrimitives.ReverseEndianness(bigEndianWorkaround, bigEndianWorkaround); + } + + return new BigInteger(buffer, true, false).ToString(CultureInfo.InvariantCulture); + } + } + + /// + /// Calculates a hash code for this Discord permission set. The hash code is only guaranteed to be consistent + /// within a process, and sharing this data across process boundaries is dangerous. + /// + public override int GetHashCode() + => HashCode.Combine(this.data); + + /// + /// Provides an enumeration of all permissions specified by this set. + /// + public DiscordPermissionEnumerable EnumeratePermissions() + => new(this.data); + + public static bool operator ==(DiscordPermissions left, DiscordPermissions right) => left.Equals(right); + public static bool operator !=(DiscordPermissions left, DiscordPermissions right) => !(left == right); + + private static string[] CreatePermissionNameArray() + { + int highest = (int)DiscordPermissionExtensions.GetValues()[^1]; + string[] names = new string[highest + 1]; + + for (int i = 0; i <= highest; i++) + { + names[i] = ((DiscordPermission)i).ToStringFast(true); + } + + return names; + } + + [DoesNotReturn] + [DebuggerHidden] + [StackTraceHidden] + private static void ThrowFormatException(string format) + => throw new FormatException($"The format string \"{format}\" was empty or malformed: it must contain an instruction to print a permission."); + + // we will be using an inline array from the start here so that further increases in the bit width + // only require increasing this number instead of switching to a new backing implementation strategy. + // if Discord changes the way permissions are represented in the API, this will obviously have to change. + // + // this should always be backed by a 32-bit integer, to make our life easier around popcnt and BitHelper. + // + /// + /// Represents a container for the backing storage of Discord permissions. + /// + [InlineArray(ContainerElementCount)] + internal struct DiscordPermissionContainer + { + public uint value; + + /// + /// Sets a specified flag to the specific value. This function fails in debug mode if the flag was out of range. + /// + public void SetFlag(int index, bool value) + { + int fieldIndex = index >> 5; + + Debug.Assert(fieldIndex < ContainerElementCount); + + int bitIndex = index & 0x1F; + ref uint segment = ref this[fieldIndex]; + BitHelper.SetFlag(ref segment, bitIndex, value); + } + + /// + /// Returns the value of a specified flag. This function fails in debug mode if the flag was out of range. + /// + public readonly bool HasFlag(int index) + { + int fieldIndex = index >> 5; + + Debug.Assert(fieldIndex < ContainerElementCount); + + int bitIndex = index & 0x1F; + uint segment = this[fieldIndex]; + return BitHelper.HasFlag(segment, bitIndex); + } + } +} + diff --git a/DSharpPlus/Entities/DiscordTeam.cs b/DSharpPlus/Entities/DiscordTeam.cs index cca8d38047..938932862f 100644 --- a/DSharpPlus/Entities/DiscordTeam.cs +++ b/DSharpPlus/Entities/DiscordTeam.cs @@ -1,164 +1,164 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using DSharpPlus.Net.Abstractions; - -namespace DSharpPlus.Entities; - -/// -/// Represents a team consisting of users. A team can own an application. -/// -public sealed class DiscordTeam : SnowflakeObject, IEquatable -{ - /// - /// Gets the team's name. - /// - public string Name { get; internal set; } - - /// - /// Gets the team's icon hash. - /// - public string IconHash { get; internal set; } - - /// - /// Gets the team's icon. - /// - public string Icon - => !string.IsNullOrWhiteSpace(this.IconHash) ? $"https://cdn.discordapp.com/team-icons/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.IconHash}.png?size=1024" : null; - - /// - /// Gets the owner of the team. - /// - public DiscordUser Owner { get; internal set; } - - /// - /// Gets the members of this team. - /// - public IReadOnlyList Members { get; internal set; } - - internal DiscordTeam(TransportTeam tt) - { - this.Id = tt.Id; - this.Name = tt.Name; - this.IconHash = tt.IconHash; - } - - /// - /// Compares this team to another object and returns whether they are equal. - /// - /// Object to compare this team to. - /// Whether this team is equal to the given object. - public override bool Equals(object obj) - => obj is DiscordTeam other && this == other; - - /// - /// Compares this team to another team and returns whether they are equal. - /// - /// Team to compare to. - /// Whether the teams are equal. - public bool Equals(DiscordTeam other) - => this == other; - - /// - /// Gets the hash code of this team. - /// - /// Hash code of this team. - public override int GetHashCode() - => this.Id.GetHashCode(); - - /// - /// Converts this team to its string representation. - /// - /// The string representation of this team. - public override string ToString() - => $"Team: {this.Name} ({this.Id})"; - - public static bool operator ==(DiscordTeam left, DiscordTeam right) - => left?.Id == right?.Id; - - public static bool operator !=(DiscordTeam left, DiscordTeam right) - => left?.Id != right?.Id; -} - -/// -/// Represents a member of . -/// -public sealed class DiscordTeamMember : IEquatable -{ - /// - /// Gets the member's membership status. - /// - public DiscordTeamMembershipStatus MembershipStatus { get; internal set; } - - /// - /// Gets the member's permissions within the team. - /// - public IReadOnlyList Permissions { get; internal set; } - - /// - /// Gets the team this member belongs to. - /// - public DiscordTeam Team { get; internal set; } - - /// - /// Gets the user who is the team member. - /// - public DiscordUser User { get; internal set; } - - internal DiscordTeamMember(TransportTeamMember ttm) - { - this.MembershipStatus = (DiscordTeamMembershipStatus)ttm.MembershipState; - this.Permissions = new ReadOnlySet(new HashSet(ttm.Permissions)); - } - - /// - /// Compares this team member to another object and returns whether they are equal. - /// - /// Object to compare to. - /// Whether this team is equal to given object. - public override bool Equals(object obj) - => obj is DiscordTeamMember other && this == other; - - /// - /// Compares this team member to another team member and returns whether they are equal. - /// - /// Team member to compare to. - /// Whether this team member is equal to the given one. - public bool Equals(DiscordTeamMember other) - => this == other; - - /// - /// Gets a hash code of this team member. - /// - /// Hash code of this team member. - public override int GetHashCode() => HashCode.Combine(this.User, this.Team); - - /// - /// Converts this team member to their string representation. - /// - /// String representation of this team member. - public override string ToString() - => $"Team member: {this.User.Username}#{this.User.Discriminator} ({this.User.Id}), part of team {this.Team.Name} ({this.Team.Id})"; - - public static bool operator ==(DiscordTeamMember left, DiscordTeamMember right) - => left?.Team?.Id == right?.Team?.Id && left?.User?.Id == right?.User?.Id; - - public static bool operator !=(DiscordTeamMember left, DiscordTeamMember right) - => left?.Team?.Id != right?.Team?.Id || left?.User?.Id != right?.User?.Id; -} - -/// -/// Signifies the status of user's team membership. -/// -public enum DiscordTeamMembershipStatus : int -{ - /// - /// Indicates that this user is invited to the team, and is pending membership. - /// - Invited = 1, - - /// - /// Indicates that this user is a member of the team. - /// - Accepted = 2 -} +using System; +using System.Collections.Generic; +using System.Globalization; +using DSharpPlus.Net.Abstractions; + +namespace DSharpPlus.Entities; + +/// +/// Represents a team consisting of users. A team can own an application. +/// +public sealed class DiscordTeam : SnowflakeObject, IEquatable +{ + /// + /// Gets the team's name. + /// + public string Name { get; internal set; } + + /// + /// Gets the team's icon hash. + /// + public string IconHash { get; internal set; } + + /// + /// Gets the team's icon. + /// + public string Icon + => !string.IsNullOrWhiteSpace(this.IconHash) ? $"https://cdn.discordapp.com/team-icons/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.IconHash}.png?size=1024" : null; + + /// + /// Gets the owner of the team. + /// + public DiscordUser Owner { get; internal set; } + + /// + /// Gets the members of this team. + /// + public IReadOnlyList Members { get; internal set; } + + internal DiscordTeam(TransportTeam tt) + { + this.Id = tt.Id; + this.Name = tt.Name; + this.IconHash = tt.IconHash; + } + + /// + /// Compares this team to another object and returns whether they are equal. + /// + /// Object to compare this team to. + /// Whether this team is equal to the given object. + public override bool Equals(object obj) + => obj is DiscordTeam other && this == other; + + /// + /// Compares this team to another team and returns whether they are equal. + /// + /// Team to compare to. + /// Whether the teams are equal. + public bool Equals(DiscordTeam other) + => this == other; + + /// + /// Gets the hash code of this team. + /// + /// Hash code of this team. + public override int GetHashCode() + => this.Id.GetHashCode(); + + /// + /// Converts this team to its string representation. + /// + /// The string representation of this team. + public override string ToString() + => $"Team: {this.Name} ({this.Id})"; + + public static bool operator ==(DiscordTeam left, DiscordTeam right) + => left?.Id == right?.Id; + + public static bool operator !=(DiscordTeam left, DiscordTeam right) + => left?.Id != right?.Id; +} + +/// +/// Represents a member of . +/// +public sealed class DiscordTeamMember : IEquatable +{ + /// + /// Gets the member's membership status. + /// + public DiscordTeamMembershipStatus MembershipStatus { get; internal set; } + + /// + /// Gets the member's permissions within the team. + /// + public IReadOnlyList Permissions { get; internal set; } + + /// + /// Gets the team this member belongs to. + /// + public DiscordTeam Team { get; internal set; } + + /// + /// Gets the user who is the team member. + /// + public DiscordUser User { get; internal set; } + + internal DiscordTeamMember(TransportTeamMember ttm) + { + this.MembershipStatus = (DiscordTeamMembershipStatus)ttm.MembershipState; + this.Permissions = new ReadOnlySet(new HashSet(ttm.Permissions)); + } + + /// + /// Compares this team member to another object and returns whether they are equal. + /// + /// Object to compare to. + /// Whether this team is equal to given object. + public override bool Equals(object obj) + => obj is DiscordTeamMember other && this == other; + + /// + /// Compares this team member to another team member and returns whether they are equal. + /// + /// Team member to compare to. + /// Whether this team member is equal to the given one. + public bool Equals(DiscordTeamMember other) + => this == other; + + /// + /// Gets a hash code of this team member. + /// + /// Hash code of this team member. + public override int GetHashCode() => HashCode.Combine(this.User, this.Team); + + /// + /// Converts this team member to their string representation. + /// + /// String representation of this team member. + public override string ToString() + => $"Team member: {this.User.Username}#{this.User.Discriminator} ({this.User.Id}), part of team {this.Team.Name} ({this.Team.Id})"; + + public static bool operator ==(DiscordTeamMember left, DiscordTeamMember right) + => left?.Team?.Id == right?.Team?.Id && left?.User?.Id == right?.User?.Id; + + public static bool operator !=(DiscordTeamMember left, DiscordTeamMember right) + => left?.Team?.Id != right?.Team?.Id || left?.User?.Id != right?.User?.Id; +} + +/// +/// Signifies the status of user's team membership. +/// +public enum DiscordTeamMembershipStatus : int +{ + /// + /// Indicates that this user is invited to the team, and is pending membership. + /// + Invited = 1, + + /// + /// Indicates that this user is a member of the team. + /// + Accepted = 2 +} diff --git a/DSharpPlus/Entities/DiscordUri.cs b/DSharpPlus/Entities/DiscordUri.cs index 9c60e4e8ae..8f396715fd 100644 --- a/DSharpPlus/Entities/DiscordUri.cs +++ b/DSharpPlus/Entities/DiscordUri.cs @@ -1,79 +1,79 @@ -using System; -using Newtonsoft.Json; - -namespace DSharpPlus.Net; - -/// -/// An URI in a Discord embed doesn't necessarily conform to the RFC 3986. If it uses the attachment:// -/// protocol, it mustn't contain a trailing slash to be interpreted correctly as an embed attachment reference by -/// Discord. -/// -[JsonConverter(typeof(DiscordUriJsonConverter))] -public readonly record struct DiscordUri -{ - private readonly string uri; - - /// - /// The type of this URI. - /// - public DiscordUriType Type { get; } - - internal DiscordUri(Uri value) - { - this.uri = value.AbsoluteUri; - this.Type = DiscordUriType.Standard; - } - - internal DiscordUri(string value) - { - ArgumentNullException.ThrowIfNull(value); - - this.uri = value; - this.Type = IsStandard(this.uri) ? DiscordUriType.Standard : DiscordUriType.NonStandard; - } - - private static bool IsStandard(string value) => !value.StartsWith("attachment://"); - - /// - /// Returns a string representation of this DiscordUri. - /// - /// This DiscordUri, as a string. - public override string? ToString() => this.uri; - - /// - /// Converts this DiscordUri into a canonical representation of a if it can be represented as - /// such, throwing an exception otherwise. - /// - /// A canonical representation of this DiscordUri. - /// If is not , as - /// that would mean creating an invalid Uri, which would result in loss of data. - public Uri? ToUri() - => this.Type == DiscordUriType.Standard - ? new Uri(this.uri) - : throw new UriFormatException( - $@"DiscordUri ""{this.uri}"" would be invalid as a regular URI, please check the {nameof(this.Type)} property first."); - - internal sealed class DiscordUriJsonConverter : JsonConverter - { - public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) - => writer.WriteValue((value as DiscordUri?)?.uri); - - public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) - => reader.Value is null ? null : new DiscordUri((string)reader.Value); - - public override bool CanConvert(Type objectType) => objectType == typeof(DiscordUri); - } -} - -public enum DiscordUriType : byte -{ - /// - /// Represents a URI that conforms to RFC 3986. - /// - Standard, - - /// - /// Represents a URI that does not conform to RFC 3986. - /// - NonStandard -} +using System; +using Newtonsoft.Json; + +namespace DSharpPlus.Net; + +/// +/// An URI in a Discord embed doesn't necessarily conform to the RFC 3986. If it uses the attachment:// +/// protocol, it mustn't contain a trailing slash to be interpreted correctly as an embed attachment reference by +/// Discord. +/// +[JsonConverter(typeof(DiscordUriJsonConverter))] +public readonly record struct DiscordUri +{ + private readonly string uri; + + /// + /// The type of this URI. + /// + public DiscordUriType Type { get; } + + internal DiscordUri(Uri value) + { + this.uri = value.AbsoluteUri; + this.Type = DiscordUriType.Standard; + } + + internal DiscordUri(string value) + { + ArgumentNullException.ThrowIfNull(value); + + this.uri = value; + this.Type = IsStandard(this.uri) ? DiscordUriType.Standard : DiscordUriType.NonStandard; + } + + private static bool IsStandard(string value) => !value.StartsWith("attachment://"); + + /// + /// Returns a string representation of this DiscordUri. + /// + /// This DiscordUri, as a string. + public override string? ToString() => this.uri; + + /// + /// Converts this DiscordUri into a canonical representation of a if it can be represented as + /// such, throwing an exception otherwise. + /// + /// A canonical representation of this DiscordUri. + /// If is not , as + /// that would mean creating an invalid Uri, which would result in loss of data. + public Uri? ToUri() + => this.Type == DiscordUriType.Standard + ? new Uri(this.uri) + : throw new UriFormatException( + $@"DiscordUri ""{this.uri}"" would be invalid as a regular URI, please check the {nameof(this.Type)} property first."); + + internal sealed class DiscordUriJsonConverter : JsonConverter + { + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) + => writer.WriteValue((value as DiscordUri?)?.uri); + + public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) + => reader.Value is null ? null : new DiscordUri((string)reader.Value); + + public override bool CanConvert(Type objectType) => objectType == typeof(DiscordUri); + } +} + +public enum DiscordUriType : byte +{ + /// + /// Represents a URI that conforms to RFC 3986. + /// + Standard, + + /// + /// Represents a URI that does not conform to RFC 3986. + /// + NonStandard +} diff --git a/DSharpPlus/Entities/Emoji/DiscordEmoji.EmojiUtils.cs b/DSharpPlus/Entities/Emoji/DiscordEmoji.EmojiUtils.cs index fde1ce9b84..6c4ac743ba 100644 --- a/DSharpPlus/Entities/Emoji/DiscordEmoji.EmojiUtils.cs +++ b/DSharpPlus/Entities/Emoji/DiscordEmoji.EmojiUtils.cs @@ -1,11716 +1,11716 @@ -using System.Collections.Frozen; -using System.Collections.Generic; - -namespace DSharpPlus.Entities; - -public partial class DiscordEmoji -{ - /// - /// Gets a mapping of :name: => unicode. - /// - private static FrozenDictionary UnicodeEmojis { get; } - - /// - /// Gets a mapping of unicode => :name:. - /// - private static FrozenDictionary DiscordNameLookup { get; } - - static DiscordEmoji() - { - #region Generated Emoji Map - // Generated by Discord Emoji Map generator by Emzi0767 - // Generated from: - // version 2025-04-07T09.04.02.807+00:00 - // url https://static.emzi0767.com/misc/discordEmojiMap.min.json - // definition count 3,799 - - UnicodeEmojis = new Dictionary - { - [":100:"] = "\U0001f4af", - [":1234:"] = "\U0001f522", - [":input_numbers:"] = "\U0001f522", - [":8ball:"] = "\U0001f3b1", - [":a:"] = "\U0001f170\ufe0f", - [":ab:"] = "\U0001f18e", - [":abacus:"] = "\U0001f9ee", - [":abc:"] = "\U0001f524", - [":abcd:"] = "\U0001f521", - [":accept:"] = "\U0001f251", - [":accordion:"] = "\U0001fa97", - [":adhesive_bandage:"] = "\U0001fa79", - [":adult:"] = "\U0001f9d1", - [":person:"] = "\U0001f9d1", - [":adult_tone1:"] = "\U0001f9d1\U0001f3fb", - [":adult_light_skin_tone:"] = "\U0001f9d1\U0001f3fb", - [":adult::skin-tone-1:"] = "\U0001f9d1\U0001f3fb", - [":person::skin-tone-1:"] = "\U0001f9d1\U0001f3fb", - [":adult_tone2:"] = "\U0001f9d1\U0001f3fc", - [":adult_medium_light_skin_tone:"] = "\U0001f9d1\U0001f3fc", - [":adult::skin-tone-2:"] = "\U0001f9d1\U0001f3fc", - [":person::skin-tone-2:"] = "\U0001f9d1\U0001f3fc", - [":adult_tone3:"] = "\U0001f9d1\U0001f3fd", - [":adult_medium_skin_tone:"] = "\U0001f9d1\U0001f3fd", - [":adult::skin-tone-3:"] = "\U0001f9d1\U0001f3fd", - [":person::skin-tone-3:"] = "\U0001f9d1\U0001f3fd", - [":adult_tone4:"] = "\U0001f9d1\U0001f3fe", - [":adult_medium_dark_skin_tone:"] = "\U0001f9d1\U0001f3fe", - [":adult::skin-tone-4:"] = "\U0001f9d1\U0001f3fe", - [":person::skin-tone-4:"] = "\U0001f9d1\U0001f3fe", - [":adult_tone5:"] = "\U0001f9d1\U0001f3ff", - [":adult_dark_skin_tone:"] = "\U0001f9d1\U0001f3ff", - [":adult::skin-tone-5:"] = "\U0001f9d1\U0001f3ff", - [":person::skin-tone-5:"] = "\U0001f9d1\U0001f3ff", - [":aerial_tramway:"] = "\U0001f6a1", - [":airplane:"] = "\u2708\ufe0f", - [":airplane_arriving:"] = "\U0001f6ec", - [":airplane_departure:"] = "\U0001f6eb", - [":airplane_small:"] = "\U0001f6e9\ufe0f", - [":small_airplane:"] = "\U0001f6e9\ufe0f", - [":alarm_clock:"] = "\u23f0", - [":alembic:"] = "\u2697\ufe0f", - [":alien:"] = "\U0001f47d", - [":ambulance:"] = "\U0001f691", - [":amphora:"] = "\U0001f3fa", - [":anatomical_heart:"] = "\U0001fac0", - [":anchor:"] = "\u2693", - [":angel:"] = "\U0001f47c", - [":baby_angel:"] = "\U0001f47c", - [":angel_tone1:"] = "\U0001f47c\U0001f3fb", - [":angel::skin-tone-1:"] = "\U0001f47c\U0001f3fb", - [":baby_angel::skin-tone-1:"] = "\U0001f47c\U0001f3fb", - [":angel_tone2:"] = "\U0001f47c\U0001f3fc", - [":angel::skin-tone-2:"] = "\U0001f47c\U0001f3fc", - [":baby_angel::skin-tone-2:"] = "\U0001f47c\U0001f3fc", - [":angel_tone3:"] = "\U0001f47c\U0001f3fd", - [":angel::skin-tone-3:"] = "\U0001f47c\U0001f3fd", - [":baby_angel::skin-tone-3:"] = "\U0001f47c\U0001f3fd", - [":angel_tone4:"] = "\U0001f47c\U0001f3fe", - [":angel::skin-tone-4:"] = "\U0001f47c\U0001f3fe", - [":baby_angel::skin-tone-4:"] = "\U0001f47c\U0001f3fe", - [":angel_tone5:"] = "\U0001f47c\U0001f3ff", - [":angel::skin-tone-5:"] = "\U0001f47c\U0001f3ff", - [":baby_angel::skin-tone-5:"] = "\U0001f47c\U0001f3ff", - [":anger:"] = "\U0001f4a2", - [":anger_right:"] = "\U0001f5ef\ufe0f", - [":right_anger_bubble:"] = "\U0001f5ef\ufe0f", - [":angry:"] = "\U0001f620", - [":angry_face:"] = "\U0001f620", - [">:("] = "\U0001f620", - [">:-("] = "\U0001f620", - [">=("] = "\U0001f620", - [">=-("] = "\U0001f620", - [":anguished:"] = "\U0001f627", - [":ant:"] = "\U0001f41c", - [":apple:"] = "\U0001f34e", - [":red_apple:"] = "\U0001f34e", - [":aquarius:"] = "\u2652", - [":aries:"] = "\u2648", - [":arrow_backward:"] = "\u25c0\ufe0f", - [":arrow_double_down:"] = "\u23ec", - [":arrow_double_up:"] = "\u23eb", - [":arrow_down:"] = "\u2b07\ufe0f", - [":down_arrow:"] = "\u2b07\ufe0f", - [":arrow_down_small:"] = "\U0001f53d", - [":arrow_forward:"] = "\u25b6\ufe0f", - [":arrow_heading_down:"] = "\u2935\ufe0f", - [":arrow_heading_up:"] = "\u2934\ufe0f", - [":arrow_left:"] = "\u2b05\ufe0f", - [":left_arrow:"] = "\u2b05\ufe0f", - [":arrow_lower_left:"] = "\u2199\ufe0f", - [":arrow_lower_right:"] = "\u2198\ufe0f", - [":arrow_right:"] = "\u27a1\ufe0f", - [":right_arrow:"] = "\u27a1\ufe0f", - [":arrow_right_hook:"] = "\u21aa\ufe0f", - [":arrow_up:"] = "\u2b06\ufe0f", - [":up_arrow:"] = "\u2b06\ufe0f", - [":arrow_up_down:"] = "\u2195\ufe0f", - [":up_down_arrow:"] = "\u2195\ufe0f", - [":arrow_up_small:"] = "\U0001f53c", - [":arrow_upper_left:"] = "\u2196\ufe0f", - [":up_left_arrow:"] = "\u2196\ufe0f", - [":arrow_upper_right:"] = "\u2197\ufe0f", - [":arrows_clockwise:"] = "\U0001f503", - [":arrows_counterclockwise:"] = "\U0001f504", - [":art:"] = "\U0001f3a8", - [":articulated_lorry:"] = "\U0001f69b", - [":artist:"] = "\U0001f9d1\u200d\U0001f3a8", - [":artist_tone1:"] = "\U0001f9d1\U0001f3fb\u200d\U0001f3a8", - [":artist_light_skin_tone:"] = "\U0001f9d1\U0001f3fb\u200d\U0001f3a8", - [":artist::skin-tone-1:"] = "\U0001f9d1\U0001f3fb\u200d\U0001f3a8", - [":artist_tone2:"] = "\U0001f9d1\U0001f3fc\u200d\U0001f3a8", - [":artist_medium_light_skin_tone:"] = "\U0001f9d1\U0001f3fc\u200d\U0001f3a8", - [":artist::skin-tone-2:"] = "\U0001f9d1\U0001f3fc\u200d\U0001f3a8", - [":artist_tone3:"] = "\U0001f9d1\U0001f3fd\u200d\U0001f3a8", - [":artist_medium_skin_tone:"] = "\U0001f9d1\U0001f3fd\u200d\U0001f3a8", - [":artist::skin-tone-3:"] = "\U0001f9d1\U0001f3fd\u200d\U0001f3a8", - [":artist_tone4:"] = "\U0001f9d1\U0001f3fe\u200d\U0001f3a8", - [":artist_medium_dark_skin_tone:"] = "\U0001f9d1\U0001f3fe\u200d\U0001f3a8", - [":artist::skin-tone-4:"] = "\U0001f9d1\U0001f3fe\u200d\U0001f3a8", - [":artist_tone5:"] = "\U0001f9d1\U0001f3ff\u200d\U0001f3a8", - [":artist_dark_skin_tone:"] = "\U0001f9d1\U0001f3ff\u200d\U0001f3a8", - [":artist::skin-tone-5:"] = "\U0001f9d1\U0001f3ff\u200d\U0001f3a8", - [":asterisk:"] = "\u002a\ufe0f\u20e3", - [":keycap_asterisk:"] = "\u002a\ufe0f\u20e3", - [":astonished:"] = "\U0001f632", - [":astronaut:"] = "\U0001f9d1\u200d\U0001f680", - [":astronaut_tone1:"] = "\U0001f9d1\U0001f3fb\u200d\U0001f680", - [":astronaut_light_skin_tone:"] = "\U0001f9d1\U0001f3fb\u200d\U0001f680", - [":astronaut::skin-tone-1:"] = "\U0001f9d1\U0001f3fb\u200d\U0001f680", - [":astronaut_tone2:"] = "\U0001f9d1\U0001f3fc\u200d\U0001f680", - [":astronaut_medium_light_skin_tone:"] = "\U0001f9d1\U0001f3fc\u200d\U0001f680", - [":astronaut::skin-tone-2:"] = "\U0001f9d1\U0001f3fc\u200d\U0001f680", - [":astronaut_tone3:"] = "\U0001f9d1\U0001f3fd\u200d\U0001f680", - [":astronaut_medium_skin_tone:"] = "\U0001f9d1\U0001f3fd\u200d\U0001f680", - [":astronaut::skin-tone-3:"] = "\U0001f9d1\U0001f3fd\u200d\U0001f680", - [":astronaut_tone4:"] = "\U0001f9d1\U0001f3fe\u200d\U0001f680", - [":astronaut_medium_dark_skin_tone:"] = "\U0001f9d1\U0001f3fe\u200d\U0001f680", - [":astronaut::skin-tone-4:"] = "\U0001f9d1\U0001f3fe\u200d\U0001f680", - [":astronaut_tone5:"] = "\U0001f9d1\U0001f3ff\u200d\U0001f680", - [":astronaut_dark_skin_tone:"] = "\U0001f9d1\U0001f3ff\u200d\U0001f680", - [":astronaut::skin-tone-5:"] = "\U0001f9d1\U0001f3ff\u200d\U0001f680", - [":athletic_shoe:"] = "\U0001f45f", - [":running_shoe:"] = "\U0001f45f", - [":atm:"] = "\U0001f3e7", - [":atom:"] = "\u269b\ufe0f", - [":atom_symbol:"] = "\u269b\ufe0f", - [":auto_rickshaw:"] = "\U0001f6fa", - [":avocado:"] = "\U0001f951", - [":axe:"] = "\U0001fa93", - [":b:"] = "\U0001f171\ufe0f", - [":baby:"] = "\U0001f476", - [":baby_bottle:"] = "\U0001f37c", - [":baby_chick:"] = "\U0001f424", - [":baby_symbol:"] = "\U0001f6bc", - [":baby_tone1:"] = "\U0001f476\U0001f3fb", - [":baby::skin-tone-1:"] = "\U0001f476\U0001f3fb", - [":baby_tone2:"] = "\U0001f476\U0001f3fc", - [":baby::skin-tone-2:"] = "\U0001f476\U0001f3fc", - [":baby_tone3:"] = "\U0001f476\U0001f3fd", - [":baby::skin-tone-3:"] = "\U0001f476\U0001f3fd", - [":baby_tone4:"] = "\U0001f476\U0001f3fe", - [":baby::skin-tone-4:"] = "\U0001f476\U0001f3fe", - [":baby_tone5:"] = "\U0001f476\U0001f3ff", - [":baby::skin-tone-5:"] = "\U0001f476\U0001f3ff", - [":back:"] = "\U0001f519", - [":back_arrow:"] = "\U0001f519", - [":bacon:"] = "\U0001f953", - [":badger:"] = "\U0001f9a1", - [":badminton:"] = "\U0001f3f8", - [":bagel:"] = "\U0001f96f", - [":baggage_claim:"] = "\U0001f6c4", - [":ballet_shoes:"] = "\U0001fa70", - [":balloon:"] = "\U0001f388", - [":ballot_box:"] = "\U0001f5f3\ufe0f", - [":ballot_box_with_ballot:"] = "\U0001f5f3\ufe0f", - [":ballot_box_with_check:"] = "\u2611\ufe0f", - [":bamboo:"] = "\U0001f38d", - [":banana:"] = "\U0001f34c", - [":bangbang:"] = "\u203c\ufe0f", - [":banjo:"] = "\U0001fa95", - [":bank:"] = "\U0001f3e6", - [":bar_chart:"] = "\U0001f4ca", - [":barber:"] = "\U0001f488", - [":barber_pole:"] = "\U0001f488", - [":baseball:"] = "\u26be", - [":basket:"] = "\U0001f9fa", - [":basketball:"] = "\U0001f3c0", - [":bat:"] = "\U0001f987", - [":bath:"] = "\U0001f6c0", - [":bath_tone1:"] = "\U0001f6c0\U0001f3fb", - [":bath::skin-tone-1:"] = "\U0001f6c0\U0001f3fb", - [":bath_tone2:"] = "\U0001f6c0\U0001f3fc", - [":bath::skin-tone-2:"] = "\U0001f6c0\U0001f3fc", - [":bath_tone3:"] = "\U0001f6c0\U0001f3fd", - [":bath::skin-tone-3:"] = "\U0001f6c0\U0001f3fd", - [":bath_tone4:"] = "\U0001f6c0\U0001f3fe", - [":bath::skin-tone-4:"] = "\U0001f6c0\U0001f3fe", - [":bath_tone5:"] = "\U0001f6c0\U0001f3ff", - [":bath::skin-tone-5:"] = "\U0001f6c0\U0001f3ff", - [":bathtub:"] = "\U0001f6c1", - [":battery:"] = "\U0001f50b", - [":beach:"] = "\U0001f3d6\ufe0f", - [":beach_with_umbrella:"] = "\U0001f3d6\ufe0f", - [":beach_umbrella:"] = "\u26f1\ufe0f", - [":umbrella_on_ground:"] = "\u26f1\ufe0f", - [":beans:"] = "\U0001fad8", - [":bear:"] = "\U0001f43b", - [":bearded_person:"] = "\U0001f9d4", - [":person_beard:"] = "\U0001f9d4", - [":bearded_person_tone1:"] = "\U0001f9d4\U0001f3fb", - [":bearded_person_light_skin_tone:"] = "\U0001f9d4\U0001f3fb", - [":bearded_person::skin-tone-1:"] = "\U0001f9d4\U0001f3fb", - [":person_beard::skin-tone-1:"] = "\U0001f9d4\U0001f3fb", - [":bearded_person_tone2:"] = "\U0001f9d4\U0001f3fc", - [":bearded_person_medium_light_skin_tone:"] = "\U0001f9d4\U0001f3fc", - [":bearded_person::skin-tone-2:"] = "\U0001f9d4\U0001f3fc", - [":person_beard::skin-tone-2:"] = "\U0001f9d4\U0001f3fc", - [":bearded_person_tone3:"] = "\U0001f9d4\U0001f3fd", - [":bearded_person_medium_skin_tone:"] = "\U0001f9d4\U0001f3fd", - [":bearded_person::skin-tone-3:"] = "\U0001f9d4\U0001f3fd", - [":person_beard::skin-tone-3:"] = "\U0001f9d4\U0001f3fd", - [":bearded_person_tone4:"] = "\U0001f9d4\U0001f3fe", - [":bearded_person_medium_dark_skin_tone:"] = "\U0001f9d4\U0001f3fe", - [":bearded_person::skin-tone-4:"] = "\U0001f9d4\U0001f3fe", - [":person_beard::skin-tone-4:"] = "\U0001f9d4\U0001f3fe", - [":bearded_person_tone5:"] = "\U0001f9d4\U0001f3ff", - [":bearded_person_dark_skin_tone:"] = "\U0001f9d4\U0001f3ff", - [":bearded_person::skin-tone-5:"] = "\U0001f9d4\U0001f3ff", - [":person_beard::skin-tone-5:"] = "\U0001f9d4\U0001f3ff", - [":beaver:"] = "\U0001f9ab", - [":bed:"] = "\U0001f6cf\ufe0f", - [":bee:"] = "\U0001f41d", - [":honeybee:"] = "\U0001f41d", - [":beer:"] = "\U0001f37a", - [":beer_mug:"] = "\U0001f37a", - [":beers:"] = "\U0001f37b", - [":beetle:"] = "\U0001fab2", - [":beginner:"] = "\U0001f530", - [":bell:"] = "\U0001f514", - [":bell_pepper:"] = "\U0001fad1", - [":bellhop:"] = "\U0001f6ce\ufe0f", - [":bellhop_bell:"] = "\U0001f6ce\ufe0f", - [":bento:"] = "\U0001f371", - [":bento_box:"] = "\U0001f371", - [":beverage_box:"] = "\U0001f9c3", - [":bike:"] = "\U0001f6b2", - [":bicycle:"] = "\U0001f6b2", - [":bikini:"] = "\U0001f459", - [":billed_cap:"] = "\U0001f9e2", - [":biohazard:"] = "\u2623\ufe0f", - [":biohazard_sign:"] = "\u2623\ufe0f", - [":bird:"] = "\U0001f426", - [":birthday:"] = "\U0001f382", - [":birthday_cake:"] = "\U0001f382", - [":bison:"] = "\U0001f9ac", - [":biting_lip:"] = "\U0001fae6", - [":black_bird:"] = "\U0001f426\u200d\u2b1b", - [":black_cat:"] = "\U0001f408\u200d\u2b1b", - [":black_circle:"] = "\u26ab", - [":black_heart:"] = "\U0001f5a4", - [":black_joker:"] = "\U0001f0cf", - [":joker:"] = "\U0001f0cf", - [":black_large_square:"] = "\u2b1b", - [":black_medium_small_square:"] = "\u25fe", - [":black_medium_square:"] = "\u25fc\ufe0f", - [":black_nib:"] = "\u2712\ufe0f", - [":black_small_square:"] = "\u25aa\ufe0f", - [":black_square_button:"] = "\U0001f532", - [":blond_haired_man:"] = "\U0001f471\u200d\u2642\ufe0f", - [":blond_haired_man_tone1:"] = "\U0001f471\U0001f3fb\u200d\u2642\ufe0f", - [":blond_haired_man_light_skin_tone:"] = "\U0001f471\U0001f3fb\u200d\u2642\ufe0f", - [":blond_haired_man::skin-tone-1:"] = "\U0001f471\U0001f3fb\u200d\u2642\ufe0f", - [":blond_haired_man_tone2:"] = "\U0001f471\U0001f3fc\u200d\u2642\ufe0f", - [":blond_haired_man_medium_light_skin_tone:"] = "\U0001f471\U0001f3fc\u200d\u2642\ufe0f", - [":blond_haired_man::skin-tone-2:"] = "\U0001f471\U0001f3fc\u200d\u2642\ufe0f", - [":blond_haired_man_tone3:"] = "\U0001f471\U0001f3fd\u200d\u2642\ufe0f", - [":blond_haired_man_medium_skin_tone:"] = "\U0001f471\U0001f3fd\u200d\u2642\ufe0f", - [":blond_haired_man::skin-tone-3:"] = "\U0001f471\U0001f3fd\u200d\u2642\ufe0f", - [":blond_haired_man_tone4:"] = "\U0001f471\U0001f3fe\u200d\u2642\ufe0f", - [":blond_haired_man_medium_dark_skin_tone:"] = "\U0001f471\U0001f3fe\u200d\u2642\ufe0f", - [":blond_haired_man::skin-tone-4:"] = "\U0001f471\U0001f3fe\u200d\u2642\ufe0f", - [":blond_haired_man_tone5:"] = "\U0001f471\U0001f3ff\u200d\u2642\ufe0f", - [":blond_haired_man_dark_skin_tone:"] = "\U0001f471\U0001f3ff\u200d\u2642\ufe0f", - [":blond_haired_man::skin-tone-5:"] = "\U0001f471\U0001f3ff\u200d\u2642\ufe0f", - [":blond_haired_person:"] = "\U0001f471", - [":person_with_blond_hair:"] = "\U0001f471", - [":blond_haired_person_tone1:"] = "\U0001f471\U0001f3fb", - [":person_with_blond_hair_tone1:"] = "\U0001f471\U0001f3fb", - [":blond_haired_person::skin-tone-1:"] = "\U0001f471\U0001f3fb", - [":person_with_blond_hair::skin-tone-1:"] = "\U0001f471\U0001f3fb", - [":blond_haired_person_tone2:"] = "\U0001f471\U0001f3fc", - [":person_with_blond_hair_tone2:"] = "\U0001f471\U0001f3fc", - [":blond_haired_person::skin-tone-2:"] = "\U0001f471\U0001f3fc", - [":person_with_blond_hair::skin-tone-2:"] = "\U0001f471\U0001f3fc", - [":blond_haired_person_tone3:"] = "\U0001f471\U0001f3fd", - [":person_with_blond_hair_tone3:"] = "\U0001f471\U0001f3fd", - [":blond_haired_person::skin-tone-3:"] = "\U0001f471\U0001f3fd", - [":person_with_blond_hair::skin-tone-3:"] = "\U0001f471\U0001f3fd", - [":blond_haired_person_tone4:"] = "\U0001f471\U0001f3fe", - [":person_with_blond_hair_tone4:"] = "\U0001f471\U0001f3fe", - [":blond_haired_person::skin-tone-4:"] = "\U0001f471\U0001f3fe", - [":person_with_blond_hair::skin-tone-4:"] = "\U0001f471\U0001f3fe", - [":blond_haired_person_tone5:"] = "\U0001f471\U0001f3ff", - [":person_with_blond_hair_tone5:"] = "\U0001f471\U0001f3ff", - [":blond_haired_person::skin-tone-5:"] = "\U0001f471\U0001f3ff", - [":person_with_blond_hair::skin-tone-5:"] = "\U0001f471\U0001f3ff", - [":blond_haired_woman:"] = "\U0001f471\u200d\u2640\ufe0f", - [":blond_haired_woman_tone1:"] = "\U0001f471\U0001f3fb\u200d\u2640\ufe0f", - [":blond_haired_woman_light_skin_tone:"] = "\U0001f471\U0001f3fb\u200d\u2640\ufe0f", - [":blond_haired_woman::skin-tone-1:"] = "\U0001f471\U0001f3fb\u200d\u2640\ufe0f", - [":blond_haired_woman_tone2:"] = "\U0001f471\U0001f3fc\u200d\u2640\ufe0f", - [":blond_haired_woman_medium_light_skin_tone:"] = "\U0001f471\U0001f3fc\u200d\u2640\ufe0f", - [":blond_haired_woman::skin-tone-2:"] = "\U0001f471\U0001f3fc\u200d\u2640\ufe0f", - [":blond_haired_woman_tone3:"] = "\U0001f471\U0001f3fd\u200d\u2640\ufe0f", - [":blond_haired_woman_medium_skin_tone:"] = "\U0001f471\U0001f3fd\u200d\u2640\ufe0f", - [":blond_haired_woman::skin-tone-3:"] = "\U0001f471\U0001f3fd\u200d\u2640\ufe0f", - [":blond_haired_woman_tone4:"] = "\U0001f471\U0001f3fe\u200d\u2640\ufe0f", - [":blond_haired_woman_medium_dark_skin_tone:"] = "\U0001f471\U0001f3fe\u200d\u2640\ufe0f", - [":blond_haired_woman::skin-tone-4:"] = "\U0001f471\U0001f3fe\u200d\u2640\ufe0f", - [":blond_haired_woman_tone5:"] = "\U0001f471\U0001f3ff\u200d\u2640\ufe0f", - [":blond_haired_woman_dark_skin_tone:"] = "\U0001f471\U0001f3ff\u200d\u2640\ufe0f", - [":blond_haired_woman::skin-tone-5:"] = "\U0001f471\U0001f3ff\u200d\u2640\ufe0f", - [":blossom:"] = "\U0001f33c", - [":blowfish:"] = "\U0001f421", - [":blue_book:"] = "\U0001f4d8", - [":blue_car:"] = "\U0001f699", - [":blue_circle:"] = "\U0001f535", - [":blue_heart:"] = "\U0001f499", - [":blue_square:"] = "\U0001f7e6", - [":blueberries:"] = "\U0001fad0", - [":blush:"] = "\U0001f60a", - [":\")"] = "\U0001f60a", - [":-\")"] = "\U0001f60a", - ["=\")"] = "\U0001f60a", - ["=-\")"] = "\U0001f60a", - [":boar:"] = "\U0001f417", - [":bomb:"] = "\U0001f4a3", - [":bone:"] = "\U0001f9b4", - [":book:"] = "\U0001f4d6", - [":open_book:"] = "\U0001f4d6", - [":bookmark:"] = "\U0001f516", - [":bookmark_tabs:"] = "\U0001f4d1", - [":books:"] = "\U0001f4da", - [":boom:"] = "\U0001f4a5", - [":collision:"] = "\U0001f4a5", - [":boomerang:"] = "\U0001fa83", - [":boot:"] = "\U0001f462", - [":womans_boot:"] = "\U0001f462", - [":bouquet:"] = "\U0001f490", - [":bow_and_arrow:"] = "\U0001f3f9", - [":archery:"] = "\U0001f3f9", - [":bowl_with_spoon:"] = "\U0001f963", - [":bowling:"] = "\U0001f3b3", - [":boxing_glove:"] = "\U0001f94a", - [":boxing_gloves:"] = "\U0001f94a", - [":boy:"] = "\U0001f466", - [":boy_tone1:"] = "\U0001f466\U0001f3fb", - [":boy::skin-tone-1:"] = "\U0001f466\U0001f3fb", - [":boy_tone2:"] = "\U0001f466\U0001f3fc", - [":boy::skin-tone-2:"] = "\U0001f466\U0001f3fc", - [":boy_tone3:"] = "\U0001f466\U0001f3fd", - [":boy::skin-tone-3:"] = "\U0001f466\U0001f3fd", - [":boy_tone4:"] = "\U0001f466\U0001f3fe", - [":boy::skin-tone-4:"] = "\U0001f466\U0001f3fe", - [":boy_tone5:"] = "\U0001f466\U0001f3ff", - [":boy::skin-tone-5:"] = "\U0001f466\U0001f3ff", - [":brain:"] = "\U0001f9e0", - [":bread:"] = "\U0001f35e", - [":breast_feeding:"] = "\U0001f931", - [":breast_feeding_tone1:"] = "\U0001f931\U0001f3fb", - [":breast_feeding_light_skin_tone:"] = "\U0001f931\U0001f3fb", - [":breast_feeding::skin-tone-1:"] = "\U0001f931\U0001f3fb", - [":breast_feeding_tone2:"] = "\U0001f931\U0001f3fc", - [":breast_feeding_medium_light_skin_tone:"] = "\U0001f931\U0001f3fc", - [":breast_feeding::skin-tone-2:"] = "\U0001f931\U0001f3fc", - [":breast_feeding_tone3:"] = "\U0001f931\U0001f3fd", - [":breast_feeding_medium_skin_tone:"] = "\U0001f931\U0001f3fd", - [":breast_feeding::skin-tone-3:"] = "\U0001f931\U0001f3fd", - [":breast_feeding_tone4:"] = "\U0001f931\U0001f3fe", - [":breast_feeding_medium_dark_skin_tone:"] = "\U0001f931\U0001f3fe", - [":breast_feeding::skin-tone-4:"] = "\U0001f931\U0001f3fe", - [":breast_feeding_tone5:"] = "\U0001f931\U0001f3ff", - [":breast_feeding_dark_skin_tone:"] = "\U0001f931\U0001f3ff", - [":breast_feeding::skin-tone-5:"] = "\U0001f931\U0001f3ff", - [":bricks:"] = "\U0001f9f1", - [":brick:"] = "\U0001f9f1", - [":bridge_at_night:"] = "\U0001f309", - [":briefcase:"] = "\U0001f4bc", - [":briefs:"] = "\U0001fa72", - [":broccoli:"] = "\U0001f966", - [":broken_chain:"] = "\u26d3\ufe0f\u200d\U0001f4a5", - [":broken_heart:"] = "\U0001f494", - [" - { - ["\U0001f4af"] = ":100:", - ["\U0001f522"] = ":1234:", - ["\U0001f3b1"] = ":8ball:", - ["\U0001f170\ufe0f"] = ":a:", - ["\U0001f170"] = ":a:", - ["\U0001f18e"] = ":ab:", - ["\U0001f9ee"] = ":abacus:", - ["\U0001f524"] = ":abc:", - ["\U0001f521"] = ":abcd:", - ["\U0001f251"] = ":accept:", - ["\U0001fa97"] = ":accordion:", - ["\U0001fa79"] = ":adhesive_bandage:", - ["\U0001f9d1\U0001f3fb"] = ":adult_tone1:", - ["\U0001f9d1\U0001f3fc"] = ":adult_tone2:", - ["\U0001f9d1\U0001f3fd"] = ":adult_tone3:", - ["\U0001f9d1\U0001f3fe"] = ":adult_tone4:", - ["\U0001f9d1\U0001f3ff"] = ":adult_tone5:", - ["\U0001f9d1"] = ":adult:", - ["\U0001f6a1"] = ":aerial_tramway:", - ["\U0001f6ec"] = ":airplane_arriving:", - ["\U0001f6eb"] = ":airplane_departure:", - ["\U0001f6e9\ufe0f"] = ":airplane_small:", - ["\U0001f6e9"] = ":airplane_small:", - ["\u2708\ufe0f"] = ":airplane:", - ["\u2708"] = ":airplane:", - ["\u23f0"] = ":alarm_clock:", - ["\u2697\ufe0f"] = ":alembic:", - ["\u2697"] = ":alembic:", - ["\U0001f47d"] = ":alien:", - ["\U0001f691"] = ":ambulance:", - ["\U0001f3fa"] = ":amphora:", - ["\U0001fac0"] = ":anatomical_heart:", - ["\u2693"] = ":anchor:", - ["\U0001f47c\U0001f3fb"] = ":angel_tone1:", - ["\U0001f47c\U0001f3fc"] = ":angel_tone2:", - ["\U0001f47c\U0001f3fd"] = ":angel_tone3:", - ["\U0001f47c\U0001f3fe"] = ":angel_tone4:", - ["\U0001f47c\U0001f3ff"] = ":angel_tone5:", - ["\U0001f47c"] = ":angel:", - ["\U0001f5ef\ufe0f"] = ":anger_right:", - ["\U0001f5ef"] = ":anger_right:", - ["\U0001f4a2"] = ":anger:", - ["\U0001f620"] = ":angry:", - ["\U0001f627"] = ":anguished:", - ["\U0001f41c"] = ":ant:", - ["\U0001f34e"] = ":apple:", - ["\u2652"] = ":aquarius:", - ["\u2648"] = ":aries:", - ["\u25c0\ufe0f"] = ":arrow_backward:", - ["\u25c0"] = ":arrow_backward:", - ["\u23ec"] = ":arrow_double_down:", - ["\u23eb"] = ":arrow_double_up:", - ["\U0001f53d"] = ":arrow_down_small:", - ["\u2b07\ufe0f"] = ":arrow_down:", - ["\u2b07"] = ":arrow_down:", - ["\u25b6\ufe0f"] = ":arrow_forward:", - ["\u25b6"] = ":arrow_forward:", - ["\u2935\ufe0f"] = ":arrow_heading_down:", - ["\u2935"] = ":arrow_heading_down:", - ["\u2934\ufe0f"] = ":arrow_heading_up:", - ["\u2934"] = ":arrow_heading_up:", - ["\u2b05\ufe0f"] = ":arrow_left:", - ["\u2b05"] = ":arrow_left:", - ["\u2199\ufe0f"] = ":arrow_lower_left:", - ["\u2199"] = ":arrow_lower_left:", - ["\u2198\ufe0f"] = ":arrow_lower_right:", - ["\u2198"] = ":arrow_lower_right:", - ["\u21aa\ufe0f"] = ":arrow_right_hook:", - ["\u21aa"] = ":arrow_right_hook:", - ["\u27a1\ufe0f"] = ":arrow_right:", - ["\u27a1"] = ":arrow_right:", - ["\u2195\ufe0f"] = ":arrow_up_down:", - ["\u2195"] = ":arrow_up_down:", - ["\U0001f53c"] = ":arrow_up_small:", - ["\u2b06\ufe0f"] = ":arrow_up:", - ["\u2b06"] = ":arrow_up:", - ["\u2196\ufe0f"] = ":arrow_upper_left:", - ["\u2196"] = ":arrow_upper_left:", - ["\u2197\ufe0f"] = ":arrow_upper_right:", - ["\u2197"] = ":arrow_upper_right:", - ["\U0001f503"] = ":arrows_clockwise:", - ["\U0001f504"] = ":arrows_counterclockwise:", - ["\U0001f3a8"] = ":art:", - ["\U0001f69b"] = ":articulated_lorry:", - ["\U0001f9d1\U0001f3fb\u200d\U0001f3a8"] = ":artist_tone1:", - ["\U0001f9d1\U0001f3fc\u200d\U0001f3a8"] = ":artist_tone2:", - ["\U0001f9d1\U0001f3fd\u200d\U0001f3a8"] = ":artist_tone3:", - ["\U0001f9d1\U0001f3fe\u200d\U0001f3a8"] = ":artist_tone4:", - ["\U0001f9d1\U0001f3ff\u200d\U0001f3a8"] = ":artist_tone5:", - ["\U0001f9d1\u200d\U0001f3a8"] = ":artist:", - ["\u002a\ufe0f\u20e3"] = ":asterisk:", - ["\u002a\u20e3"] = ":asterisk:", - ["\U0001f632"] = ":astonished:", - ["\U0001f9d1\U0001f3fb\u200d\U0001f680"] = ":astronaut_tone1:", - ["\U0001f9d1\U0001f3fc\u200d\U0001f680"] = ":astronaut_tone2:", - ["\U0001f9d1\U0001f3fd\u200d\U0001f680"] = ":astronaut_tone3:", - ["\U0001f9d1\U0001f3fe\u200d\U0001f680"] = ":astronaut_tone4:", - ["\U0001f9d1\U0001f3ff\u200d\U0001f680"] = ":astronaut_tone5:", - ["\U0001f9d1\u200d\U0001f680"] = ":astronaut:", - ["\U0001f45f"] = ":athletic_shoe:", - ["\U0001f3e7"] = ":atm:", - ["\u269b\ufe0f"] = ":atom:", - ["\u269b"] = ":atom:", - ["\U0001f6fa"] = ":auto_rickshaw:", - ["\U0001f951"] = ":avocado:", - ["\U0001fa93"] = ":axe:", - ["\U0001f171\ufe0f"] = ":b:", - ["\U0001f171"] = ":b:", - ["\U0001f37c"] = ":baby_bottle:", - ["\U0001f424"] = ":baby_chick:", - ["\U0001f6bc"] = ":baby_symbol:", - ["\U0001f476\U0001f3fb"] = ":baby_tone1:", - ["\U0001f476\U0001f3fc"] = ":baby_tone2:", - ["\U0001f476\U0001f3fd"] = ":baby_tone3:", - ["\U0001f476\U0001f3fe"] = ":baby_tone4:", - ["\U0001f476\U0001f3ff"] = ":baby_tone5:", - ["\U0001f476"] = ":baby:", - ["\U0001f519"] = ":back:", - ["\U0001f953"] = ":bacon:", - ["\U0001f9a1"] = ":badger:", - ["\U0001f3f8"] = ":badminton:", - ["\U0001f96f"] = ":bagel:", - ["\U0001f6c4"] = ":baggage_claim:", - ["\U0001fa70"] = ":ballet_shoes:", - ["\U0001f388"] = ":balloon:", - ["\u2611\ufe0f"] = ":ballot_box_with_check:", - ["\u2611"] = ":ballot_box_with_check:", - ["\U0001f5f3\ufe0f"] = ":ballot_box:", - ["\U0001f5f3"] = ":ballot_box:", - ["\U0001f38d"] = ":bamboo:", - ["\U0001f34c"] = ":banana:", - ["\u203c\ufe0f"] = ":bangbang:", - ["\u203c"] = ":bangbang:", - ["\U0001fa95"] = ":banjo:", - ["\U0001f3e6"] = ":bank:", - ["\U0001f4ca"] = ":bar_chart:", - ["\U0001f488"] = ":barber:", - ["\u26be"] = ":baseball:", - ["\U0001f9fa"] = ":basket:", - ["\U0001f3c0"] = ":basketball:", - ["\U0001f987"] = ":bat:", - ["\U0001f6c0\U0001f3fb"] = ":bath_tone1:", - ["\U0001f6c0\U0001f3fc"] = ":bath_tone2:", - ["\U0001f6c0\U0001f3fd"] = ":bath_tone3:", - ["\U0001f6c0\U0001f3fe"] = ":bath_tone4:", - ["\U0001f6c0\U0001f3ff"] = ":bath_tone5:", - ["\U0001f6c0"] = ":bath:", - ["\U0001f6c1"] = ":bathtub:", - ["\U0001f50b"] = ":battery:", - ["\u26f1\ufe0f"] = ":beach_umbrella:", - ["\u26f1"] = ":beach_umbrella:", - ["\U0001f3d6\ufe0f"] = ":beach:", - ["\U0001f3d6"] = ":beach:", - ["\U0001fad8"] = ":beans:", - ["\U0001f43b"] = ":bear:", - ["\U0001f9d4\U0001f3fb"] = ":bearded_person_tone1:", - ["\U0001f9d4\U0001f3fc"] = ":bearded_person_tone2:", - ["\U0001f9d4\U0001f3fd"] = ":bearded_person_tone3:", - ["\U0001f9d4\U0001f3fe"] = ":bearded_person_tone4:", - ["\U0001f9d4\U0001f3ff"] = ":bearded_person_tone5:", - ["\U0001f9d4"] = ":bearded_person:", - ["\U0001f9ab"] = ":beaver:", - ["\U0001f6cf\ufe0f"] = ":bed:", - ["\U0001f6cf"] = ":bed:", - ["\U0001f41d"] = ":bee:", - ["\U0001f37a"] = ":beer:", - ["\U0001f37b"] = ":beers:", - ["\U0001fab2"] = ":beetle:", - ["\U0001f530"] = ":beginner:", - ["\U0001fad1"] = ":bell_pepper:", - ["\U0001f514"] = ":bell:", - ["\U0001f6ce\ufe0f"] = ":bellhop:", - ["\U0001f6ce"] = ":bellhop:", - ["\U0001f371"] = ":bento:", - ["\U0001f9c3"] = ":beverage_box:", - ["\U0001f6b2"] = ":bike:", - ["\U0001f459"] = ":bikini:", - ["\U0001f9e2"] = ":billed_cap:", - ["\u2623\ufe0f"] = ":biohazard:", - ["\u2623"] = ":biohazard:", - ["\U0001f426"] = ":bird:", - ["\U0001f382"] = ":birthday:", - ["\U0001f9ac"] = ":bison:", - ["\U0001fae6"] = ":biting_lip:", - ["\U0001f426\u200d\u2b1b"] = ":black_bird:", - ["\U0001f408\u200d\u2b1b"] = ":black_cat:", - ["\u26ab"] = ":black_circle:", - ["\U0001f5a4"] = ":black_heart:", - ["\U0001f0cf"] = ":black_joker:", - ["\u2b1b"] = ":black_large_square:", - ["\u25fe"] = ":black_medium_small_square:", - ["\u25fc\ufe0f"] = ":black_medium_square:", - ["\u25fc"] = ":black_medium_square:", - ["\u2712\ufe0f"] = ":black_nib:", - ["\u2712"] = ":black_nib:", - ["\u25aa\ufe0f"] = ":black_small_square:", - ["\u25aa"] = ":black_small_square:", - ["\U0001f532"] = ":black_square_button:", - ["\U0001f471\U0001f3fb\u200d\u2642\ufe0f"] = ":blond_haired_man_tone1:", - ["\U0001f471\U0001f3fc\u200d\u2642\ufe0f"] = ":blond_haired_man_tone2:", - ["\U0001f471\U0001f3fd\u200d\u2642\ufe0f"] = ":blond_haired_man_tone3:", - ["\U0001f471\U0001f3fe\u200d\u2642\ufe0f"] = ":blond_haired_man_tone4:", - ["\U0001f471\U0001f3ff\u200d\u2642\ufe0f"] = ":blond_haired_man_tone5:", - ["\U0001f471\u200d\u2642\ufe0f"] = ":blond_haired_man:", - ["\U0001f471\U0001f3fb"] = ":blond_haired_person_tone1:", - ["\U0001f471\U0001f3fc"] = ":blond_haired_person_tone2:", - ["\U0001f471\U0001f3fd"] = ":blond_haired_person_tone3:", - ["\U0001f471\U0001f3fe"] = ":blond_haired_person_tone4:", - ["\U0001f471\U0001f3ff"] = ":blond_haired_person_tone5:", - ["\U0001f471"] = ":blond_haired_person:", - ["\U0001f471\U0001f3fb\u200d\u2640\ufe0f"] = ":blond_haired_woman_tone1:", - ["\U0001f471\U0001f3fc\u200d\u2640\ufe0f"] = ":blond_haired_woman_tone2:", - ["\U0001f471\U0001f3fd\u200d\u2640\ufe0f"] = ":blond_haired_woman_tone3:", - ["\U0001f471\U0001f3fe\u200d\u2640\ufe0f"] = ":blond_haired_woman_tone4:", - ["\U0001f471\U0001f3ff\u200d\u2640\ufe0f"] = ":blond_haired_woman_tone5:", - ["\U0001f471\u200d\u2640\ufe0f"] = ":blond_haired_woman:", - ["\U0001f33c"] = ":blossom:", - ["\U0001f421"] = ":blowfish:", - ["\U0001f4d8"] = ":blue_book:", - ["\U0001f699"] = ":blue_car:", - ["\U0001f535"] = ":blue_circle:", - ["\U0001f499"] = ":blue_heart:", - ["\U0001f7e6"] = ":blue_square:", - ["\U0001fad0"] = ":blueberries:", - ["\U0001f60a"] = ":blush:", - ["\U0001f417"] = ":boar:", - ["\U0001f4a3"] = ":bomb:", - ["\U0001f9b4"] = ":bone:", - ["\U0001f4d6"] = ":book:", - ["\U0001f4d1"] = ":bookmark_tabs:", - ["\U0001f516"] = ":bookmark:", - ["\U0001f4da"] = ":books:", - ["\U0001f4a5"] = ":boom:", - ["\U0001fa83"] = ":boomerang:", - ["\U0001f462"] = ":boot:", - ["\U0001f490"] = ":bouquet:", - ["\U0001f3f9"] = ":bow_and_arrow:", - ["\U0001f963"] = ":bowl_with_spoon:", - ["\U0001f3b3"] = ":bowling:", - ["\U0001f94a"] = ":boxing_glove:", - ["\U0001f466\U0001f3fb"] = ":boy_tone1:", - ["\U0001f466\U0001f3fc"] = ":boy_tone2:", - ["\U0001f466\U0001f3fd"] = ":boy_tone3:", - ["\U0001f466\U0001f3fe"] = ":boy_tone4:", - ["\U0001f466\U0001f3ff"] = ":boy_tone5:", - ["\U0001f466"] = ":boy:", - ["\U0001f9e0"] = ":brain:", - ["\U0001f35e"] = ":bread:", - ["\U0001f931\U0001f3fb"] = ":breast_feeding_tone1:", - ["\U0001f931\U0001f3fc"] = ":breast_feeding_tone2:", - ["\U0001f931\U0001f3fd"] = ":breast_feeding_tone3:", - ["\U0001f931\U0001f3fe"] = ":breast_feeding_tone4:", - ["\U0001f931\U0001f3ff"] = ":breast_feeding_tone5:", - ["\U0001f931"] = ":breast_feeding:", - ["\U0001f9f1"] = ":bricks:", - ["\U0001f309"] = ":bridge_at_night:", - ["\U0001f4bc"] = ":briefcase:", - ["\U0001fa72"] = ":briefs:", - ["\U0001f966"] = ":broccoli:", - ["\u26d3\ufe0f\u200d\U0001f4a5"] = ":broken_chain:", - ["\U0001f494"] = ":broken_heart:", - ["\U0001f9f9"] = ":broom:", - ["\U0001f7e4"] = ":brown_circle:", - ["\U0001f90e"] = ":brown_heart:", - ["\U0001f344\u200d\U0001f7eb"] = ":brown_mushroom:", - ["\U0001f7eb"] = ":brown_square:", - ["\U0001f9cb"] = ":bubble_tea:", - ["\U0001fae7"] = ":bubbles:", - ["\U0001faa3"] = ":bucket:", - ["\U0001f41b"] = ":bug:", - ["\U0001f4a1"] = ":bulb:", - ["\U0001f685"] = ":bullettrain_front:", - ["\U0001f684"] = ":bullettrain_side:", - ["\U0001f32f"] = ":burrito:", - ["\U0001f68c"] = ":bus:", - ["\U0001f68f"] = ":busstop:", - ["\U0001f464"] = ":bust_in_silhouette:", - ["\U0001f465"] = ":busts_in_silhouette:", - ["\U0001f9c8"] = ":butter:", - ["\U0001f98b"] = ":butterfly:", - ["\U0001f335"] = ":cactus:", - ["\U0001f370"] = ":cake:", - ["\U0001f5d3\ufe0f"] = ":calendar_spiral:", - ["\U0001f5d3"] = ":calendar_spiral:", - ["\U0001f4c6"] = ":calendar:", - ["\U0001f919\U0001f3fb"] = ":call_me_tone1:", - ["\U0001f919\U0001f3fc"] = ":call_me_tone2:", - ["\U0001f919\U0001f3fd"] = ":call_me_tone3:", - ["\U0001f919\U0001f3fe"] = ":call_me_tone4:", - ["\U0001f919\U0001f3ff"] = ":call_me_tone5:", - ["\U0001f919"] = ":call_me:", - ["\U0001f4f2"] = ":calling:", - ["\U0001f42b"] = ":camel:", - ["\U0001f4f8"] = ":camera_with_flash:", - ["\U0001f4f7"] = ":camera:", - ["\U0001f3d5\ufe0f"] = ":camping:", - ["\U0001f3d5"] = ":camping:", - ["\u264b"] = ":cancer:", - ["\U0001f56f\ufe0f"] = ":candle:", - ["\U0001f56f"] = ":candle:", - ["\U0001f36c"] = ":candy:", - ["\U0001f96b"] = ":canned_food:", - ["\U0001f6f6"] = ":canoe:", - ["\U0001f520"] = ":capital_abcd:", - ["\u2651"] = ":capricorn:", - ["\U0001f5c3\ufe0f"] = ":card_box:", - ["\U0001f5c3"] = ":card_box:", - ["\U0001f4c7"] = ":card_index:", - ["\U0001f3a0"] = ":carousel_horse:", - ["\U0001fa9a"] = ":carpentry_saw:", - ["\U0001f955"] = ":carrot:", - ["\U0001f431"] = ":cat:", - ["\U0001f408"] = ":cat2:", - ["\U0001f4bf"] = ":cd:", - ["\u26d3\ufe0f"] = ":chains:", - ["\u26d3"] = ":chains:", - ["\U0001fa91"] = ":chair:", - ["\U0001f942"] = ":champagne_glass:", - ["\U0001f37e"] = ":champagne:", - ["\U0001f4c9"] = ":chart_with_downwards_trend:", - ["\U0001f4c8"] = ":chart_with_upwards_trend:", - ["\U0001f4b9"] = ":chart:", - ["\U0001f3c1"] = ":checkered_flag:", - ["\U0001f9c0"] = ":cheese:", - ["\U0001f352"] = ":cherries:", - ["\U0001f338"] = ":cherry_blossom:", - ["\u265f\ufe0f"] = ":chess_pawn:", - ["\u265f"] = ":chess_pawn:", - ["\U0001f330"] = ":chestnut:", - ["\U0001f414"] = ":chicken:", - ["\U0001f9d2\U0001f3fb"] = ":child_tone1:", - ["\U0001f9d2\U0001f3fc"] = ":child_tone2:", - ["\U0001f9d2\U0001f3fd"] = ":child_tone3:", - ["\U0001f9d2\U0001f3fe"] = ":child_tone4:", - ["\U0001f9d2\U0001f3ff"] = ":child_tone5:", - ["\U0001f9d2"] = ":child:", - ["\U0001f6b8"] = ":children_crossing:", - ["\U0001f43f\ufe0f"] = ":chipmunk:", - ["\U0001f43f"] = ":chipmunk:", - ["\U0001f36b"] = ":chocolate_bar:", - ["\U0001f962"] = ":chopsticks:", - ["\U0001f384"] = ":christmas_tree:", - ["\u26ea"] = ":church:", - ["\U0001f3a6"] = ":cinema:", - ["\U0001f3aa"] = ":circus_tent:", - ["\U0001f306"] = ":city_dusk:", - ["\U0001f307"] = ":city_sunset:", - ["\U0001f3d9\ufe0f"] = ":cityscape:", - ["\U0001f3d9"] = ":cityscape:", - ["\U0001f191"] = ":cl:", - ["\U0001f44f\U0001f3fb"] = ":clap_tone1:", - ["\U0001f44f\U0001f3fc"] = ":clap_tone2:", - ["\U0001f44f\U0001f3fd"] = ":clap_tone3:", - ["\U0001f44f\U0001f3fe"] = ":clap_tone4:", - ["\U0001f44f\U0001f3ff"] = ":clap_tone5:", - ["\U0001f44f"] = ":clap:", - ["\U0001f3ac"] = ":clapper:", - ["\U0001f3db\ufe0f"] = ":classical_building:", - ["\U0001f3db"] = ":classical_building:", - ["\U0001f4cb"] = ":clipboard:", - ["\U0001f570\ufe0f"] = ":clock:", - ["\U0001f570"] = ":clock:", - ["\U0001f550"] = ":clock1:", - ["\U0001f559"] = ":clock10:", - ["\U0001f565"] = ":clock1030:", - ["\U0001f55a"] = ":clock11:", - ["\U0001f566"] = ":clock1130:", - ["\U0001f55b"] = ":clock12:", - ["\U0001f567"] = ":clock1230:", - ["\U0001f55c"] = ":clock130:", - ["\U0001f551"] = ":clock2:", - ["\U0001f55d"] = ":clock230:", - ["\U0001f552"] = ":clock3:", - ["\U0001f55e"] = ":clock330:", - ["\U0001f553"] = ":clock4:", - ["\U0001f55f"] = ":clock430:", - ["\U0001f554"] = ":clock5:", - ["\U0001f560"] = ":clock530:", - ["\U0001f555"] = ":clock6:", - ["\U0001f561"] = ":clock630:", - ["\U0001f556"] = ":clock7:", - ["\U0001f562"] = ":clock730:", - ["\U0001f557"] = ":clock8:", - ["\U0001f563"] = ":clock830:", - ["\U0001f558"] = ":clock9:", - ["\U0001f564"] = ":clock930:", - ["\U0001f4d5"] = ":closed_book:", - ["\U0001f510"] = ":closed_lock_with_key:", - ["\U0001f302"] = ":closed_umbrella:", - ["\U0001f329\ufe0f"] = ":cloud_lightning:", - ["\U0001f329"] = ":cloud_lightning:", - ["\U0001f327\ufe0f"] = ":cloud_rain:", - ["\U0001f327"] = ":cloud_rain:", - ["\U0001f328\ufe0f"] = ":cloud_snow:", - ["\U0001f328"] = ":cloud_snow:", - ["\U0001f32a\ufe0f"] = ":cloud_tornado:", - ["\U0001f32a"] = ":cloud_tornado:", - ["\u2601\ufe0f"] = ":cloud:", - ["\u2601"] = ":cloud:", - ["\U0001f921"] = ":clown:", - ["\u2663\ufe0f"] = ":clubs:", - ["\u2663"] = ":clubs:", - ["\U0001f9e5"] = ":coat:", - ["\U0001fab3"] = ":cockroach:", - ["\U0001f378"] = ":cocktail:", - ["\U0001f965"] = ":coconut:", - ["\u2615"] = ":coffee:", - ["\u26b0\ufe0f"] = ":coffin:", - ["\u26b0"] = ":coffin:", - ["\U0001fa99"] = ":coin:", - ["\U0001f976"] = ":cold_face:", - ["\U0001f630"] = ":cold_sweat:", - ["\u2604\ufe0f"] = ":comet:", - ["\u2604"] = ":comet:", - ["\U0001f9ed"] = ":compass:", - ["\U0001f5dc\ufe0f"] = ":compression:", - ["\U0001f5dc"] = ":compression:", - ["\U0001f4bb"] = ":computer:", - ["\U0001f38a"] = ":confetti_ball:", - ["\U0001f616"] = ":confounded:", - ["\U0001f615"] = ":confused:", - ["\u3297\ufe0f"] = ":congratulations:", - ["\u3297"] = ":congratulations:", - ["\U0001f3d7\ufe0f"] = ":construction_site:", - ["\U0001f3d7"] = ":construction_site:", - ["\U0001f477\U0001f3fb"] = ":construction_worker_tone1:", - ["\U0001f477\U0001f3fc"] = ":construction_worker_tone2:", - ["\U0001f477\U0001f3fd"] = ":construction_worker_tone3:", - ["\U0001f477\U0001f3fe"] = ":construction_worker_tone4:", - ["\U0001f477\U0001f3ff"] = ":construction_worker_tone5:", - ["\U0001f477"] = ":construction_worker:", - ["\U0001f6a7"] = ":construction:", - ["\U0001f39b\ufe0f"] = ":control_knobs:", - ["\U0001f39b"] = ":control_knobs:", - ["\U0001f3ea"] = ":convenience_store:", - ["\U0001f9d1\U0001f3fb\u200d\U0001f373"] = ":cook_tone1:", - ["\U0001f9d1\U0001f3fc\u200d\U0001f373"] = ":cook_tone2:", - ["\U0001f9d1\U0001f3fd\u200d\U0001f373"] = ":cook_tone3:", - ["\U0001f9d1\U0001f3fe\u200d\U0001f373"] = ":cook_tone4:", - ["\U0001f9d1\U0001f3ff\u200d\U0001f373"] = ":cook_tone5:", - ["\U0001f9d1\u200d\U0001f373"] = ":cook:", - ["\U0001f36a"] = ":cookie:", - ["\U0001f373"] = ":cooking:", - ["\U0001f192"] = ":cool:", - ["\u00a9\ufe0f"] = ":copyright:", - ["\u00a9"] = ":copyright:", - ["\U0001fab8"] = ":coral:", - ["\U0001f33d"] = ":corn:", - ["\U0001f6cb\ufe0f"] = ":couch:", - ["\U0001f6cb"] = ":couch:", - ["\U0001f468\u200d\u2764\ufe0f\u200d\U0001f468"] = ":couple_with_heart_man_man_tone5_tone4:", - ["\U0001f491"] = ":couple_with_heart_tone5:", - ["\U0001f469\u200d\u2764\ufe0f\u200d\U0001f468"] = ":couple_with_heart_woman_man_tone5_tone4:", - ["\U0001f469\u200d\u2764\ufe0f\u200d\U0001f469"] = ":couple_ww:", - ["\U0001f42e"] = ":cow:", - ["\U0001f404"] = ":cow2:", - ["\U0001f920"] = ":cowboy:", - ["\U0001f980"] = ":crab:", - ["\U0001f58d\ufe0f"] = ":crayon:", - ["\U0001f58d"] = ":crayon:", - ["\U0001f4b3"] = ":credit_card:", - ["\U0001f319"] = ":crescent_moon:", - ["\U0001f3cf"] = ":cricket_game:", - ["\U0001f997"] = ":cricket:", - ["\U0001f40a"] = ":crocodile:", - ["\U0001f950"] = ":croissant:", - ["\u271d\ufe0f"] = ":cross:", - ["\u271d"] = ":cross:", - ["\U0001f38c"] = ":crossed_flags:", - ["\u2694\ufe0f"] = ":crossed_swords:", - ["\u2694"] = ":crossed_swords:", - ["\U0001f451"] = ":crown:", - ["\U0001f6f3\ufe0f"] = ":cruise_ship:", - ["\U0001f6f3"] = ":cruise_ship:", - ["\U0001fa7c"] = ":crutch:", - ["\U0001f622"] = ":cry:", - ["\U0001f63f"] = ":crying_cat_face:", - ["\U0001f52e"] = ":crystal_ball:", - ["\U0001f952"] = ":cucumber:", - ["\U0001f964"] = ":cup_with_straw:", - ["\U0001f9c1"] = ":cupcake:", - ["\U0001f498"] = ":cupid:", - ["\U0001f94c"] = ":curling_stone:", - ["\u27b0"] = ":curly_loop:", - ["\U0001f4b1"] = ":currency_exchange:", - ["\U0001f35b"] = ":curry:", - ["\U0001f36e"] = ":custard:", - ["\U0001f6c3"] = ":customs:", - ["\U0001f969"] = ":cut_of_meat:", - ["\U0001f300"] = ":cyclone:", - ["\U0001f5e1\ufe0f"] = ":dagger:", - ["\U0001f5e1"] = ":dagger:", - ["\U0001f483\U0001f3fb"] = ":dancer_tone1:", - ["\U0001f483\U0001f3fc"] = ":dancer_tone2:", - ["\U0001f483\U0001f3fd"] = ":dancer_tone3:", - ["\U0001f483\U0001f3fe"] = ":dancer_tone4:", - ["\U0001f483\U0001f3ff"] = ":dancer_tone5:", - ["\U0001f483"] = ":dancer:", - ["\U0001f361"] = ":dango:", - ["\U0001f576\ufe0f"] = ":dark_sunglasses:", - ["\U0001f576"] = ":dark_sunglasses:", - ["\U0001f3af"] = ":dart:", - ["\U0001f4a8"] = ":dash:", - ["\U0001f4c5"] = ":date:", - ["\U0001f9cf\U0001f3fb\u200d\u2642\ufe0f"] = ":deaf_man_tone1:", - ["\U0001f9cf\U0001f3fc\u200d\u2642\ufe0f"] = ":deaf_man_tone2:", - ["\U0001f9cf\U0001f3fd\u200d\u2642\ufe0f"] = ":deaf_man_tone3:", - ["\U0001f9cf\U0001f3fe\u200d\u2642\ufe0f"] = ":deaf_man_tone4:", - ["\U0001f9cf\U0001f3ff\u200d\u2642\ufe0f"] = ":deaf_man_tone5:", - ["\U0001f9cf\u200d\u2642\ufe0f"] = ":deaf_man:", - ["\U0001f9cf\U0001f3fb"] = ":deaf_person_tone1:", - ["\U0001f9cf\U0001f3fc"] = ":deaf_person_tone2:", - ["\U0001f9cf\U0001f3fd"] = ":deaf_person_tone3:", - ["\U0001f9cf\U0001f3fe"] = ":deaf_person_tone4:", - ["\U0001f9cf\U0001f3ff"] = ":deaf_person_tone5:", - ["\U0001f9cf"] = ":deaf_person:", - ["\U0001f9cf\U0001f3fb\u200d\u2640\ufe0f"] = ":deaf_woman_tone1:", - ["\U0001f9cf\U0001f3fc\u200d\u2640\ufe0f"] = ":deaf_woman_tone2:", - ["\U0001f9cf\U0001f3fd\u200d\u2640\ufe0f"] = ":deaf_woman_tone3:", - ["\U0001f9cf\U0001f3fe\u200d\u2640\ufe0f"] = ":deaf_woman_tone4:", - ["\U0001f9cf\U0001f3ff\u200d\u2640\ufe0f"] = ":deaf_woman_tone5:", - ["\U0001f9cf\u200d\u2640\ufe0f"] = ":deaf_woman:", - ["\U0001f333"] = ":deciduous_tree:", - ["\U0001f98c"] = ":deer:", - ["\U0001f3ec"] = ":department_store:", - ["\U0001f3dc\ufe0f"] = ":desert:", - ["\U0001f3dc"] = ":desert:", - ["\U0001f5a5\ufe0f"] = ":desktop:", - ["\U0001f5a5"] = ":desktop:", - ["\U0001f575\U0001f3fb"] = ":detective_tone1:", - ["\U0001f575\U0001f3fc"] = ":detective_tone2:", - ["\U0001f575\U0001f3fd"] = ":detective_tone3:", - ["\U0001f575\U0001f3fe"] = ":detective_tone4:", - ["\U0001f575\U0001f3ff"] = ":detective_tone5:", - ["\U0001f575\ufe0f"] = ":detective:", - ["\U0001f575"] = ":detective:", - ["\U0001f4a0"] = ":diamond_shape_with_a_dot_inside:", - ["\u2666\ufe0f"] = ":diamonds:", - ["\u2666"] = ":diamonds:", - ["\U0001f625"] = ":disappointed_relieved:", - ["\U0001f61e"] = ":disappointed:", - ["\U0001f978"] = ":disguised_face:", - ["\U0001f5c2\ufe0f"] = ":dividers:", - ["\U0001f5c2"] = ":dividers:", - ["\U0001f93f"] = ":diving_mask:", - ["\U0001fa94"] = ":diya_lamp:", - ["\U0001f635"] = ":dizzy_face:", - ["\U0001f4ab"] = ":dizzy:", - ["\U0001f9ec"] = ":dna:", - ["\U0001f6af"] = ":do_not_litter:", - ["\U0001f9a4"] = ":dodo:", - ["\U0001f436"] = ":dog:", - ["\U0001f415"] = ":dog2:", - ["\U0001f4b5"] = ":dollar:", - ["\U0001f38e"] = ":dolls:", - ["\U0001f42c"] = ":dolphin:", - ["\U0001facf"] = ":donkey:", - ["\U0001f6aa"] = ":door:", - ["\U0001fae5"] = ":dotted_line_face:", - ["\U0001f369"] = ":doughnut:", - ["\U0001f54a\ufe0f"] = ":dove:", - ["\U0001f54a"] = ":dove:", - ["\U0001f432"] = ":dragon_face:", - ["\U0001f409"] = ":dragon:", - ["\U0001f457"] = ":dress:", - ["\U0001f42a"] = ":dromedary_camel:", - ["\U0001f924"] = ":drooling_face:", - ["\U0001fa78"] = ":drop_of_blood:", - ["\U0001f4a7"] = ":droplet:", - ["\U0001f941"] = ":drum:", - ["\U0001f986"] = ":duck:", - ["\U0001f95f"] = ":dumpling:", - ["\U0001f4c0"] = ":dvd:", - ["\U0001f4e7"] = ":e_mail:", - ["\U0001f985"] = ":eagle:", - ["\U0001f33e"] = ":ear_of_rice:", - ["\U0001f442\U0001f3fb"] = ":ear_tone1:", - ["\U0001f442\U0001f3fc"] = ":ear_tone2:", - ["\U0001f442\U0001f3fd"] = ":ear_tone3:", - ["\U0001f442\U0001f3fe"] = ":ear_tone4:", - ["\U0001f442\U0001f3ff"] = ":ear_tone5:", - ["\U0001f9bb\U0001f3fb"] = ":ear_with_hearing_aid_tone1:", - ["\U0001f9bb\U0001f3fc"] = ":ear_with_hearing_aid_tone2:", - ["\U0001f9bb\U0001f3fd"] = ":ear_with_hearing_aid_tone3:", - ["\U0001f9bb\U0001f3fe"] = ":ear_with_hearing_aid_tone4:", - ["\U0001f9bb\U0001f3ff"] = ":ear_with_hearing_aid_tone5:", - ["\U0001f9bb"] = ":ear_with_hearing_aid:", - ["\U0001f442"] = ":ear:", - ["\U0001f30d"] = ":earth_africa:", - ["\U0001f30e"] = ":earth_americas:", - ["\U0001f30f"] = ":earth_asia:", - ["\U0001f95a"] = ":egg:", - ["\U0001f346"] = ":eggplant:", - ["\u2734\ufe0f"] = ":eight_pointed_black_star:", - ["\u2734"] = ":eight_pointed_black_star:", - ["\u2733\ufe0f"] = ":eight_spoked_asterisk:", - ["\u2733"] = ":eight_spoked_asterisk:", - ["\u0038\ufe0f\u20e3"] = ":eight:", - ["\u0038\u20e3"] = ":eight:", - ["\u23cf\ufe0f"] = ":eject:", - ["\u23cf"] = ":eject:", - ["\U0001f50c"] = ":electric_plug:", - ["\U0001f418"] = ":elephant:", - ["\U0001f6d7"] = ":elevator:", - ["\U0001f9dd\U0001f3fb"] = ":elf_tone1:", - ["\U0001f9dd\U0001f3fc"] = ":elf_tone2:", - ["\U0001f9dd\U0001f3fd"] = ":elf_tone3:", - ["\U0001f9dd\U0001f3fe"] = ":elf_tone4:", - ["\U0001f9dd\U0001f3ff"] = ":elf_tone5:", - ["\U0001f9dd"] = ":elf:", - ["\U0001fab9"] = ":empty_nest:", - ["\U0001f51a"] = ":end:", - ["\U0001f3f4\U000e0067\U000e0062\U000e0065\U000e006e\U000e0067\U000e007f"] = ":england:", - ["\U0001f4e9"] = ":envelope_with_arrow:", - ["\u2709\ufe0f"] = ":envelope:", - ["\u2709"] = ":envelope:", - ["\U0001f4b6"] = ":euro:", - ["\U0001f3f0"] = ":european_castle:", - ["\U0001f3e4"] = ":european_post_office:", - ["\U0001f332"] = ":evergreen_tree:", - ["\u2757"] = ":exclamation:", - ["\U0001f92f"] = ":exploding_head:", - ["\U0001f611"] = ":expressionless:", - ["\U0001f441\u200d\U0001f5e8"] = ":eye_in_speech_bubble:", - ["\U0001f441\ufe0f"] = ":eye:", - ["\U0001f441"] = ":eye:", - ["\U0001f453"] = ":eyeglasses:", - ["\U0001f440"] = ":eyes:", - ["\U0001f62e\u200d\U0001f4a8"] = ":face_exhaling:", - ["\U0001f979"] = ":face_holding_back_tears:", - ["\U0001f636\u200d\U0001f32b\ufe0f"] = ":face_in_clouds:", - ["\U0001f92e"] = ":face_vomiting:", - ["\U0001fae4"] = ":face_with_diagonal_mouth:", - ["\U0001f92d"] = ":face_with_hand_over_mouth:", - ["\U0001f9d0"] = ":face_with_monocle:", - ["\U0001fae2"] = ":face_with_open_eyes_and_hand_over_mouth:", - ["\U0001fae3"] = ":face_with_peeking_eye:", - ["\U0001f928"] = ":face_with_raised_eyebrow:", - ["\U0001f635\u200d\U0001f4ab"] = ":face_with_spiral_eyes:", - ["\U0001f92c"] = ":face_with_symbols_over_mouth:", - ["\U0001f9d1\U0001f3fb\u200d\U0001f3ed"] = ":factory_worker_tone1:", - ["\U0001f9d1\U0001f3fc\u200d\U0001f3ed"] = ":factory_worker_tone2:", - ["\U0001f9d1\U0001f3fd\u200d\U0001f3ed"] = ":factory_worker_tone3:", - ["\U0001f9d1\U0001f3fe\u200d\U0001f3ed"] = ":factory_worker_tone4:", - ["\U0001f9d1\U0001f3ff\u200d\U0001f3ed"] = ":factory_worker_tone5:", - ["\U0001f9d1\u200d\U0001f3ed"] = ":factory_worker:", - ["\U0001f3ed"] = ":factory:", - ["\U0001f9da\U0001f3fb"] = ":fairy_tone1:", - ["\U0001f9da\U0001f3fc"] = ":fairy_tone2:", - ["\U0001f9da\U0001f3fd"] = ":fairy_tone3:", - ["\U0001f9da\U0001f3fe"] = ":fairy_tone4:", - ["\U0001f9da\U0001f3ff"] = ":fairy_tone5:", - ["\U0001f9da"] = ":fairy:", - ["\U0001f9c6"] = ":falafel:", - ["\U0001f342"] = ":fallen_leaf:", - ["\U0001f9d1\u200d\U0001f9d1\u200d\U0001f9d2\u200d\U0001f9d2"] = ":family_adult_adult_child_child:", - ["\U0001f9d1\u200d\U0001f9d1\u200d\U0001f9d2"] = ":family_adult_adult_child:", - ["\U0001f9d1\u200d\U0001f9d2\u200d\U0001f9d2"] = ":family_adult_child_child:", - ["\U0001f9d1\u200d\U0001f9d2"] = ":family_adult_child:", - ["\U0001f468\u200d\U0001f466\u200d\U0001f466"] = ":family_man_boy_boy:", - ["\U0001f468\u200d\U0001f466"] = ":family_man_boy:", - ["\U0001f468\u200d\U0001f467\u200d\U0001f466"] = ":family_man_girl_boy:", - ["\U0001f468\u200d\U0001f467\u200d\U0001f467"] = ":family_man_girl_girl:", - ["\U0001f468\u200d\U0001f467"] = ":family_man_girl:", - ["\U0001f468\u200d\U0001f469\u200d\U0001f466"] = ":family_man_woman_boy:", - ["\U0001f468\u200d\U0001f468\u200d\U0001f466"] = ":family_mmb:", - ["\U0001f468\u200d\U0001f468\u200d\U0001f466\u200d\U0001f466"] = ":family_mmbb:", - ["\U0001f468\u200d\U0001f468\u200d\U0001f467"] = ":family_mmg:", - ["\U0001f468\u200d\U0001f468\u200d\U0001f467\u200d\U0001f466"] = ":family_mmgb:", - ["\U0001f468\u200d\U0001f468\u200d\U0001f467\u200d\U0001f467"] = ":family_mmgg:", - ["\U0001f468\u200d\U0001f469\u200d\U0001f466\u200d\U0001f466"] = ":family_mwbb:", - ["\U0001f468\u200d\U0001f469\u200d\U0001f467"] = ":family_mwg:", - ["\U0001f468\u200d\U0001f469\u200d\U0001f467\u200d\U0001f466"] = ":family_mwgb:", - ["\U0001f468\u200d\U0001f469\u200d\U0001f467\u200d\U0001f467"] = ":family_mwgg:", - ["\U0001f469\u200d\U0001f466\u200d\U0001f466"] = ":family_woman_boy_boy:", - ["\U0001f469\u200d\U0001f466"] = ":family_woman_boy:", - ["\U0001f469\u200d\U0001f467\u200d\U0001f466"] = ":family_woman_girl_boy:", - ["\U0001f469\u200d\U0001f467\u200d\U0001f467"] = ":family_woman_girl_girl:", - ["\U0001f469\u200d\U0001f467"] = ":family_woman_girl:", - ["\U0001f469\u200d\U0001f469\u200d\U0001f466"] = ":family_wwb:", - ["\U0001f469\u200d\U0001f469\u200d\U0001f466\u200d\U0001f466"] = ":family_wwbb:", - ["\U0001f469\u200d\U0001f469\u200d\U0001f467"] = ":family_wwg:", - ["\U0001f469\u200d\U0001f469\u200d\U0001f467\u200d\U0001f466"] = ":family_wwgb:", - ["\U0001f469\u200d\U0001f469\u200d\U0001f467\u200d\U0001f467"] = ":family_wwgg:", - ["\U0001f46a"] = ":family:", - ["\U0001f9d1\U0001f3fb\u200d\U0001f33e"] = ":farmer_tone1:", - ["\U0001f9d1\U0001f3fc\u200d\U0001f33e"] = ":farmer_tone2:", - ["\U0001f9d1\U0001f3fd\u200d\U0001f33e"] = ":farmer_tone3:", - ["\U0001f9d1\U0001f3fe\u200d\U0001f33e"] = ":farmer_tone4:", - ["\U0001f9d1\U0001f3ff\u200d\U0001f33e"] = ":farmer_tone5:", - ["\U0001f9d1\u200d\U0001f33e"] = ":farmer:", - ["\u23e9"] = ":fast_forward:", - ["\U0001f4e0"] = ":fax:", - ["\U0001f628"] = ":fearful:", - ["\U0001fab6"] = ":feather:", - ["\U0001f43e"] = ":feet:", - ["\u2640\ufe0f"] = ":female_sign:", - ["\u2640"] = ":female_sign:", - ["\U0001f3a1"] = ":ferris_wheel:", - ["\u26f4\ufe0f"] = ":ferry:", - ["\u26f4"] = ":ferry:", - ["\U0001f3d1"] = ":field_hockey:", - ["\U0001f5c4\ufe0f"] = ":file_cabinet:", - ["\U0001f5c4"] = ":file_cabinet:", - ["\U0001f4c1"] = ":file_folder:", - ["\U0001f39e\ufe0f"] = ":film_frames:", - ["\U0001f39e"] = ":film_frames:", - ["\U0001f91e\U0001f3fb"] = ":fingers_crossed_tone1:", - ["\U0001f91e\U0001f3fc"] = ":fingers_crossed_tone2:", - ["\U0001f91e\U0001f3fd"] = ":fingers_crossed_tone3:", - ["\U0001f91e\U0001f3fe"] = ":fingers_crossed_tone4:", - ["\U0001f91e\U0001f3ff"] = ":fingers_crossed_tone5:", - ["\U0001f91e"] = ":fingers_crossed:", - ["\U0001f692"] = ":fire_engine:", - ["\U0001f9ef"] = ":fire_extinguisher:", - ["\U0001f525"] = ":fire:", - ["\U0001f9e8"] = ":firecracker:", - ["\U0001f9d1\U0001f3fb\u200d\U0001f692"] = ":firefighter_tone1:", - ["\U0001f9d1\U0001f3fc\u200d\U0001f692"] = ":firefighter_tone2:", - ["\U0001f9d1\U0001f3fd\u200d\U0001f692"] = ":firefighter_tone3:", - ["\U0001f9d1\U0001f3fe\u200d\U0001f692"] = ":firefighter_tone4:", - ["\U0001f9d1\U0001f3ff\u200d\U0001f692"] = ":firefighter_tone5:", - ["\U0001f9d1\u200d\U0001f692"] = ":firefighter:", - ["\U0001f386"] = ":fireworks:", - ["\U0001f947"] = ":first_place:", - ["\U0001f31b"] = ":first_quarter_moon_with_face:", - ["\U0001f313"] = ":first_quarter_moon:", - ["\U0001f365"] = ":fish_cake:", - ["\U0001f41f"] = ":fish:", - ["\U0001f3a3"] = ":fishing_pole_and_fish:", - ["\u270a\U0001f3fb"] = ":fist_tone1:", - ["\u270a\U0001f3fc"] = ":fist_tone2:", - ["\u270a\U0001f3fd"] = ":fist_tone3:", - ["\u270a\U0001f3fe"] = ":fist_tone4:", - ["\u270a\U0001f3ff"] = ":fist_tone5:", - ["\u270a"] = ":fist:", - ["\u0035\ufe0f\u20e3"] = ":five:", - ["\u0035\u20e3"] = ":five:", - ["\U0001f1e6\U0001f1e8"] = ":flag_ac:", - ["\U0001f1e6\U0001f1e9"] = ":flag_ad:", - ["\U0001f1e6\U0001f1ea"] = ":flag_ae:", - ["\U0001f1e6\U0001f1eb"] = ":flag_af:", - ["\U0001f1e6\U0001f1ec"] = ":flag_ag:", - ["\U0001f1e6\U0001f1ee"] = ":flag_ai:", - ["\U0001f1e6\U0001f1f1"] = ":flag_al:", - ["\U0001f1e6\U0001f1f2"] = ":flag_am:", - ["\U0001f1e6\U0001f1f4"] = ":flag_ao:", - ["\U0001f1e6\U0001f1f6"] = ":flag_aq:", - ["\U0001f1e6\U0001f1f7"] = ":flag_ar:", - ["\U0001f1e6\U0001f1f8"] = ":flag_as:", - ["\U0001f1e6\U0001f1f9"] = ":flag_at:", - ["\U0001f1e6\U0001f1fa"] = ":flag_au:", - ["\U0001f1e6\U0001f1fc"] = ":flag_aw:", - ["\U0001f1e6\U0001f1fd"] = ":flag_ax:", - ["\U0001f1e6\U0001f1ff"] = ":flag_az:", - ["\U0001f1e7\U0001f1e6"] = ":flag_ba:", - ["\U0001f1e7\U0001f1e7"] = ":flag_bb:", - ["\U0001f1e7\U0001f1e9"] = ":flag_bd:", - ["\U0001f1e7\U0001f1ea"] = ":flag_be:", - ["\U0001f1e7\U0001f1eb"] = ":flag_bf:", - ["\U0001f1e7\U0001f1ec"] = ":flag_bg:", - ["\U0001f1e7\U0001f1ed"] = ":flag_bh:", - ["\U0001f1e7\U0001f1ee"] = ":flag_bi:", - ["\U0001f1e7\U0001f1ef"] = ":flag_bj:", - ["\U0001f1e7\U0001f1f1"] = ":flag_bl:", - ["\U0001f3f4"] = ":flag_black:", - ["\U0001f1e7\U0001f1f2"] = ":flag_bm:", - ["\U0001f1e7\U0001f1f3"] = ":flag_bn:", - ["\U0001f1e7\U0001f1f4"] = ":flag_bo:", - ["\U0001f1e7\U0001f1f6"] = ":flag_bq:", - ["\U0001f1e7\U0001f1f7"] = ":flag_br:", - ["\U0001f1e7\U0001f1f8"] = ":flag_bs:", - ["\U0001f1e7\U0001f1f9"] = ":flag_bt:", - ["\U0001f1e7\U0001f1fb"] = ":flag_bv:", - ["\U0001f1e7\U0001f1fc"] = ":flag_bw:", - ["\U0001f1e7\U0001f1fe"] = ":flag_by:", - ["\U0001f1e7\U0001f1ff"] = ":flag_bz:", - ["\U0001f1e8\U0001f1e6"] = ":flag_ca:", - ["\U0001f1e8\U0001f1e8"] = ":flag_cc:", - ["\U0001f1e8\U0001f1e9"] = ":flag_cd:", - ["\U0001f1e8\U0001f1eb"] = ":flag_cf:", - ["\U0001f1e8\U0001f1ec"] = ":flag_cg:", - ["\U0001f1e8\U0001f1ed"] = ":flag_ch:", - ["\U0001f1e8\U0001f1ee"] = ":flag_ci:", - ["\U0001f1e8\U0001f1f0"] = ":flag_ck:", - ["\U0001f1e8\U0001f1f1"] = ":flag_cl:", - ["\U0001f1e8\U0001f1f2"] = ":flag_cm:", - ["\U0001f1e8\U0001f1f3"] = ":flag_cn:", - ["\U0001f1e8\U0001f1f4"] = ":flag_co:", - ["\U0001f1e8\U0001f1f5"] = ":flag_cp:", - ["\U0001f1e8\U0001f1f7"] = ":flag_cr:", - ["\U0001f1e8\U0001f1fa"] = ":flag_cu:", - ["\U0001f1e8\U0001f1fb"] = ":flag_cv:", - ["\U0001f1e8\U0001f1fc"] = ":flag_cw:", - ["\U0001f1e8\U0001f1fd"] = ":flag_cx:", - ["\U0001f1e8\U0001f1fe"] = ":flag_cy:", - ["\U0001f1e8\U0001f1ff"] = ":flag_cz:", - ["\U0001f1e9\U0001f1ea"] = ":flag_de:", - ["\U0001f1e9\U0001f1ec"] = ":flag_dg:", - ["\U0001f1e9\U0001f1ef"] = ":flag_dj:", - ["\U0001f1e9\U0001f1f0"] = ":flag_dk:", - ["\U0001f1e9\U0001f1f2"] = ":flag_dm:", - ["\U0001f1e9\U0001f1f4"] = ":flag_do:", - ["\U0001f1e9\U0001f1ff"] = ":flag_dz:", - ["\U0001f1ea\U0001f1e6"] = ":flag_ea:", - ["\U0001f1ea\U0001f1e8"] = ":flag_ec:", - ["\U0001f1ea\U0001f1ea"] = ":flag_ee:", - ["\U0001f1ea\U0001f1ec"] = ":flag_eg:", - ["\U0001f1ea\U0001f1ed"] = ":flag_eh:", - ["\U0001f1ea\U0001f1f7"] = ":flag_er:", - ["\U0001f1ea\U0001f1f8"] = ":flag_es:", - ["\U0001f1ea\U0001f1f9"] = ":flag_et:", - ["\U0001f1ea\U0001f1fa"] = ":flag_eu:", - ["\U0001f1eb\U0001f1ee"] = ":flag_fi:", - ["\U0001f1eb\U0001f1ef"] = ":flag_fj:", - ["\U0001f1eb\U0001f1f0"] = ":flag_fk:", - ["\U0001f1eb\U0001f1f2"] = ":flag_fm:", - ["\U0001f1eb\U0001f1f4"] = ":flag_fo:", - ["\U0001f1eb\U0001f1f7"] = ":flag_fr:", - ["\U0001f1ec\U0001f1e6"] = ":flag_ga:", - ["\U0001f1ec\U0001f1e7"] = ":flag_gb:", - ["\U0001f1ec\U0001f1e9"] = ":flag_gd:", - ["\U0001f1ec\U0001f1ea"] = ":flag_ge:", - ["\U0001f1ec\U0001f1eb"] = ":flag_gf:", - ["\U0001f1ec\U0001f1ec"] = ":flag_gg:", - ["\U0001f1ec\U0001f1ed"] = ":flag_gh:", - ["\U0001f1ec\U0001f1ee"] = ":flag_gi:", - ["\U0001f1ec\U0001f1f1"] = ":flag_gl:", - ["\U0001f1ec\U0001f1f2"] = ":flag_gm:", - ["\U0001f1ec\U0001f1f3"] = ":flag_gn:", - ["\U0001f1ec\U0001f1f5"] = ":flag_gp:", - ["\U0001f1ec\U0001f1f6"] = ":flag_gq:", - ["\U0001f1ec\U0001f1f7"] = ":flag_gr:", - ["\U0001f1ec\U0001f1f8"] = ":flag_gs:", - ["\U0001f1ec\U0001f1f9"] = ":flag_gt:", - ["\U0001f1ec\U0001f1fa"] = ":flag_gu:", - ["\U0001f1ec\U0001f1fc"] = ":flag_gw:", - ["\U0001f1ec\U0001f1fe"] = ":flag_gy:", - ["\U0001f1ed\U0001f1f0"] = ":flag_hk:", - ["\U0001f1ed\U0001f1f2"] = ":flag_hm:", - ["\U0001f1ed\U0001f1f3"] = ":flag_hn:", - ["\U0001f1ed\U0001f1f7"] = ":flag_hr:", - ["\U0001f1ed\U0001f1f9"] = ":flag_ht:", - ["\U0001f1ed\U0001f1fa"] = ":flag_hu:", - ["\U0001f1ee\U0001f1e8"] = ":flag_ic:", - ["\U0001f1ee\U0001f1e9"] = ":flag_id:", - ["\U0001f1ee\U0001f1ea"] = ":flag_ie:", - ["\U0001f1ee\U0001f1f1"] = ":flag_il:", - ["\U0001f1ee\U0001f1f2"] = ":flag_im:", - ["\U0001f1ee\U0001f1f3"] = ":flag_in:", - ["\U0001f1ee\U0001f1f4"] = ":flag_io:", - ["\U0001f1ee\U0001f1f6"] = ":flag_iq:", - ["\U0001f1ee\U0001f1f7"] = ":flag_ir:", - ["\U0001f1ee\U0001f1f8"] = ":flag_is:", - ["\U0001f1ee\U0001f1f9"] = ":flag_it:", - ["\U0001f1ef\U0001f1ea"] = ":flag_je:", - ["\U0001f1ef\U0001f1f2"] = ":flag_jm:", - ["\U0001f1ef\U0001f1f4"] = ":flag_jo:", - ["\U0001f1ef\U0001f1f5"] = ":flag_jp:", - ["\U0001f1f0\U0001f1ea"] = ":flag_ke:", - ["\U0001f1f0\U0001f1ec"] = ":flag_kg:", - ["\U0001f1f0\U0001f1ed"] = ":flag_kh:", - ["\U0001f1f0\U0001f1ee"] = ":flag_ki:", - ["\U0001f1f0\U0001f1f2"] = ":flag_km:", - ["\U0001f1f0\U0001f1f3"] = ":flag_kn:", - ["\U0001f1f0\U0001f1f5"] = ":flag_kp:", - ["\U0001f1f0\U0001f1f7"] = ":flag_kr:", - ["\U0001f1f0\U0001f1fc"] = ":flag_kw:", - ["\U0001f1f0\U0001f1fe"] = ":flag_ky:", - ["\U0001f1f0\U0001f1ff"] = ":flag_kz:", - ["\U0001f1f1\U0001f1e6"] = ":flag_la:", - ["\U0001f1f1\U0001f1e7"] = ":flag_lb:", - ["\U0001f1f1\U0001f1e8"] = ":flag_lc:", - ["\U0001f1f1\U0001f1ee"] = ":flag_li:", - ["\U0001f1f1\U0001f1f0"] = ":flag_lk:", - ["\U0001f1f1\U0001f1f7"] = ":flag_lr:", - ["\U0001f1f1\U0001f1f8"] = ":flag_ls:", - ["\U0001f1f1\U0001f1f9"] = ":flag_lt:", - ["\U0001f1f1\U0001f1fa"] = ":flag_lu:", - ["\U0001f1f1\U0001f1fb"] = ":flag_lv:", - ["\U0001f1f1\U0001f1fe"] = ":flag_ly:", - ["\U0001f1f2\U0001f1e6"] = ":flag_ma:", - ["\U0001f1f2\U0001f1e8"] = ":flag_mc:", - ["\U0001f1f2\U0001f1e9"] = ":flag_md:", - ["\U0001f1f2\U0001f1ea"] = ":flag_me:", - ["\U0001f1f2\U0001f1eb"] = ":flag_mf:", - ["\U0001f1f2\U0001f1ec"] = ":flag_mg:", - ["\U0001f1f2\U0001f1ed"] = ":flag_mh:", - ["\U0001f1f2\U0001f1f0"] = ":flag_mk:", - ["\U0001f1f2\U0001f1f1"] = ":flag_ml:", - ["\U0001f1f2\U0001f1f2"] = ":flag_mm:", - ["\U0001f1f2\U0001f1f3"] = ":flag_mn:", - ["\U0001f1f2\U0001f1f4"] = ":flag_mo:", - ["\U0001f1f2\U0001f1f5"] = ":flag_mp:", - ["\U0001f1f2\U0001f1f6"] = ":flag_mq:", - ["\U0001f1f2\U0001f1f7"] = ":flag_mr:", - ["\U0001f1f2\U0001f1f8"] = ":flag_ms:", - ["\U0001f1f2\U0001f1f9"] = ":flag_mt:", - ["\U0001f1f2\U0001f1fa"] = ":flag_mu:", - ["\U0001f1f2\U0001f1fb"] = ":flag_mv:", - ["\U0001f1f2\U0001f1fc"] = ":flag_mw:", - ["\U0001f1f2\U0001f1fd"] = ":flag_mx:", - ["\U0001f1f2\U0001f1fe"] = ":flag_my:", - ["\U0001f1f2\U0001f1ff"] = ":flag_mz:", - ["\U0001f1f3\U0001f1e6"] = ":flag_na:", - ["\U0001f1f3\U0001f1e8"] = ":flag_nc:", - ["\U0001f1f3\U0001f1ea"] = ":flag_ne:", - ["\U0001f1f3\U0001f1eb"] = ":flag_nf:", - ["\U0001f1f3\U0001f1ec"] = ":flag_ng:", - ["\U0001f1f3\U0001f1ee"] = ":flag_ni:", - ["\U0001f1f3\U0001f1f1"] = ":flag_nl:", - ["\U0001f1f3\U0001f1f4"] = ":flag_no:", - ["\U0001f1f3\U0001f1f5"] = ":flag_np:", - ["\U0001f1f3\U0001f1f7"] = ":flag_nr:", - ["\U0001f1f3\U0001f1fa"] = ":flag_nu:", - ["\U0001f1f3\U0001f1ff"] = ":flag_nz:", - ["\U0001f1f4\U0001f1f2"] = ":flag_om:", - ["\U0001f1f5\U0001f1e6"] = ":flag_pa:", - ["\U0001f1f5\U0001f1ea"] = ":flag_pe:", - ["\U0001f1f5\U0001f1eb"] = ":flag_pf:", - ["\U0001f1f5\U0001f1ec"] = ":flag_pg:", - ["\U0001f1f5\U0001f1ed"] = ":flag_ph:", - ["\U0001f1f5\U0001f1f0"] = ":flag_pk:", - ["\U0001f1f5\U0001f1f1"] = ":flag_pl:", - ["\U0001f1f5\U0001f1f2"] = ":flag_pm:", - ["\U0001f1f5\U0001f1f3"] = ":flag_pn:", - ["\U0001f1f5\U0001f1f7"] = ":flag_pr:", - ["\U0001f1f5\U0001f1f8"] = ":flag_ps:", - ["\U0001f1f5\U0001f1f9"] = ":flag_pt:", - ["\U0001f1f5\U0001f1fc"] = ":flag_pw:", - ["\U0001f1f5\U0001f1fe"] = ":flag_py:", - ["\U0001f1f6\U0001f1e6"] = ":flag_qa:", - ["\U0001f1f7\U0001f1ea"] = ":flag_re:", - ["\U0001f1f7\U0001f1f4"] = ":flag_ro:", - ["\U0001f1f7\U0001f1f8"] = ":flag_rs:", - ["\U0001f1f7\U0001f1fa"] = ":flag_ru:", - ["\U0001f1f7\U0001f1fc"] = ":flag_rw:", - ["\U0001f1f8\U0001f1e6"] = ":flag_sa:", - ["\U0001f1f8\U0001f1e7"] = ":flag_sb:", - ["\U0001f1f8\U0001f1e8"] = ":flag_sc:", - ["\U0001f1f8\U0001f1e9"] = ":flag_sd:", - ["\U0001f1f8\U0001f1ea"] = ":flag_se:", - ["\U0001f1f8\U0001f1ec"] = ":flag_sg:", - ["\U0001f1f8\U0001f1ed"] = ":flag_sh:", - ["\U0001f1f8\U0001f1ee"] = ":flag_si:", - ["\U0001f1f8\U0001f1ef"] = ":flag_sj:", - ["\U0001f1f8\U0001f1f0"] = ":flag_sk:", - ["\U0001f1f8\U0001f1f1"] = ":flag_sl:", - ["\U0001f1f8\U0001f1f2"] = ":flag_sm:", - ["\U0001f1f8\U0001f1f3"] = ":flag_sn:", - ["\U0001f1f8\U0001f1f4"] = ":flag_so:", - ["\U0001f1f8\U0001f1f7"] = ":flag_sr:", - ["\U0001f1f8\U0001f1f8"] = ":flag_ss:", - ["\U0001f1f8\U0001f1f9"] = ":flag_st:", - ["\U0001f1f8\U0001f1fb"] = ":flag_sv:", - ["\U0001f1f8\U0001f1fd"] = ":flag_sx:", - ["\U0001f1f8\U0001f1fe"] = ":flag_sy:", - ["\U0001f1f8\U0001f1ff"] = ":flag_sz:", - ["\U0001f1f9\U0001f1e6"] = ":flag_ta:", - ["\U0001f1f9\U0001f1e8"] = ":flag_tc:", - ["\U0001f1f9\U0001f1e9"] = ":flag_td:", - ["\U0001f1f9\U0001f1eb"] = ":flag_tf:", - ["\U0001f1f9\U0001f1ec"] = ":flag_tg:", - ["\U0001f1f9\U0001f1ed"] = ":flag_th:", - ["\U0001f1f9\U0001f1ef"] = ":flag_tj:", - ["\U0001f1f9\U0001f1f0"] = ":flag_tk:", - ["\U0001f1f9\U0001f1f1"] = ":flag_tl:", - ["\U0001f1f9\U0001f1f2"] = ":flag_tm:", - ["\U0001f1f9\U0001f1f3"] = ":flag_tn:", - ["\U0001f1f9\U0001f1f4"] = ":flag_to:", - ["\U0001f1f9\U0001f1f7"] = ":flag_tr:", - ["\U0001f1f9\U0001f1f9"] = ":flag_tt:", - ["\U0001f1f9\U0001f1fb"] = ":flag_tv:", - ["\U0001f1f9\U0001f1fc"] = ":flag_tw:", - ["\U0001f1f9\U0001f1ff"] = ":flag_tz:", - ["\U0001f1fa\U0001f1e6"] = ":flag_ua:", - ["\U0001f1fa\U0001f1ec"] = ":flag_ug:", - ["\U0001f1fa\U0001f1f2"] = ":flag_um:", - ["\U0001f1fa\U0001f1f8"] = ":flag_us:", - ["\U0001f1fa\U0001f1fe"] = ":flag_uy:", - ["\U0001f1fa\U0001f1ff"] = ":flag_uz:", - ["\U0001f1fb\U0001f1e6"] = ":flag_va:", - ["\U0001f1fb\U0001f1e8"] = ":flag_vc:", - ["\U0001f1fb\U0001f1ea"] = ":flag_ve:", - ["\U0001f1fb\U0001f1ec"] = ":flag_vg:", - ["\U0001f1fb\U0001f1ee"] = ":flag_vi:", - ["\U0001f1fb\U0001f1f3"] = ":flag_vn:", - ["\U0001f1fb\U0001f1fa"] = ":flag_vu:", - ["\U0001f1fc\U0001f1eb"] = ":flag_wf:", - ["\U0001f3f3\ufe0f"] = ":flag_white:", - ["\U0001f3f3"] = ":flag_white:", - ["\U0001f1fc\U0001f1f8"] = ":flag_ws:", - ["\U0001f1fd\U0001f1f0"] = ":flag_xk:", - ["\U0001f1fe\U0001f1ea"] = ":flag_ye:", - ["\U0001f1fe\U0001f1f9"] = ":flag_yt:", - ["\U0001f1ff\U0001f1e6"] = ":flag_za:", - ["\U0001f1ff\U0001f1f2"] = ":flag_zm:", - ["\U0001f1ff\U0001f1fc"] = ":flag_zw:", - ["\U0001f38f"] = ":flags:", - ["\U0001f9a9"] = ":flamingo:", - ["\U0001f526"] = ":flashlight:", - ["\U0001fad3"] = ":flatbread:", - ["\u269c\ufe0f"] = ":fleur_de_lis:", - ["\u269c"] = ":fleur_de_lis:", - ["\U0001f4be"] = ":floppy_disk:", - ["\U0001f3b4"] = ":flower_playing_cards:", - ["\U0001f633"] = ":flushed:", - ["\U0001fa88"] = ":flute:", - ["\U0001fab0"] = ":fly:", - ["\U0001f94f"] = ":flying_disc:", - ["\U0001f6f8"] = ":flying_saucer:", - ["\U0001f32b\ufe0f"] = ":fog:", - ["\U0001f32b"] = ":fog:", - ["\U0001f301"] = ":foggy:", - ["\U0001faad"] = ":folding_hand_fan:", - ["\U0001fad5"] = ":fondue:", - ["\U0001f9b6\U0001f3fb"] = ":foot_tone1:", - ["\U0001f9b6\U0001f3fc"] = ":foot_tone2:", - ["\U0001f9b6\U0001f3fd"] = ":foot_tone3:", - ["\U0001f9b6\U0001f3fe"] = ":foot_tone4:", - ["\U0001f9b6\U0001f3ff"] = ":foot_tone5:", - ["\U0001f9b6"] = ":foot:", - ["\U0001f3c8"] = ":football:", - ["\U0001f463"] = ":footprints:", - ["\U0001f374"] = ":fork_and_knife:", - ["\U0001f37d\ufe0f"] = ":fork_knife_plate:", - ["\U0001f37d"] = ":fork_knife_plate:", - ["\U0001f960"] = ":fortune_cookie:", - ["\u26f2"] = ":fountain:", - ["\U0001f340"] = ":four_leaf_clover:", - ["\u0034\ufe0f\u20e3"] = ":four:", - ["\u0034\u20e3"] = ":four:", - ["\U0001f98a"] = ":fox:", - ["\U0001f5bc\ufe0f"] = ":frame_photo:", - ["\U0001f5bc"] = ":frame_photo:", - ["\U0001f193"] = ":free:", - ["\U0001f956"] = ":french_bread:", - ["\U0001f364"] = ":fried_shrimp:", - ["\U0001f35f"] = ":fries:", - ["\U0001f438"] = ":frog:", - ["\U0001f626"] = ":frowning:", - ["\u2639\ufe0f"] = ":frowning2:", - ["\u2639"] = ":frowning2:", - ["\u26fd"] = ":fuelpump:", - ["\U0001f31d"] = ":full_moon_with_face:", - ["\U0001f315"] = ":full_moon:", - ["\U0001f3b2"] = ":game_die:", - ["\U0001f9c4"] = ":garlic:", - ["\u2699\ufe0f"] = ":gear:", - ["\u2699"] = ":gear:", - ["\U0001f48e"] = ":gem:", - ["\u264a"] = ":gemini:", - ["\U0001f9de"] = ":genie:", - ["\U0001f47b"] = ":ghost:", - ["\U0001f49d"] = ":gift_heart:", - ["\U0001f381"] = ":gift:", - ["\U0001fada"] = ":ginger_root:", - ["\U0001f992"] = ":giraffe:", - ["\U0001f467\U0001f3fb"] = ":girl_tone1:", - ["\U0001f467\U0001f3fc"] = ":girl_tone2:", - ["\U0001f467\U0001f3fd"] = ":girl_tone3:", - ["\U0001f467\U0001f3fe"] = ":girl_tone4:", - ["\U0001f467\U0001f3ff"] = ":girl_tone5:", - ["\U0001f467"] = ":girl:", - ["\U0001f310"] = ":globe_with_meridians:", - ["\U0001f9e4"] = ":gloves:", - ["\U0001f945"] = ":goal:", - ["\U0001f410"] = ":goat:", - ["\U0001f97d"] = ":goggles:", - ["\u26f3"] = ":golf:", - ["\U0001fabf"] = ":goose:", - ["\U0001f98d"] = ":gorilla:", - ["\U0001f347"] = ":grapes:", - ["\U0001f34f"] = ":green_apple:", - ["\U0001f4d7"] = ":green_book:", - ["\U0001f7e2"] = ":green_circle:", - ["\U0001f49a"] = ":green_heart:", - ["\U0001f7e9"] = ":green_square:", - ["\u2755"] = ":grey_exclamation:", - ["\U0001fa76"] = ":grey_heart:", - ["\u2754"] = ":grey_question:", - ["\U0001f62c"] = ":grimacing:", - ["\U0001f601"] = ":grin:", - ["\U0001f600"] = ":grinning:", - ["\U0001f482\U0001f3fb"] = ":guard_tone1:", - ["\U0001f482\U0001f3fc"] = ":guard_tone2:", - ["\U0001f482\U0001f3fd"] = ":guard_tone3:", - ["\U0001f482\U0001f3fe"] = ":guard_tone4:", - ["\U0001f482\U0001f3ff"] = ":guard_tone5:", - ["\U0001f482"] = ":guard:", - ["\U0001f9ae"] = ":guide_dog:", - ["\U0001f3b8"] = ":guitar:", - ["\U0001f52b"] = ":gun:", - ["\U0001faae"] = ":hair_pick:", - ["\U0001f354"] = ":hamburger:", - ["\u2692\ufe0f"] = ":hammer_pick:", - ["\u2692"] = ":hammer_pick:", - ["\U0001f528"] = ":hammer:", - ["\U0001faac"] = ":hamsa:", - ["\U0001f439"] = ":hamster:", - ["\U0001f590\U0001f3fb"] = ":hand_splayed_tone1:", - ["\U0001f590\U0001f3fc"] = ":hand_splayed_tone2:", - ["\U0001f590\U0001f3fd"] = ":hand_splayed_tone3:", - ["\U0001f590\U0001f3fe"] = ":hand_splayed_tone4:", - ["\U0001f590\U0001f3ff"] = ":hand_splayed_tone5:", - ["\U0001f590\ufe0f"] = ":hand_splayed:", - ["\U0001f590"] = ":hand_splayed:", - ["\U0001faf0\U0001f3fb"] = ":hand_with_index_finger_and_thumb_crossed_tone1:", - ["\U0001faf0\U0001f3fc"] = ":hand_with_index_finger_and_thumb_crossed_tone2:", - ["\U0001faf0\U0001f3fd"] = ":hand_with_index_finger_and_thumb_crossed_tone3:", - ["\U0001faf0\U0001f3fe"] = ":hand_with_index_finger_and_thumb_crossed_tone4:", - ["\U0001faf0\U0001f3ff"] = ":hand_with_index_finger_and_thumb_crossed_tone5:", - ["\U0001faf0"] = ":hand_with_index_finger_and_thumb_crossed:", - ["\U0001f45c"] = ":handbag:", - ["\U0001f91d"] = ":handshake_tone5_tone4:", - ["\u0023\ufe0f\u20e3"] = ":hash:", - ["\u0023\u20e3"] = ":hash:", - ["\U0001f425"] = ":hatched_chick:", - ["\U0001f423"] = ":hatching_chick:", - ["\U0001f915"] = ":head_bandage:", - ["\U0001f642\u200d\u2194\ufe0f"] = ":head_shaking_horizontally:", - ["\U0001f642\u200d\u2195\ufe0f"] = ":head_shaking_vertically:", - ["\U0001f3a7"] = ":headphones:", - ["\U0001faa6"] = ":headstone:", - ["\U0001f9d1\U0001f3fb\u200d\u2695\ufe0f"] = ":health_worker_tone1:", - ["\U0001f9d1\U0001f3fc\u200d\u2695\ufe0f"] = ":health_worker_tone2:", - ["\U0001f9d1\U0001f3fd\u200d\u2695\ufe0f"] = ":health_worker_tone3:", - ["\U0001f9d1\U0001f3fe\u200d\u2695\ufe0f"] = ":health_worker_tone4:", - ["\U0001f9d1\U0001f3ff\u200d\u2695\ufe0f"] = ":health_worker_tone5:", - ["\U0001f9d1\u200d\u2695\ufe0f"] = ":health_worker:", - ["\U0001f649"] = ":hear_no_evil:", - ["\U0001f49f"] = ":heart_decoration:", - ["\u2763\ufe0f"] = ":heart_exclamation:", - ["\u2763"] = ":heart_exclamation:", - ["\U0001f63b"] = ":heart_eyes_cat:", - ["\U0001f60d"] = ":heart_eyes:", - ["\U0001faf6\U0001f3fb"] = ":heart_hands_tone1:", - ["\U0001faf6\U0001f3fc"] = ":heart_hands_tone2:", - ["\U0001faf6\U0001f3fd"] = ":heart_hands_tone3:", - ["\U0001faf6\U0001f3fe"] = ":heart_hands_tone4:", - ["\U0001faf6\U0001f3ff"] = ":heart_hands_tone5:", - ["\U0001faf6"] = ":heart_hands:", - ["\u2764\ufe0f\u200d\U0001f525"] = ":heart_on_fire:", - ["\u2764\ufe0f"] = ":heart:", - ["\u2764"] = ":heart:", - ["\U0001f493"] = ":heartbeat:", - ["\U0001f497"] = ":heartpulse:", - ["\u2665\ufe0f"] = ":hearts:", - ["\u2665"] = ":hearts:", - ["\u2714\ufe0f"] = ":heavy_check_mark:", - ["\u2714"] = ":heavy_check_mark:", - ["\u2797"] = ":heavy_division_sign:", - ["\U0001f4b2"] = ":heavy_dollar_sign:", - ["\U0001f7f0"] = ":heavy_equals_sign:", - ["\u2796"] = ":heavy_minus_sign:", - ["\u2716\ufe0f"] = ":heavy_multiplication_x:", - ["\u2716"] = ":heavy_multiplication_x:", - ["\u2795"] = ":heavy_plus_sign:", - ["\U0001f994"] = ":hedgehog:", - ["\U0001f681"] = ":helicopter:", - ["\u26d1\ufe0f"] = ":helmet_with_cross:", - ["\u26d1"] = ":helmet_with_cross:", - ["\U0001f33f"] = ":herb:", - ["\U0001f33a"] = ":hibiscus:", - ["\U0001f506"] = ":high_brightness:", - ["\U0001f460"] = ":high_heel:", - ["\U0001f97e"] = ":hiking_boot:", - ["\U0001f6d5"] = ":hindu_temple:", - ["\U0001f99b"] = ":hippopotamus:", - ["\U0001f3d2"] = ":hockey:", - ["\U0001f573\ufe0f"] = ":hole:", - ["\U0001f573"] = ":hole:", - ["\U0001f3d8\ufe0f"] = ":homes:", - ["\U0001f3d8"] = ":homes:", - ["\U0001f36f"] = ":honey_pot:", - ["\U0001fa9d"] = ":hook:", - ["\U0001f3c7\U0001f3fb"] = ":horse_racing_tone1:", - ["\U0001f3c7\U0001f3fc"] = ":horse_racing_tone2:", - ["\U0001f3c7\U0001f3fd"] = ":horse_racing_tone3:", - ["\U0001f3c7\U0001f3fe"] = ":horse_racing_tone4:", - ["\U0001f3c7\U0001f3ff"] = ":horse_racing_tone5:", - ["\U0001f3c7"] = ":horse_racing:", - ["\U0001f434"] = ":horse:", - ["\U0001f3e5"] = ":hospital:", - ["\U0001f975"] = ":hot_face:", - ["\U0001f336\ufe0f"] = ":hot_pepper:", - ["\U0001f336"] = ":hot_pepper:", - ["\U0001f32d"] = ":hotdog:", - ["\U0001f3e8"] = ":hotel:", - ["\u2668\ufe0f"] = ":hotsprings:", - ["\u2668"] = ":hotsprings:", - ["\u23f3"] = ":hourglass_flowing_sand:", - ["\u231b"] = ":hourglass:", - ["\U0001f3da\ufe0f"] = ":house_abandoned:", - ["\U0001f3da"] = ":house_abandoned:", - ["\U0001f3e1"] = ":house_with_garden:", - ["\U0001f3e0"] = ":house:", - ["\U0001f917"] = ":hugging:", - ["\U0001f62f"] = ":hushed:", - ["\U0001f6d6"] = ":hut:", - ["\U0001fabb"] = ":hyacinth:", - ["\U0001f368"] = ":ice_cream:", - ["\U0001f9ca"] = ":ice_cube:", - ["\u26f8\ufe0f"] = ":ice_skate:", - ["\u26f8"] = ":ice_skate:", - ["\U0001f366"] = ":icecream:", - ["\U0001f194"] = ":id:", - ["\U0001faaa"] = ":identification_card:", - ["\U0001f250"] = ":ideograph_advantage:", - ["\U0001f47f"] = ":imp:", - ["\U0001f4e5"] = ":inbox_tray:", - ["\U0001f4e8"] = ":incoming_envelope:", - ["\U0001faf5\U0001f3fb"] = ":index_pointing_at_the_viewer_tone1:", - ["\U0001faf5\U0001f3fc"] = ":index_pointing_at_the_viewer_tone2:", - ["\U0001faf5\U0001f3fd"] = ":index_pointing_at_the_viewer_tone3:", - ["\U0001faf5\U0001f3fe"] = ":index_pointing_at_the_viewer_tone4:", - ["\U0001faf5\U0001f3ff"] = ":index_pointing_at_the_viewer_tone5:", - ["\U0001faf5"] = ":index_pointing_at_the_viewer:", - ["\u267e\ufe0f"] = ":infinity:", - ["\u267e"] = ":infinity:", - ["\u2139\ufe0f"] = ":information_source:", - ["\u2139"] = ":information_source:", - ["\U0001f607"] = ":innocent:", - ["\u2049\ufe0f"] = ":interrobang:", - ["\u2049"] = ":interrobang:", - ["\U0001f3dd\ufe0f"] = ":island:", - ["\U0001f3dd"] = ":island:", - ["\U0001f3ee"] = ":izakaya_lantern:", - ["\U0001f383"] = ":jack_o_lantern:", - ["\U0001f5fe"] = ":japan:", - ["\U0001f3ef"] = ":japanese_castle:", - ["\U0001f47a"] = ":japanese_goblin:", - ["\U0001f479"] = ":japanese_ogre:", - ["\U0001fad9"] = ":jar:", - ["\U0001f456"] = ":jeans:", - ["\U0001fabc"] = ":jellyfish:", - ["\U0001f9e9"] = ":jigsaw:", - ["\U0001f639"] = ":joy_cat:", - ["\U0001f602"] = ":joy:", - ["\U0001f579\ufe0f"] = ":joystick:", - ["\U0001f579"] = ":joystick:", - ["\U0001f9d1\U0001f3fb\u200d\u2696\ufe0f"] = ":judge_tone1:", - ["\U0001f9d1\U0001f3fc\u200d\u2696\ufe0f"] = ":judge_tone2:", - ["\U0001f9d1\U0001f3fd\u200d\u2696\ufe0f"] = ":judge_tone3:", - ["\U0001f9d1\U0001f3fe\u200d\u2696\ufe0f"] = ":judge_tone4:", - ["\U0001f9d1\U0001f3ff\u200d\u2696\ufe0f"] = ":judge_tone5:", - ["\U0001f9d1\u200d\u2696\ufe0f"] = ":judge:", - ["\U0001f54b"] = ":kaaba:", - ["\U0001f998"] = ":kangaroo:", - ["\U0001f511"] = ":key:", - ["\U0001f5dd\ufe0f"] = ":key2:", - ["\U0001f5dd"] = ":key2:", - ["\u2328\ufe0f"] = ":keyboard:", - ["\u2328"] = ":keyboard:", - ["\U0001f51f"] = ":keycap_ten:", - ["\U0001faaf"] = ":khanda:", - ["\U0001f458"] = ":kimono:", - ["\U0001f468\u200d\u2764\ufe0f\u200d\U0001f48b\u200d\U0001f468"] = ":kiss_mm:", - ["\U0001f48f"] = ":kiss_tone5:", - ["\U0001f469\u200d\u2764\ufe0f\u200d\U0001f48b\u200d\U0001f468"] = ":kiss_woman_man_tone5_tone4:", - ["\U0001f469\u200d\u2764\ufe0f\u200d\U0001f48b\u200d\U0001f469"] = ":kiss_ww:", - ["\U0001f48b"] = ":kiss:", - ["\U0001f63d"] = ":kissing_cat:", - ["\U0001f61a"] = ":kissing_closed_eyes:", - ["\U0001f618"] = ":kissing_heart:", - ["\U0001f619"] = ":kissing_smiling_eyes:", - ["\U0001f617"] = ":kissing:", - ["\U0001fa81"] = ":kite:", - ["\U0001f95d"] = ":kiwi:", - ["\U0001f52a"] = ":knife:", - ["\U0001faa2"] = ":knot:", - ["\U0001f428"] = ":koala:", - ["\U0001f201"] = ":koko:", - ["\U0001f97c"] = ":lab_coat:", - ["\U0001f3f7\ufe0f"] = ":label:", - ["\U0001f3f7"] = ":label:", - ["\U0001f94d"] = ":lacrosse:", - ["\U0001fa9c"] = ":ladder:", - ["\U0001f41e"] = ":lady_beetle:", - ["\U0001f537"] = ":large_blue_diamond:", - ["\U0001f536"] = ":large_orange_diamond:", - ["\U0001f31c"] = ":last_quarter_moon_with_face:", - ["\U0001f317"] = ":last_quarter_moon:", - ["\U0001f606"] = ":laughing:", - ["\U0001f96c"] = ":leafy_green:", - ["\U0001f343"] = ":leaves:", - ["\U0001f4d2"] = ":ledger:", - ["\U0001f91b\U0001f3fb"] = ":left_facing_fist_tone1:", - ["\U0001f91b\U0001f3fc"] = ":left_facing_fist_tone2:", - ["\U0001f91b\U0001f3fd"] = ":left_facing_fist_tone3:", - ["\U0001f91b\U0001f3fe"] = ":left_facing_fist_tone4:", - ["\U0001f91b\U0001f3ff"] = ":left_facing_fist_tone5:", - ["\U0001f91b"] = ":left_facing_fist:", - ["\U0001f6c5"] = ":left_luggage:", - ["\u2194\ufe0f"] = ":left_right_arrow:", - ["\u2194"] = ":left_right_arrow:", - ["\u21a9\ufe0f"] = ":leftwards_arrow_with_hook:", - ["\u21a9"] = ":leftwards_arrow_with_hook:", - ["\U0001faf2\U0001f3fb"] = ":leftwards_hand_tone1:", - ["\U0001faf2\U0001f3fc"] = ":leftwards_hand_tone2:", - ["\U0001faf2\U0001f3fd"] = ":leftwards_hand_tone3:", - ["\U0001faf2\U0001f3fe"] = ":leftwards_hand_tone4:", - ["\U0001faf2\U0001f3ff"] = ":leftwards_hand_tone5:", - ["\U0001faf2"] = ":leftwards_hand:", - ["\U0001faf7\U0001f3fb"] = ":leftwards_pushing_hand_tone1:", - ["\U0001faf7\U0001f3fc"] = ":leftwards_pushing_hand_tone2:", - ["\U0001faf7\U0001f3fd"] = ":leftwards_pushing_hand_tone3:", - ["\U0001faf7\U0001f3fe"] = ":leftwards_pushing_hand_tone4:", - ["\U0001faf7\U0001f3ff"] = ":leftwards_pushing_hand_tone5:", - ["\U0001faf7"] = ":leftwards_pushing_hand:", - ["\U0001f9b5\U0001f3fb"] = ":leg_tone1:", - ["\U0001f9b5\U0001f3fc"] = ":leg_tone2:", - ["\U0001f9b5\U0001f3fd"] = ":leg_tone3:", - ["\U0001f9b5\U0001f3fe"] = ":leg_tone4:", - ["\U0001f9b5\U0001f3ff"] = ":leg_tone5:", - ["\U0001f9b5"] = ":leg:", - ["\U0001f34b"] = ":lemon:", - ["\u264c"] = ":leo:", - ["\U0001f406"] = ":leopard:", - ["\U0001f39a\ufe0f"] = ":level_slider:", - ["\U0001f39a"] = ":level_slider:", - ["\U0001f574\U0001f3fb"] = ":levitate_tone1:", - ["\U0001f574\U0001f3fc"] = ":levitate_tone2:", - ["\U0001f574\U0001f3fd"] = ":levitate_tone3:", - ["\U0001f574\U0001f3fe"] = ":levitate_tone4:", - ["\U0001f574\U0001f3ff"] = ":levitate_tone5:", - ["\U0001f574\ufe0f"] = ":levitate:", - ["\U0001f574"] = ":levitate:", - ["\u264e"] = ":libra:", - ["\U0001fa75"] = ":light_blue_heart:", - ["\U0001f688"] = ":light_rail:", - ["\U0001f34b\u200d\U0001f7e9"] = ":lime:", - ["\U0001f517"] = ":link:", - ["\U0001f981"] = ":lion_face:", - ["\U0001f444"] = ":lips:", - ["\U0001f484"] = ":lipstick:", - ["\U0001f98e"] = ":lizard:", - ["\U0001f999"] = ":llama:", - ["\U0001f99e"] = ":lobster:", - ["\U0001f50f"] = ":lock_with_ink_pen:", - ["\U0001f512"] = ":lock:", - ["\U0001f36d"] = ":lollipop:", - ["\U0001fa98"] = ":long_drum:", - ["\u27bf"] = ":loop:", - ["\U0001fab7"] = ":lotus:", - ["\U0001f50a"] = ":loud_sound:", - ["\U0001f4e2"] = ":loudspeaker:", - ["\U0001f3e9"] = ":love_hotel:", - ["\U0001f48c"] = ":love_letter:", - ["\U0001f91f\U0001f3fb"] = ":love_you_gesture_tone1:", - ["\U0001f91f\U0001f3fc"] = ":love_you_gesture_tone2:", - ["\U0001f91f\U0001f3fd"] = ":love_you_gesture_tone3:", - ["\U0001f91f\U0001f3fe"] = ":love_you_gesture_tone4:", - ["\U0001f91f\U0001f3ff"] = ":love_you_gesture_tone5:", - ["\U0001f91f"] = ":love_you_gesture:", - ["\U0001faab"] = ":low_battery:", - ["\U0001f505"] = ":low_brightness:", - ["\U0001f9f3"] = ":luggage:", - ["\U0001fac1"] = ":lungs:", - ["\U0001f925"] = ":lying_face:", - ["\u24c2\ufe0f"] = ":m:", - ["\u24c2"] = ":m:", - ["\U0001f50e"] = ":mag_right:", - ["\U0001f50d"] = ":mag:", - ["\U0001f9d9\U0001f3fb"] = ":mage_tone1:", - ["\U0001f9d9\U0001f3fc"] = ":mage_tone2:", - ["\U0001f9d9\U0001f3fd"] = ":mage_tone3:", - ["\U0001f9d9\U0001f3fe"] = ":mage_tone4:", - ["\U0001f9d9\U0001f3ff"] = ":mage_tone5:", - ["\U0001f9d9"] = ":mage:", - ["\U0001fa84"] = ":magic_wand:", - ["\U0001f9f2"] = ":magnet:", - ["\U0001f004"] = ":mahjong:", - ["\U0001f4ea"] = ":mailbox_closed:", - ["\U0001f4ec"] = ":mailbox_with_mail:", - ["\U0001f4ed"] = ":mailbox_with_no_mail:", - ["\U0001f4eb"] = ":mailbox:", - ["\u2642\ufe0f"] = ":male_sign:", - ["\u2642"] = ":male_sign:", - ["\U0001f9a3"] = ":mammoth:", - ["\U0001f468\U0001f3fb\u200d\U0001f3a8"] = ":man_artist_tone1:", - ["\U0001f468\U0001f3fc\u200d\U0001f3a8"] = ":man_artist_tone2:", - ["\U0001f468\U0001f3fd\u200d\U0001f3a8"] = ":man_artist_tone3:", - ["\U0001f468\U0001f3fe\u200d\U0001f3a8"] = ":man_artist_tone4:", - ["\U0001f468\U0001f3ff\u200d\U0001f3a8"] = ":man_artist_tone5:", - ["\U0001f468\u200d\U0001f3a8"] = ":man_artist:", - ["\U0001f468\U0001f3fb\u200d\U0001f680"] = ":man_astronaut_tone1:", - ["\U0001f468\U0001f3fc\u200d\U0001f680"] = ":man_astronaut_tone2:", - ["\U0001f468\U0001f3fd\u200d\U0001f680"] = ":man_astronaut_tone3:", - ["\U0001f468\U0001f3fe\u200d\U0001f680"] = ":man_astronaut_tone4:", - ["\U0001f468\U0001f3ff\u200d\U0001f680"] = ":man_astronaut_tone5:", - ["\U0001f468\u200d\U0001f680"] = ":man_astronaut:", - ["\U0001f468\U0001f3fb\u200d\U0001f9b2"] = ":man_bald_tone1:", - ["\U0001f468\U0001f3fc\u200d\U0001f9b2"] = ":man_bald_tone2:", - ["\U0001f468\U0001f3fd\u200d\U0001f9b2"] = ":man_bald_tone3:", - ["\U0001f468\U0001f3fe\u200d\U0001f9b2"] = ":man_bald_tone4:", - ["\U0001f468\U0001f3ff\u200d\U0001f9b2"] = ":man_bald_tone5:", - ["\U0001f468\u200d\U0001f9b2"] = ":man_bald:", - ["\U0001f9d4\u200d\u2642\ufe0f"] = ":man_beard:", - ["\U0001f6b4\U0001f3fb\u200d\u2642\ufe0f"] = ":man_biking_tone1:", - ["\U0001f6b4\U0001f3fc\u200d\u2642\ufe0f"] = ":man_biking_tone2:", - ["\U0001f6b4\U0001f3fd\u200d\u2642\ufe0f"] = ":man_biking_tone3:", - ["\U0001f6b4\U0001f3fe\u200d\u2642\ufe0f"] = ":man_biking_tone4:", - ["\U0001f6b4\U0001f3ff\u200d\u2642\ufe0f"] = ":man_biking_tone5:", - ["\U0001f6b4\u200d\u2642\ufe0f"] = ":man_biking:", - ["\u26f9\U0001f3fb\u200d\u2642\ufe0f"] = ":man_bouncing_ball_tone1:", - ["\u26f9\U0001f3fc\u200d\u2642\ufe0f"] = ":man_bouncing_ball_tone2:", - ["\u26f9\U0001f3fd\u200d\u2642\ufe0f"] = ":man_bouncing_ball_tone3:", - ["\u26f9\U0001f3fe\u200d\u2642\ufe0f"] = ":man_bouncing_ball_tone4:", - ["\u26f9\U0001f3ff\u200d\u2642\ufe0f"] = ":man_bouncing_ball_tone5:", - ["\u26f9\ufe0f\u200d\u2642\ufe0f"] = ":man_bouncing_ball:", - ["\U0001f647\U0001f3fb\u200d\u2642\ufe0f"] = ":man_bowing_tone1:", - ["\U0001f647\U0001f3fc\u200d\u2642\ufe0f"] = ":man_bowing_tone2:", - ["\U0001f647\U0001f3fd\u200d\u2642\ufe0f"] = ":man_bowing_tone3:", - ["\U0001f647\U0001f3fe\u200d\u2642\ufe0f"] = ":man_bowing_tone4:", - ["\U0001f647\U0001f3ff\u200d\u2642\ufe0f"] = ":man_bowing_tone5:", - ["\U0001f647\u200d\u2642\ufe0f"] = ":man_bowing:", - ["\U0001f938\U0001f3fb\u200d\u2642\ufe0f"] = ":man_cartwheeling_tone1:", - ["\U0001f938\U0001f3fc\u200d\u2642\ufe0f"] = ":man_cartwheeling_tone2:", - ["\U0001f938\U0001f3fd\u200d\u2642\ufe0f"] = ":man_cartwheeling_tone3:", - ["\U0001f938\U0001f3fe\u200d\u2642\ufe0f"] = ":man_cartwheeling_tone4:", - ["\U0001f938\U0001f3ff\u200d\u2642\ufe0f"] = ":man_cartwheeling_tone5:", - ["\U0001f938\u200d\u2642\ufe0f"] = ":man_cartwheeling:", - ["\U0001f9d7\U0001f3fb\u200d\u2642\ufe0f"] = ":man_climbing_tone1:", - ["\U0001f9d7\U0001f3fc\u200d\u2642\ufe0f"] = ":man_climbing_tone2:", - ["\U0001f9d7\U0001f3fd\u200d\u2642\ufe0f"] = ":man_climbing_tone3:", - ["\U0001f9d7\U0001f3fe\u200d\u2642\ufe0f"] = ":man_climbing_tone4:", - ["\U0001f9d7\U0001f3ff\u200d\u2642\ufe0f"] = ":man_climbing_tone5:", - ["\U0001f9d7\u200d\u2642\ufe0f"] = ":man_climbing:", - ["\U0001f477\U0001f3fb\u200d\u2642\ufe0f"] = ":man_construction_worker_tone1:", - ["\U0001f477\U0001f3fc\u200d\u2642\ufe0f"] = ":man_construction_worker_tone2:", - ["\U0001f477\U0001f3fd\u200d\u2642\ufe0f"] = ":man_construction_worker_tone3:", - ["\U0001f477\U0001f3fe\u200d\u2642\ufe0f"] = ":man_construction_worker_tone4:", - ["\U0001f477\U0001f3ff\u200d\u2642\ufe0f"] = ":man_construction_worker_tone5:", - ["\U0001f477\u200d\u2642\ufe0f"] = ":man_construction_worker:", - ["\U0001f468\U0001f3fb\u200d\U0001f373"] = ":man_cook_tone1:", - ["\U0001f468\U0001f3fc\u200d\U0001f373"] = ":man_cook_tone2:", - ["\U0001f468\U0001f3fd\u200d\U0001f373"] = ":man_cook_tone3:", - ["\U0001f468\U0001f3fe\u200d\U0001f373"] = ":man_cook_tone4:", - ["\U0001f468\U0001f3ff\u200d\U0001f373"] = ":man_cook_tone5:", - ["\U0001f468\u200d\U0001f373"] = ":man_cook:", - ["\U0001f468\U0001f3fb\u200d\U0001f9b1"] = ":man_curly_haired_tone1:", - ["\U0001f468\U0001f3fc\u200d\U0001f9b1"] = ":man_curly_haired_tone2:", - ["\U0001f468\U0001f3fd\u200d\U0001f9b1"] = ":man_curly_haired_tone3:", - ["\U0001f468\U0001f3fe\u200d\U0001f9b1"] = ":man_curly_haired_tone4:", - ["\U0001f468\U0001f3ff\u200d\U0001f9b1"] = ":man_curly_haired_tone5:", - ["\U0001f468\u200d\U0001f9b1"] = ":man_curly_haired:", - ["\U0001f57a\U0001f3fb"] = ":man_dancing_tone1:", - ["\U0001f57a\U0001f3fc"] = ":man_dancing_tone2:", - ["\U0001f57a\U0001f3fd"] = ":man_dancing_tone3:", - ["\U0001f57a\U0001f3fe"] = ":man_dancing_tone4:", - ["\U0001f57a\U0001f3ff"] = ":man_dancing_tone5:", - ["\U0001f57a"] = ":man_dancing:", - ["\U0001f575\U0001f3fb\u200d\u2642\ufe0f"] = ":man_detective_tone1:", - ["\U0001f575\U0001f3fc\u200d\u2642\ufe0f"] = ":man_detective_tone2:", - ["\U0001f575\U0001f3fd\u200d\u2642\ufe0f"] = ":man_detective_tone3:", - ["\U0001f575\U0001f3fe\u200d\u2642\ufe0f"] = ":man_detective_tone4:", - ["\U0001f575\U0001f3ff\u200d\u2642\ufe0f"] = ":man_detective_tone5:", - ["\U0001f575\ufe0f\u200d\u2642\ufe0f"] = ":man_detective:", - ["\U0001f9dd\U0001f3fb\u200d\u2642\ufe0f"] = ":man_elf_tone1:", - ["\U0001f9dd\U0001f3fc\u200d\u2642\ufe0f"] = ":man_elf_tone2:", - ["\U0001f9dd\U0001f3fd\u200d\u2642\ufe0f"] = ":man_elf_tone3:", - ["\U0001f9dd\U0001f3fe\u200d\u2642\ufe0f"] = ":man_elf_tone4:", - ["\U0001f9dd\U0001f3ff\u200d\u2642\ufe0f"] = ":man_elf_tone5:", - ["\U0001f9dd\u200d\u2642\ufe0f"] = ":man_elf:", - ["\U0001f926\U0001f3fb\u200d\u2642\ufe0f"] = ":man_facepalming_tone1:", - ["\U0001f926\U0001f3fc\u200d\u2642\ufe0f"] = ":man_facepalming_tone2:", - ["\U0001f926\U0001f3fd\u200d\u2642\ufe0f"] = ":man_facepalming_tone3:", - ["\U0001f926\U0001f3fe\u200d\u2642\ufe0f"] = ":man_facepalming_tone4:", - ["\U0001f926\U0001f3ff\u200d\u2642\ufe0f"] = ":man_facepalming_tone5:", - ["\U0001f926\u200d\u2642\ufe0f"] = ":man_facepalming:", - ["\U0001f468\U0001f3fb\u200d\U0001f3ed"] = ":man_factory_worker_tone1:", - ["\U0001f468\U0001f3fc\u200d\U0001f3ed"] = ":man_factory_worker_tone2:", - ["\U0001f468\U0001f3fd\u200d\U0001f3ed"] = ":man_factory_worker_tone3:", - ["\U0001f468\U0001f3fe\u200d\U0001f3ed"] = ":man_factory_worker_tone4:", - ["\U0001f468\U0001f3ff\u200d\U0001f3ed"] = ":man_factory_worker_tone5:", - ["\U0001f468\u200d\U0001f3ed"] = ":man_factory_worker:", - ["\U0001f9da\U0001f3fb\u200d\u2642\ufe0f"] = ":man_fairy_tone1:", - ["\U0001f9da\U0001f3fc\u200d\u2642\ufe0f"] = ":man_fairy_tone2:", - ["\U0001f9da\U0001f3fd\u200d\u2642\ufe0f"] = ":man_fairy_tone3:", - ["\U0001f9da\U0001f3fe\u200d\u2642\ufe0f"] = ":man_fairy_tone4:", - ["\U0001f9da\U0001f3ff\u200d\u2642\ufe0f"] = ":man_fairy_tone5:", - ["\U0001f9da\u200d\u2642\ufe0f"] = ":man_fairy:", - ["\U0001f468\U0001f3fb\u200d\U0001f33e"] = ":man_farmer_tone1:", - ["\U0001f468\U0001f3fc\u200d\U0001f33e"] = ":man_farmer_tone2:", - ["\U0001f468\U0001f3fd\u200d\U0001f33e"] = ":man_farmer_tone3:", - ["\U0001f468\U0001f3fe\u200d\U0001f33e"] = ":man_farmer_tone4:", - ["\U0001f468\U0001f3ff\u200d\U0001f33e"] = ":man_farmer_tone5:", - ["\U0001f468\u200d\U0001f33e"] = ":man_farmer:", - ["\U0001f468\U0001f3fb\u200d\U0001f37c"] = ":man_feeding_baby_tone1:", - ["\U0001f468\U0001f3fc\u200d\U0001f37c"] = ":man_feeding_baby_tone2:", - ["\U0001f468\U0001f3fd\u200d\U0001f37c"] = ":man_feeding_baby_tone3:", - ["\U0001f468\U0001f3fe\u200d\U0001f37c"] = ":man_feeding_baby_tone4:", - ["\U0001f468\U0001f3ff\u200d\U0001f37c"] = ":man_feeding_baby_tone5:", - ["\U0001f468\u200d\U0001f37c"] = ":man_feeding_baby:", - ["\U0001f468\U0001f3fb\u200d\U0001f692"] = ":man_firefighter_tone1:", - ["\U0001f468\U0001f3fc\u200d\U0001f692"] = ":man_firefighter_tone2:", - ["\U0001f468\U0001f3fd\u200d\U0001f692"] = ":man_firefighter_tone3:", - ["\U0001f468\U0001f3fe\u200d\U0001f692"] = ":man_firefighter_tone4:", - ["\U0001f468\U0001f3ff\u200d\U0001f692"] = ":man_firefighter_tone5:", - ["\U0001f468\u200d\U0001f692"] = ":man_firefighter:", - ["\U0001f64d\U0001f3fb\u200d\u2642\ufe0f"] = ":man_frowning_tone1:", - ["\U0001f64d\U0001f3fc\u200d\u2642\ufe0f"] = ":man_frowning_tone2:", - ["\U0001f64d\U0001f3fd\u200d\u2642\ufe0f"] = ":man_frowning_tone3:", - ["\U0001f64d\U0001f3fe\u200d\u2642\ufe0f"] = ":man_frowning_tone4:", - ["\U0001f64d\U0001f3ff\u200d\u2642\ufe0f"] = ":man_frowning_tone5:", - ["\U0001f64d\u200d\u2642\ufe0f"] = ":man_frowning:", - ["\U0001f9de\u200d\u2642\ufe0f"] = ":man_genie:", - ["\U0001f645\U0001f3fb\u200d\u2642\ufe0f"] = ":man_gesturing_no_tone1:", - ["\U0001f645\U0001f3fc\u200d\u2642\ufe0f"] = ":man_gesturing_no_tone2:", - ["\U0001f645\U0001f3fd\u200d\u2642\ufe0f"] = ":man_gesturing_no_tone3:", - ["\U0001f645\U0001f3fe\u200d\u2642\ufe0f"] = ":man_gesturing_no_tone4:", - ["\U0001f645\U0001f3ff\u200d\u2642\ufe0f"] = ":man_gesturing_no_tone5:", - ["\U0001f645\u200d\u2642\ufe0f"] = ":man_gesturing_no:", - ["\U0001f646\U0001f3fb\u200d\u2642\ufe0f"] = ":man_gesturing_ok_tone1:", - ["\U0001f646\U0001f3fc\u200d\u2642\ufe0f"] = ":man_gesturing_ok_tone2:", - ["\U0001f646\U0001f3fd\u200d\u2642\ufe0f"] = ":man_gesturing_ok_tone3:", - ["\U0001f646\U0001f3fe\u200d\u2642\ufe0f"] = ":man_gesturing_ok_tone4:", - ["\U0001f646\U0001f3ff\u200d\u2642\ufe0f"] = ":man_gesturing_ok_tone5:", - ["\U0001f646\u200d\u2642\ufe0f"] = ":man_gesturing_ok:", - ["\U0001f486\U0001f3fb\u200d\u2642\ufe0f"] = ":man_getting_face_massage_tone1:", - ["\U0001f486\U0001f3fc\u200d\u2642\ufe0f"] = ":man_getting_face_massage_tone2:", - ["\U0001f486\U0001f3fd\u200d\u2642\ufe0f"] = ":man_getting_face_massage_tone3:", - ["\U0001f486\U0001f3fe\u200d\u2642\ufe0f"] = ":man_getting_face_massage_tone4:", - ["\U0001f486\U0001f3ff\u200d\u2642\ufe0f"] = ":man_getting_face_massage_tone5:", - ["\U0001f486\u200d\u2642\ufe0f"] = ":man_getting_face_massage:", - ["\U0001f487\U0001f3fb\u200d\u2642\ufe0f"] = ":man_getting_haircut_tone1:", - ["\U0001f487\U0001f3fc\u200d\u2642\ufe0f"] = ":man_getting_haircut_tone2:", - ["\U0001f487\U0001f3fd\u200d\u2642\ufe0f"] = ":man_getting_haircut_tone3:", - ["\U0001f487\U0001f3fe\u200d\u2642\ufe0f"] = ":man_getting_haircut_tone4:", - ["\U0001f487\U0001f3ff\u200d\u2642\ufe0f"] = ":man_getting_haircut_tone5:", - ["\U0001f487\u200d\u2642\ufe0f"] = ":man_getting_haircut:", - ["\U0001f3cc\U0001f3fb\u200d\u2642\ufe0f"] = ":man_golfing_tone1:", - ["\U0001f3cc\U0001f3fc\u200d\u2642\ufe0f"] = ":man_golfing_tone2:", - ["\U0001f3cc\U0001f3fd\u200d\u2642\ufe0f"] = ":man_golfing_tone3:", - ["\U0001f3cc\U0001f3fe\u200d\u2642\ufe0f"] = ":man_golfing_tone4:", - ["\U0001f3cc\U0001f3ff\u200d\u2642\ufe0f"] = ":man_golfing_tone5:", - ["\U0001f3cc\ufe0f\u200d\u2642\ufe0f"] = ":man_golfing:", - ["\U0001f482\U0001f3fb\u200d\u2642\ufe0f"] = ":man_guard_tone1:", - ["\U0001f482\U0001f3fc\u200d\u2642\ufe0f"] = ":man_guard_tone2:", - ["\U0001f482\U0001f3fd\u200d\u2642\ufe0f"] = ":man_guard_tone3:", - ["\U0001f482\U0001f3fe\u200d\u2642\ufe0f"] = ":man_guard_tone4:", - ["\U0001f482\U0001f3ff\u200d\u2642\ufe0f"] = ":man_guard_tone5:", - ["\U0001f482\u200d\u2642\ufe0f"] = ":man_guard:", - ["\U0001f468\U0001f3fb\u200d\u2695\ufe0f"] = ":man_health_worker_tone1:", - ["\U0001f468\U0001f3fc\u200d\u2695\ufe0f"] = ":man_health_worker_tone2:", - ["\U0001f468\U0001f3fd\u200d\u2695\ufe0f"] = ":man_health_worker_tone3:", - ["\U0001f468\U0001f3fe\u200d\u2695\ufe0f"] = ":man_health_worker_tone4:", - ["\U0001f468\U0001f3ff\u200d\u2695\ufe0f"] = ":man_health_worker_tone5:", - ["\U0001f468\u200d\u2695\ufe0f"] = ":man_health_worker:", - ["\U0001f9d8\U0001f3fb\u200d\u2642\ufe0f"] = ":man_in_lotus_position_tone1:", - ["\U0001f9d8\U0001f3fc\u200d\u2642\ufe0f"] = ":man_in_lotus_position_tone2:", - ["\U0001f9d8\U0001f3fd\u200d\u2642\ufe0f"] = ":man_in_lotus_position_tone3:", - ["\U0001f9d8\U0001f3fe\u200d\u2642\ufe0f"] = ":man_in_lotus_position_tone4:", - ["\U0001f9d8\U0001f3ff\u200d\u2642\ufe0f"] = ":man_in_lotus_position_tone5:", - ["\U0001f9d8\u200d\u2642\ufe0f"] = ":man_in_lotus_position:", - ["\U0001f468\U0001f3fb\u200d\U0001f9bd\u200d\u27a1\ufe0f"] = ":man_in_manual_wheelchair_facing_right_tone1:", - ["\U0001f468\U0001f3fc\u200d\U0001f9bd\u200d\u27a1\ufe0f"] = ":man_in_manual_wheelchair_facing_right_tone2:", - ["\U0001f468\U0001f3fd\u200d\U0001f9bd\u200d\u27a1\ufe0f"] = ":man_in_manual_wheelchair_facing_right_tone3:", - ["\U0001f468\U0001f3fe\u200d\U0001f9bd\u200d\u27a1\ufe0f"] = ":man_in_manual_wheelchair_facing_right_tone4:", - ["\U0001f468\U0001f3ff\u200d\U0001f9bd\u200d\u27a1\ufe0f"] = ":man_in_manual_wheelchair_facing_right_tone5:", - ["\U0001f468\u200d\U0001f9bd\u200d\u27a1\ufe0f"] = ":man_in_manual_wheelchair_facing_right:", - ["\U0001f468\U0001f3fb\u200d\U0001f9bd"] = ":man_in_manual_wheelchair_tone1:", - ["\U0001f468\U0001f3fc\u200d\U0001f9bd"] = ":man_in_manual_wheelchair_tone2:", - ["\U0001f468\U0001f3fd\u200d\U0001f9bd"] = ":man_in_manual_wheelchair_tone3:", - ["\U0001f468\U0001f3fe\u200d\U0001f9bd"] = ":man_in_manual_wheelchair_tone4:", - ["\U0001f468\U0001f3ff\u200d\U0001f9bd"] = ":man_in_manual_wheelchair_tone5:", - ["\U0001f468\u200d\U0001f9bd"] = ":man_in_manual_wheelchair:", - ["\U0001f468\U0001f3fb\u200d\U0001f9bc\u200d\u27a1\ufe0f"] = ":man_in_motorized_wheelchair_facing_right_tone1:", - ["\U0001f468\U0001f3fc\u200d\U0001f9bc\u200d\u27a1\ufe0f"] = ":man_in_motorized_wheelchair_facing_right_tone2:", - ["\U0001f468\U0001f3fd\u200d\U0001f9bc\u200d\u27a1\ufe0f"] = ":man_in_motorized_wheelchair_facing_right_tone3:", - ["\U0001f468\U0001f3fe\u200d\U0001f9bc\u200d\u27a1\ufe0f"] = ":man_in_motorized_wheelchair_facing_right_tone4:", - ["\U0001f468\U0001f3ff\u200d\U0001f9bc\u200d\u27a1\ufe0f"] = ":man_in_motorized_wheelchair_facing_right_tone5:", - ["\U0001f468\u200d\U0001f9bc\u200d\u27a1\ufe0f"] = ":man_in_motorized_wheelchair_facing_right:", - ["\U0001f468\U0001f3fb\u200d\U0001f9bc"] = ":man_in_motorized_wheelchair_tone1:", - ["\U0001f468\U0001f3fc\u200d\U0001f9bc"] = ":man_in_motorized_wheelchair_tone2:", - ["\U0001f468\U0001f3fd\u200d\U0001f9bc"] = ":man_in_motorized_wheelchair_tone3:", - ["\U0001f468\U0001f3fe\u200d\U0001f9bc"] = ":man_in_motorized_wheelchair_tone4:", - ["\U0001f468\U0001f3ff\u200d\U0001f9bc"] = ":man_in_motorized_wheelchair_tone5:", - ["\U0001f468\u200d\U0001f9bc"] = ":man_in_motorized_wheelchair:", - ["\U0001f9d6\U0001f3fb\u200d\u2642\ufe0f"] = ":man_in_steamy_room_tone1:", - ["\U0001f9d6\U0001f3fc\u200d\u2642\ufe0f"] = ":man_in_steamy_room_tone2:", - ["\U0001f9d6\U0001f3fd\u200d\u2642\ufe0f"] = ":man_in_steamy_room_tone3:", - ["\U0001f9d6\U0001f3fe\u200d\u2642\ufe0f"] = ":man_in_steamy_room_tone4:", - ["\U0001f9d6\U0001f3ff\u200d\u2642\ufe0f"] = ":man_in_steamy_room_tone5:", - ["\U0001f9d6\u200d\u2642\ufe0f"] = ":man_in_steamy_room:", - ["\U0001f935\U0001f3fb\u200d\u2642\ufe0f"] = ":man_in_tuxedo_tone1:", - ["\U0001f935\U0001f3fc\u200d\u2642\ufe0f"] = ":man_in_tuxedo_tone2:", - ["\U0001f935\U0001f3fd\u200d\u2642\ufe0f"] = ":man_in_tuxedo_tone3:", - ["\U0001f935\U0001f3fe\u200d\u2642\ufe0f"] = ":man_in_tuxedo_tone4:", - ["\U0001f935\U0001f3ff\u200d\u2642\ufe0f"] = ":man_in_tuxedo_tone5:", - ["\U0001f935\u200d\u2642\ufe0f"] = ":man_in_tuxedo:", - ["\U0001f468\U0001f3fb\u200d\u2696\ufe0f"] = ":man_judge_tone1:", - ["\U0001f468\U0001f3fc\u200d\u2696\ufe0f"] = ":man_judge_tone2:", - ["\U0001f468\U0001f3fd\u200d\u2696\ufe0f"] = ":man_judge_tone3:", - ["\U0001f468\U0001f3fe\u200d\u2696\ufe0f"] = ":man_judge_tone4:", - ["\U0001f468\U0001f3ff\u200d\u2696\ufe0f"] = ":man_judge_tone5:", - ["\U0001f468\u200d\u2696\ufe0f"] = ":man_judge:", - ["\U0001f939\U0001f3fb\u200d\u2642\ufe0f"] = ":man_juggling_tone1:", - ["\U0001f939\U0001f3fc\u200d\u2642\ufe0f"] = ":man_juggling_tone2:", - ["\U0001f939\U0001f3fd\u200d\u2642\ufe0f"] = ":man_juggling_tone3:", - ["\U0001f939\U0001f3fe\u200d\u2642\ufe0f"] = ":man_juggling_tone4:", - ["\U0001f939\U0001f3ff\u200d\u2642\ufe0f"] = ":man_juggling_tone5:", - ["\U0001f939\u200d\u2642\ufe0f"] = ":man_juggling:", - ["\U0001f9ce\U0001f3fb\u200d\u2642\ufe0f\u200d\u27a1\ufe0f"] = ":man_kneeling_facing_right_tone1:", - ["\U0001f9ce\U0001f3fc\u200d\u2642\ufe0f\u200d\u27a1\ufe0f"] = ":man_kneeling_facing_right_tone2:", - ["\U0001f9ce\U0001f3fd\u200d\u2642\ufe0f\u200d\u27a1\ufe0f"] = ":man_kneeling_facing_right_tone3:", - ["\U0001f9ce\U0001f3fe\u200d\u2642\ufe0f\u200d\u27a1\ufe0f"] = ":man_kneeling_facing_right_tone4:", - ["\U0001f9ce\U0001f3ff\u200d\u2642\ufe0f\u200d\u27a1\ufe0f"] = ":man_kneeling_facing_right_tone5:", - ["\U0001f9ce\u200d\u2642\ufe0f\u200d\u27a1\ufe0f"] = ":man_kneeling_facing_right:", - ["\U0001f9ce\U0001f3fb\u200d\u2642\ufe0f"] = ":man_kneeling_tone1:", - ["\U0001f9ce\U0001f3fc\u200d\u2642\ufe0f"] = ":man_kneeling_tone2:", - ["\U0001f9ce\U0001f3fd\u200d\u2642\ufe0f"] = ":man_kneeling_tone3:", - ["\U0001f9ce\U0001f3fe\u200d\u2642\ufe0f"] = ":man_kneeling_tone4:", - ["\U0001f9ce\U0001f3ff\u200d\u2642\ufe0f"] = ":man_kneeling_tone5:", - ["\U0001f9ce\u200d\u2642\ufe0f"] = ":man_kneeling:", - ["\U0001f3cb\U0001f3fb\u200d\u2642\ufe0f"] = ":man_lifting_weights_tone1:", - ["\U0001f3cb\U0001f3fc\u200d\u2642\ufe0f"] = ":man_lifting_weights_tone2:", - ["\U0001f3cb\U0001f3fd\u200d\u2642\ufe0f"] = ":man_lifting_weights_tone3:", - ["\U0001f3cb\U0001f3fe\u200d\u2642\ufe0f"] = ":man_lifting_weights_tone4:", - ["\U0001f3cb\U0001f3ff\u200d\u2642\ufe0f"] = ":man_lifting_weights_tone5:", - ["\U0001f3cb\ufe0f\u200d\u2642\ufe0f"] = ":man_lifting_weights:", - ["\U0001f9d9\U0001f3fb\u200d\u2642\ufe0f"] = ":man_mage_tone1:", - ["\U0001f9d9\U0001f3fc\u200d\u2642\ufe0f"] = ":man_mage_tone2:", - ["\U0001f9d9\U0001f3fd\u200d\u2642\ufe0f"] = ":man_mage_tone3:", - ["\U0001f9d9\U0001f3fe\u200d\u2642\ufe0f"] = ":man_mage_tone4:", - ["\U0001f9d9\U0001f3ff\u200d\u2642\ufe0f"] = ":man_mage_tone5:", - ["\U0001f9d9\u200d\u2642\ufe0f"] = ":man_mage:", - ["\U0001f468\U0001f3fb\u200d\U0001f527"] = ":man_mechanic_tone1:", - ["\U0001f468\U0001f3fc\u200d\U0001f527"] = ":man_mechanic_tone2:", - ["\U0001f468\U0001f3fd\u200d\U0001f527"] = ":man_mechanic_tone3:", - ["\U0001f468\U0001f3fe\u200d\U0001f527"] = ":man_mechanic_tone4:", - ["\U0001f468\U0001f3ff\u200d\U0001f527"] = ":man_mechanic_tone5:", - ["\U0001f468\u200d\U0001f527"] = ":man_mechanic:", - ["\U0001f6b5\U0001f3fb\u200d\u2642\ufe0f"] = ":man_mountain_biking_tone1:", - ["\U0001f6b5\U0001f3fc\u200d\u2642\ufe0f"] = ":man_mountain_biking_tone2:", - ["\U0001f6b5\U0001f3fd\u200d\u2642\ufe0f"] = ":man_mountain_biking_tone3:", - ["\U0001f6b5\U0001f3fe\u200d\u2642\ufe0f"] = ":man_mountain_biking_tone4:", - ["\U0001f6b5\U0001f3ff\u200d\u2642\ufe0f"] = ":man_mountain_biking_tone5:", - ["\U0001f6b5\u200d\u2642\ufe0f"] = ":man_mountain_biking:", - ["\U0001f468\U0001f3fb\u200d\U0001f4bc"] = ":man_office_worker_tone1:", - ["\U0001f468\U0001f3fc\u200d\U0001f4bc"] = ":man_office_worker_tone2:", - ["\U0001f468\U0001f3fd\u200d\U0001f4bc"] = ":man_office_worker_tone3:", - ["\U0001f468\U0001f3fe\u200d\U0001f4bc"] = ":man_office_worker_tone4:", - ["\U0001f468\U0001f3ff\u200d\U0001f4bc"] = ":man_office_worker_tone5:", - ["\U0001f468\u200d\U0001f4bc"] = ":man_office_worker:", - ["\U0001f468\U0001f3fb\u200d\u2708\ufe0f"] = ":man_pilot_tone1:", - ["\U0001f468\U0001f3fc\u200d\u2708\ufe0f"] = ":man_pilot_tone2:", - ["\U0001f468\U0001f3fd\u200d\u2708\ufe0f"] = ":man_pilot_tone3:", - ["\U0001f468\U0001f3fe\u200d\u2708\ufe0f"] = ":man_pilot_tone4:", - ["\U0001f468\U0001f3ff\u200d\u2708\ufe0f"] = ":man_pilot_tone5:", - ["\U0001f468\u200d\u2708\ufe0f"] = ":man_pilot:", - ["\U0001f93e\U0001f3fb\u200d\u2642\ufe0f"] = ":man_playing_handball_tone1:", - ["\U0001f93e\U0001f3fc\u200d\u2642\ufe0f"] = ":man_playing_handball_tone2:", - ["\U0001f93e\U0001f3fd\u200d\u2642\ufe0f"] = ":man_playing_handball_tone3:", - ["\U0001f93e\U0001f3fe\u200d\u2642\ufe0f"] = ":man_playing_handball_tone4:", - ["\U0001f93e\U0001f3ff\u200d\u2642\ufe0f"] = ":man_playing_handball_tone5:", - ["\U0001f93e\u200d\u2642\ufe0f"] = ":man_playing_handball:", - ["\U0001f93d\U0001f3fb\u200d\u2642\ufe0f"] = ":man_playing_water_polo_tone1:", - ["\U0001f93d\U0001f3fc\u200d\u2642\ufe0f"] = ":man_playing_water_polo_tone2:", - ["\U0001f93d\U0001f3fd\u200d\u2642\ufe0f"] = ":man_playing_water_polo_tone3:", - ["\U0001f93d\U0001f3fe\u200d\u2642\ufe0f"] = ":man_playing_water_polo_tone4:", - ["\U0001f93d\U0001f3ff\u200d\u2642\ufe0f"] = ":man_playing_water_polo_tone5:", - ["\U0001f93d\u200d\u2642\ufe0f"] = ":man_playing_water_polo:", - ["\U0001f46e\U0001f3fb\u200d\u2642\ufe0f"] = ":man_police_officer_tone1:", - ["\U0001f46e\U0001f3fc\u200d\u2642\ufe0f"] = ":man_police_officer_tone2:", - ["\U0001f46e\U0001f3fd\u200d\u2642\ufe0f"] = ":man_police_officer_tone3:", - ["\U0001f46e\U0001f3fe\u200d\u2642\ufe0f"] = ":man_police_officer_tone4:", - ["\U0001f46e\U0001f3ff\u200d\u2642\ufe0f"] = ":man_police_officer_tone5:", - ["\U0001f46e\u200d\u2642\ufe0f"] = ":man_police_officer:", - ["\U0001f64e\U0001f3fb\u200d\u2642\ufe0f"] = ":man_pouting_tone1:", - ["\U0001f64e\U0001f3fc\u200d\u2642\ufe0f"] = ":man_pouting_tone2:", - ["\U0001f64e\U0001f3fd\u200d\u2642\ufe0f"] = ":man_pouting_tone3:", - ["\U0001f64e\U0001f3fe\u200d\u2642\ufe0f"] = ":man_pouting_tone4:", - ["\U0001f64e\U0001f3ff\u200d\u2642\ufe0f"] = ":man_pouting_tone5:", - ["\U0001f64e\u200d\u2642\ufe0f"] = ":man_pouting:", - ["\U0001f64b\U0001f3fb\u200d\u2642\ufe0f"] = ":man_raising_hand_tone1:", - ["\U0001f64b\U0001f3fc\u200d\u2642\ufe0f"] = ":man_raising_hand_tone2:", - ["\U0001f64b\U0001f3fd\u200d\u2642\ufe0f"] = ":man_raising_hand_tone3:", - ["\U0001f64b\U0001f3fe\u200d\u2642\ufe0f"] = ":man_raising_hand_tone4:", - ["\U0001f64b\U0001f3ff\u200d\u2642\ufe0f"] = ":man_raising_hand_tone5:", - ["\U0001f64b\u200d\u2642\ufe0f"] = ":man_raising_hand:", - ["\U0001f468\U0001f3fb\u200d\U0001f9b0"] = ":man_red_haired_tone1:", - ["\U0001f468\U0001f3fc\u200d\U0001f9b0"] = ":man_red_haired_tone2:", - ["\U0001f468\U0001f3fd\u200d\U0001f9b0"] = ":man_red_haired_tone3:", - ["\U0001f468\U0001f3fe\u200d\U0001f9b0"] = ":man_red_haired_tone4:", - ["\U0001f468\U0001f3ff\u200d\U0001f9b0"] = ":man_red_haired_tone5:", - ["\U0001f468\u200d\U0001f9b0"] = ":man_red_haired:", - ["\U0001f6a3\U0001f3fb\u200d\u2642\ufe0f"] = ":man_rowing_boat_tone1:", - ["\U0001f6a3\U0001f3fc\u200d\u2642\ufe0f"] = ":man_rowing_boat_tone2:", - ["\U0001f6a3\U0001f3fd\u200d\u2642\ufe0f"] = ":man_rowing_boat_tone3:", - ["\U0001f6a3\U0001f3fe\u200d\u2642\ufe0f"] = ":man_rowing_boat_tone4:", - ["\U0001f6a3\U0001f3ff\u200d\u2642\ufe0f"] = ":man_rowing_boat_tone5:", - ["\U0001f6a3\u200d\u2642\ufe0f"] = ":man_rowing_boat:", - ["\U0001f3c3\U0001f3fb\u200d\u2642\ufe0f\u200d\u27a1\ufe0f"] = ":man_running_facing_right_tone1:", - ["\U0001f3c3\U0001f3fc\u200d\u2642\ufe0f\u200d\u27a1\ufe0f"] = ":man_running_facing_right_tone2:", - ["\U0001f3c3\U0001f3fd\u200d\u2642\ufe0f\u200d\u27a1\ufe0f"] = ":man_running_facing_right_tone3:", - ["\U0001f3c3\U0001f3fe\u200d\u2642\ufe0f\u200d\u27a1\ufe0f"] = ":man_running_facing_right_tone4:", - ["\U0001f3c3\U0001f3ff\u200d\u2642\ufe0f\u200d\u27a1\ufe0f"] = ":man_running_facing_right_tone5:", - ["\U0001f3c3\u200d\u2642\ufe0f\u200d\u27a1\ufe0f"] = ":man_running_facing_right:", - ["\U0001f3c3\U0001f3fb\u200d\u2642\ufe0f"] = ":man_running_tone1:", - ["\U0001f3c3\U0001f3fc\u200d\u2642\ufe0f"] = ":man_running_tone2:", - ["\U0001f3c3\U0001f3fd\u200d\u2642\ufe0f"] = ":man_running_tone3:", - ["\U0001f3c3\U0001f3fe\u200d\u2642\ufe0f"] = ":man_running_tone4:", - ["\U0001f3c3\U0001f3ff\u200d\u2642\ufe0f"] = ":man_running_tone5:", - ["\U0001f3c3\u200d\u2642\ufe0f"] = ":man_running:", - ["\U0001f468\U0001f3fb\u200d\U0001f52c"] = ":man_scientist_tone1:", - ["\U0001f468\U0001f3fc\u200d\U0001f52c"] = ":man_scientist_tone2:", - ["\U0001f468\U0001f3fd\u200d\U0001f52c"] = ":man_scientist_tone3:", - ["\U0001f468\U0001f3fe\u200d\U0001f52c"] = ":man_scientist_tone4:", - ["\U0001f468\U0001f3ff\u200d\U0001f52c"] = ":man_scientist_tone5:", - ["\U0001f468\u200d\U0001f52c"] = ":man_scientist:", - ["\U0001f937\U0001f3fb\u200d\u2642\ufe0f"] = ":man_shrugging_tone1:", - ["\U0001f937\U0001f3fc\u200d\u2642\ufe0f"] = ":man_shrugging_tone2:", - ["\U0001f937\U0001f3fd\u200d\u2642\ufe0f"] = ":man_shrugging_tone3:", - ["\U0001f937\U0001f3fe\u200d\u2642\ufe0f"] = ":man_shrugging_tone4:", - ["\U0001f937\U0001f3ff\u200d\u2642\ufe0f"] = ":man_shrugging_tone5:", - ["\U0001f937\u200d\u2642\ufe0f"] = ":man_shrugging:", - ["\U0001f468\U0001f3fb\u200d\U0001f3a4"] = ":man_singer_tone1:", - ["\U0001f468\U0001f3fc\u200d\U0001f3a4"] = ":man_singer_tone2:", - ["\U0001f468\U0001f3fd\u200d\U0001f3a4"] = ":man_singer_tone3:", - ["\U0001f468\U0001f3fe\u200d\U0001f3a4"] = ":man_singer_tone4:", - ["\U0001f468\U0001f3ff\u200d\U0001f3a4"] = ":man_singer_tone5:", - ["\U0001f468\u200d\U0001f3a4"] = ":man_singer:", - ["\U0001f9cd\U0001f3fb\u200d\u2642\ufe0f"] = ":man_standing_tone1:", - ["\U0001f9cd\U0001f3fc\u200d\u2642\ufe0f"] = ":man_standing_tone2:", - ["\U0001f9cd\U0001f3fd\u200d\u2642\ufe0f"] = ":man_standing_tone3:", - ["\U0001f9cd\U0001f3fe\u200d\u2642\ufe0f"] = ":man_standing_tone4:", - ["\U0001f9cd\U0001f3ff\u200d\u2642\ufe0f"] = ":man_standing_tone5:", - ["\U0001f9cd\u200d\u2642\ufe0f"] = ":man_standing:", - ["\U0001f468\U0001f3fb\u200d\U0001f393"] = ":man_student_tone1:", - ["\U0001f468\U0001f3fc\u200d\U0001f393"] = ":man_student_tone2:", - ["\U0001f468\U0001f3fd\u200d\U0001f393"] = ":man_student_tone3:", - ["\U0001f468\U0001f3fe\u200d\U0001f393"] = ":man_student_tone4:", - ["\U0001f468\U0001f3ff\u200d\U0001f393"] = ":man_student_tone5:", - ["\U0001f468\u200d\U0001f393"] = ":man_student:", - ["\U0001f9b8\U0001f3fb\u200d\u2642\ufe0f"] = ":man_superhero_tone1:", - ["\U0001f9b8\U0001f3fc\u200d\u2642\ufe0f"] = ":man_superhero_tone2:", - ["\U0001f9b8\U0001f3fd\u200d\u2642\ufe0f"] = ":man_superhero_tone3:", - ["\U0001f9b8\U0001f3fe\u200d\u2642\ufe0f"] = ":man_superhero_tone4:", - ["\U0001f9b8\U0001f3ff\u200d\u2642\ufe0f"] = ":man_superhero_tone5:", - ["\U0001f9b8\u200d\u2642\ufe0f"] = ":man_superhero:", - ["\U0001f9b9\U0001f3fb\u200d\u2642\ufe0f"] = ":man_supervillain_tone1:", - ["\U0001f9b9\U0001f3fc\u200d\u2642\ufe0f"] = ":man_supervillain_tone2:", - ["\U0001f9b9\U0001f3fd\u200d\u2642\ufe0f"] = ":man_supervillain_tone3:", - ["\U0001f9b9\U0001f3fe\u200d\u2642\ufe0f"] = ":man_supervillain_tone4:", - ["\U0001f9b9\U0001f3ff\u200d\u2642\ufe0f"] = ":man_supervillain_tone5:", - ["\U0001f9b9\u200d\u2642\ufe0f"] = ":man_supervillain:", - ["\U0001f3c4\U0001f3fb\u200d\u2642\ufe0f"] = ":man_surfing_tone1:", - ["\U0001f3c4\U0001f3fc\u200d\u2642\ufe0f"] = ":man_surfing_tone2:", - ["\U0001f3c4\U0001f3fd\u200d\u2642\ufe0f"] = ":man_surfing_tone3:", - ["\U0001f3c4\U0001f3fe\u200d\u2642\ufe0f"] = ":man_surfing_tone4:", - ["\U0001f3c4\U0001f3ff\u200d\u2642\ufe0f"] = ":man_surfing_tone5:", - ["\U0001f3c4\u200d\u2642\ufe0f"] = ":man_surfing:", - ["\U0001f3ca\U0001f3fb\u200d\u2642\ufe0f"] = ":man_swimming_tone1:", - ["\U0001f3ca\U0001f3fc\u200d\u2642\ufe0f"] = ":man_swimming_tone2:", - ["\U0001f3ca\U0001f3fd\u200d\u2642\ufe0f"] = ":man_swimming_tone3:", - ["\U0001f3ca\U0001f3fe\u200d\u2642\ufe0f"] = ":man_swimming_tone4:", - ["\U0001f3ca\U0001f3ff\u200d\u2642\ufe0f"] = ":man_swimming_tone5:", - ["\U0001f3ca\u200d\u2642\ufe0f"] = ":man_swimming:", - ["\U0001f468\U0001f3fb\u200d\U0001f3eb"] = ":man_teacher_tone1:", - ["\U0001f468\U0001f3fc\u200d\U0001f3eb"] = ":man_teacher_tone2:", - ["\U0001f468\U0001f3fd\u200d\U0001f3eb"] = ":man_teacher_tone3:", - ["\U0001f468\U0001f3fe\u200d\U0001f3eb"] = ":man_teacher_tone4:", - ["\U0001f468\U0001f3ff\u200d\U0001f3eb"] = ":man_teacher_tone5:", - ["\U0001f468\u200d\U0001f3eb"] = ":man_teacher:", - ["\U0001f468\U0001f3fb\u200d\U0001f4bb"] = ":man_technologist_tone1:", - ["\U0001f468\U0001f3fc\u200d\U0001f4bb"] = ":man_technologist_tone2:", - ["\U0001f468\U0001f3fd\u200d\U0001f4bb"] = ":man_technologist_tone3:", - ["\U0001f468\U0001f3fe\u200d\U0001f4bb"] = ":man_technologist_tone4:", - ["\U0001f468\U0001f3ff\u200d\U0001f4bb"] = ":man_technologist_tone5:", - ["\U0001f468\u200d\U0001f4bb"] = ":man_technologist:", - ["\U0001f481\U0001f3fb\u200d\u2642\ufe0f"] = ":man_tipping_hand_tone1:", - ["\U0001f481\U0001f3fc\u200d\u2642\ufe0f"] = ":man_tipping_hand_tone2:", - ["\U0001f481\U0001f3fd\u200d\u2642\ufe0f"] = ":man_tipping_hand_tone3:", - ["\U0001f481\U0001f3fe\u200d\u2642\ufe0f"] = ":man_tipping_hand_tone4:", - ["\U0001f481\U0001f3ff\u200d\u2642\ufe0f"] = ":man_tipping_hand_tone5:", - ["\U0001f481\u200d\u2642\ufe0f"] = ":man_tipping_hand:", - ["\U0001f9d4\U0001f3fb\u200d\u2642\ufe0f"] = ":man_tone1_beard:", - ["\U0001f468\U0001f3fb"] = ":man_tone1:", - ["\U0001f9d4\U0001f3fc\u200d\u2642\ufe0f"] = ":man_tone2_beard:", - ["\U0001f468\U0001f3fc"] = ":man_tone2:", - ["\U0001f9d4\U0001f3fd\u200d\u2642\ufe0f"] = ":man_tone3_beard:", - ["\U0001f468\U0001f3fd"] = ":man_tone3:", - ["\U0001f9d4\U0001f3fe\u200d\u2642\ufe0f"] = ":man_tone4_beard:", - ["\U0001f468\U0001f3fe"] = ":man_tone4:", - ["\U0001f9d4\U0001f3ff\u200d\u2642\ufe0f"] = ":man_tone5_beard:", - ["\U0001f468\U0001f3ff"] = ":man_tone5:", - ["\U0001f9db\U0001f3fb\u200d\u2642\ufe0f"] = ":man_vampire_tone1:", - ["\U0001f9db\U0001f3fc\u200d\u2642\ufe0f"] = ":man_vampire_tone2:", - ["\U0001f9db\U0001f3fd\u200d\u2642\ufe0f"] = ":man_vampire_tone3:", - ["\U0001f9db\U0001f3fe\u200d\u2642\ufe0f"] = ":man_vampire_tone4:", - ["\U0001f9db\U0001f3ff\u200d\u2642\ufe0f"] = ":man_vampire_tone5:", - ["\U0001f9db\u200d\u2642\ufe0f"] = ":man_vampire:", - ["\U0001f6b6\U0001f3fb\u200d\u2642\ufe0f\u200d\u27a1\ufe0f"] = ":man_walking_facing_right_tone1:", - ["\U0001f6b6\U0001f3fc\u200d\u2642\ufe0f\u200d\u27a1\ufe0f"] = ":man_walking_facing_right_tone2:", - ["\U0001f6b6\U0001f3fd\u200d\u2642\ufe0f\u200d\u27a1\ufe0f"] = ":man_walking_facing_right_tone3:", - ["\U0001f6b6\U0001f3fe\u200d\u2642\ufe0f\u200d\u27a1\ufe0f"] = ":man_walking_facing_right_tone4:", - ["\U0001f6b6\U0001f3ff\u200d\u2642\ufe0f\u200d\u27a1\ufe0f"] = ":man_walking_facing_right_tone5:", - ["\U0001f6b6\u200d\u2642\ufe0f\u200d\u27a1\ufe0f"] = ":man_walking_facing_right:", - ["\U0001f6b6\U0001f3fb\u200d\u2642\ufe0f"] = ":man_walking_tone1:", - ["\U0001f6b6\U0001f3fc\u200d\u2642\ufe0f"] = ":man_walking_tone2:", - ["\U0001f6b6\U0001f3fd\u200d\u2642\ufe0f"] = ":man_walking_tone3:", - ["\U0001f6b6\U0001f3fe\u200d\u2642\ufe0f"] = ":man_walking_tone4:", - ["\U0001f6b6\U0001f3ff\u200d\u2642\ufe0f"] = ":man_walking_tone5:", - ["\U0001f6b6\u200d\u2642\ufe0f"] = ":man_walking:", - ["\U0001f473\U0001f3fb\u200d\u2642\ufe0f"] = ":man_wearing_turban_tone1:", - ["\U0001f473\U0001f3fc\u200d\u2642\ufe0f"] = ":man_wearing_turban_tone2:", - ["\U0001f473\U0001f3fd\u200d\u2642\ufe0f"] = ":man_wearing_turban_tone3:", - ["\U0001f473\U0001f3fe\u200d\u2642\ufe0f"] = ":man_wearing_turban_tone4:", - ["\U0001f473\U0001f3ff\u200d\u2642\ufe0f"] = ":man_wearing_turban_tone5:", - ["\U0001f473\u200d\u2642\ufe0f"] = ":man_wearing_turban:", - ["\U0001f468\U0001f3fb\u200d\U0001f9b3"] = ":man_white_haired_tone1:", - ["\U0001f468\U0001f3fc\u200d\U0001f9b3"] = ":man_white_haired_tone2:", - ["\U0001f468\U0001f3fd\u200d\U0001f9b3"] = ":man_white_haired_tone3:", - ["\U0001f468\U0001f3fe\u200d\U0001f9b3"] = ":man_white_haired_tone4:", - ["\U0001f468\U0001f3ff\u200d\U0001f9b3"] = ":man_white_haired_tone5:", - ["\U0001f468\u200d\U0001f9b3"] = ":man_white_haired:", - ["\U0001f472\U0001f3fb"] = ":man_with_chinese_cap_tone1:", - ["\U0001f472\U0001f3fc"] = ":man_with_chinese_cap_tone2:", - ["\U0001f472\U0001f3fd"] = ":man_with_chinese_cap_tone3:", - ["\U0001f472\U0001f3fe"] = ":man_with_chinese_cap_tone4:", - ["\U0001f472\U0001f3ff"] = ":man_with_chinese_cap_tone5:", - ["\U0001f472"] = ":man_with_chinese_cap:", - ["\U0001f468\U0001f3fb\u200d\U0001f9af"] = ":man_with_probing_cane_tone1:", - ["\U0001f468\U0001f3fc\u200d\U0001f9af"] = ":man_with_probing_cane_tone2:", - ["\U0001f468\U0001f3fd\u200d\U0001f9af"] = ":man_with_probing_cane_tone3:", - ["\U0001f468\U0001f3fe\u200d\U0001f9af"] = ":man_with_probing_cane_tone4:", - ["\U0001f468\U0001f3ff\u200d\U0001f9af"] = ":man_with_probing_cane_tone5:", - ["\U0001f468\u200d\U0001f9af"] = ":man_with_probing_cane:", - ["\U0001f470\U0001f3fb\u200d\u2642\ufe0f"] = ":man_with_veil_tone1:", - ["\U0001f470\U0001f3fc\u200d\u2642\ufe0f"] = ":man_with_veil_tone2:", - ["\U0001f470\U0001f3fd\u200d\u2642\ufe0f"] = ":man_with_veil_tone3:", - ["\U0001f470\U0001f3fe\u200d\u2642\ufe0f"] = ":man_with_veil_tone4:", - ["\U0001f470\U0001f3ff\u200d\u2642\ufe0f"] = ":man_with_veil_tone5:", - ["\U0001f470\u200d\u2642\ufe0f"] = ":man_with_veil:", - ["\U0001f468\U0001f3fb\u200d\U0001f9af\u200d\u27a1\ufe0f"] = ":man_with_white_cane_facing_right_tone1:", - ["\U0001f468\U0001f3fc\u200d\U0001f9af\u200d\u27a1\ufe0f"] = ":man_with_white_cane_facing_right_tone2:", - ["\U0001f468\U0001f3fd\u200d\U0001f9af\u200d\u27a1\ufe0f"] = ":man_with_white_cane_facing_right_tone3:", - ["\U0001f468\U0001f3fe\u200d\U0001f9af\u200d\u27a1\ufe0f"] = ":man_with_white_cane_facing_right_tone4:", - ["\U0001f468\U0001f3ff\u200d\U0001f9af\u200d\u27a1\ufe0f"] = ":man_with_white_cane_facing_right_tone5:", - ["\U0001f468\u200d\U0001f9af\u200d\u27a1\ufe0f"] = ":man_with_white_cane_facing_right:", - ["\U0001f9df\u200d\u2642\ufe0f"] = ":man_zombie:", - ["\U0001f468"] = ":man:", - ["\U0001f96d"] = ":mango:", - ["\U0001f45e"] = ":mans_shoe:", - ["\U0001f9bd"] = ":manual_wheelchair:", - ["\U0001f5fa\ufe0f"] = ":map:", - ["\U0001f5fa"] = ":map:", - ["\U0001f341"] = ":maple_leaf:", - ["\U0001fa87"] = ":maracas:", - ["\U0001f94b"] = ":martial_arts_uniform:", - ["\U0001f637"] = ":mask:", - ["\U0001f9c9"] = ":mate:", - ["\U0001f356"] = ":meat_on_bone:", - ["\U0001f9d1\U0001f3fb\u200d\U0001f527"] = ":mechanic_tone1:", - ["\U0001f9d1\U0001f3fc\u200d\U0001f527"] = ":mechanic_tone2:", - ["\U0001f9d1\U0001f3fd\u200d\U0001f527"] = ":mechanic_tone3:", - ["\U0001f9d1\U0001f3fe\u200d\U0001f527"] = ":mechanic_tone4:", - ["\U0001f9d1\U0001f3ff\u200d\U0001f527"] = ":mechanic_tone5:", - ["\U0001f9d1\u200d\U0001f527"] = ":mechanic:", - ["\U0001f9be"] = ":mechanical_arm:", - ["\U0001f9bf"] = ":mechanical_leg:", - ["\U0001f3c5"] = ":medal:", - ["\u2695\ufe0f"] = ":medical_symbol:", - ["\u2695"] = ":medical_symbol:", - ["\U0001f4e3"] = ":mega:", - ["\U0001f348"] = ":melon:", - ["\U0001fae0"] = ":melting_face:", - ["\U0001f46f\u200d\u2642\ufe0f"] = ":men_with_bunny_ears_partying:", - ["\U0001f93c\u200d\u2642\ufe0f"] = ":men_wrestling:", - ["\u2764\ufe0f\u200d\U0001fa79"] = ":mending_heart:", - ["\U0001f54e"] = ":menorah:", - ["\U0001f6b9"] = ":mens:", - ["\U0001f9dc\U0001f3fb\u200d\u2640\ufe0f"] = ":mermaid_tone1:", - ["\U0001f9dc\U0001f3fc\u200d\u2640\ufe0f"] = ":mermaid_tone2:", - ["\U0001f9dc\U0001f3fd\u200d\u2640\ufe0f"] = ":mermaid_tone3:", - ["\U0001f9dc\U0001f3fe\u200d\u2640\ufe0f"] = ":mermaid_tone4:", - ["\U0001f9dc\U0001f3ff\u200d\u2640\ufe0f"] = ":mermaid_tone5:", - ["\U0001f9dc\u200d\u2640\ufe0f"] = ":mermaid:", - ["\U0001f9dc\U0001f3fb\u200d\u2642\ufe0f"] = ":merman_tone1:", - ["\U0001f9dc\U0001f3fc\u200d\u2642\ufe0f"] = ":merman_tone2:", - ["\U0001f9dc\U0001f3fd\u200d\u2642\ufe0f"] = ":merman_tone3:", - ["\U0001f9dc\U0001f3fe\u200d\u2642\ufe0f"] = ":merman_tone4:", - ["\U0001f9dc\U0001f3ff\u200d\u2642\ufe0f"] = ":merman_tone5:", - ["\U0001f9dc\u200d\u2642\ufe0f"] = ":merman:", - ["\U0001f9dc\U0001f3fb"] = ":merperson_tone1:", - ["\U0001f9dc\U0001f3fc"] = ":merperson_tone2:", - ["\U0001f9dc\U0001f3fd"] = ":merperson_tone3:", - ["\U0001f9dc\U0001f3fe"] = ":merperson_tone4:", - ["\U0001f9dc\U0001f3ff"] = ":merperson_tone5:", - ["\U0001f9dc"] = ":merperson:", - ["\U0001f918\U0001f3fb"] = ":metal_tone1:", - ["\U0001f918\U0001f3fc"] = ":metal_tone2:", - ["\U0001f918\U0001f3fd"] = ":metal_tone3:", - ["\U0001f918\U0001f3fe"] = ":metal_tone4:", - ["\U0001f918\U0001f3ff"] = ":metal_tone5:", - ["\U0001f918"] = ":metal:", - ["\U0001f687"] = ":metro:", - ["\U0001f9a0"] = ":microbe:", - ["\U0001f3a4"] = ":microphone:", - ["\U0001f399\ufe0f"] = ":microphone2:", - ["\U0001f399"] = ":microphone2:", - ["\U0001f52c"] = ":microscope:", - ["\U0001f595\U0001f3fb"] = ":middle_finger_tone1:", - ["\U0001f595\U0001f3fc"] = ":middle_finger_tone2:", - ["\U0001f595\U0001f3fd"] = ":middle_finger_tone3:", - ["\U0001f595\U0001f3fe"] = ":middle_finger_tone4:", - ["\U0001f595\U0001f3ff"] = ":middle_finger_tone5:", - ["\U0001f595"] = ":middle_finger:", - ["\U0001fa96"] = ":military_helmet:", - ["\U0001f396\ufe0f"] = ":military_medal:", - ["\U0001f396"] = ":military_medal:", - ["\U0001f95b"] = ":milk:", - ["\U0001f30c"] = ":milky_way:", - ["\U0001f690"] = ":minibus:", - ["\U0001f4bd"] = ":minidisc:", - ["\U0001faa9"] = ":mirror_ball:", - ["\U0001fa9e"] = ":mirror:", - ["\U0001f4f4"] = ":mobile_phone_off:", - ["\U0001f4f1"] = ":mobile_phone:", - ["\U0001f911"] = ":money_mouth:", - ["\U0001f4b8"] = ":money_with_wings:", - ["\U0001f4b0"] = ":moneybag:", - ["\U0001f435"] = ":monkey_face:", - ["\U0001f412"] = ":monkey:", - ["\U0001f69d"] = ":monorail:", - ["\U0001f96e"] = ":moon_cake:", - ["\U0001face"] = ":moose:", - ["\U0001f393"] = ":mortar_board:", - ["\U0001f54c"] = ":mosque:", - ["\U0001f99f"] = ":mosquito:", - ["\U0001f6f5"] = ":motor_scooter:", - ["\U0001f6e5\ufe0f"] = ":motorboat:", - ["\U0001f6e5"] = ":motorboat:", - ["\U0001f3cd\ufe0f"] = ":motorcycle:", - ["\U0001f3cd"] = ":motorcycle:", - ["\U0001f9bc"] = ":motorized_wheelchair:", - ["\U0001f6e3\ufe0f"] = ":motorway:", - ["\U0001f6e3"] = ":motorway:", - ["\U0001f5fb"] = ":mount_fuji:", - ["\U0001f6a0"] = ":mountain_cableway:", - ["\U0001f69e"] = ":mountain_railway:", - ["\U0001f3d4\ufe0f"] = ":mountain_snow:", - ["\U0001f3d4"] = ":mountain_snow:", - ["\u26f0\ufe0f"] = ":mountain:", - ["\u26f0"] = ":mountain:", - ["\U0001f5b1\ufe0f"] = ":mouse_three_button:", - ["\U0001f5b1"] = ":mouse_three_button:", - ["\U0001faa4"] = ":mouse_trap:", - ["\U0001f42d"] = ":mouse:", - ["\U0001f401"] = ":mouse2:", - ["\U0001f3a5"] = ":movie_camera:", - ["\U0001f5ff"] = ":moyai:", - ["\U0001f936\U0001f3fb"] = ":mrs_claus_tone1:", - ["\U0001f936\U0001f3fc"] = ":mrs_claus_tone2:", - ["\U0001f936\U0001f3fd"] = ":mrs_claus_tone3:", - ["\U0001f936\U0001f3fe"] = ":mrs_claus_tone4:", - ["\U0001f936\U0001f3ff"] = ":mrs_claus_tone5:", - ["\U0001f936"] = ":mrs_claus:", - ["\U0001f4aa\U0001f3fb"] = ":muscle_tone1:", - ["\U0001f4aa\U0001f3fc"] = ":muscle_tone2:", - ["\U0001f4aa\U0001f3fd"] = ":muscle_tone3:", - ["\U0001f4aa\U0001f3fe"] = ":muscle_tone4:", - ["\U0001f4aa\U0001f3ff"] = ":muscle_tone5:", - ["\U0001f4aa"] = ":muscle:", - ["\U0001f344"] = ":mushroom:", - ["\U0001f3b9"] = ":musical_keyboard:", - ["\U0001f3b5"] = ":musical_note:", - ["\U0001f3bc"] = ":musical_score:", - ["\U0001f507"] = ":mute:", - ["\U0001f9d1\U0001f3fb\u200d\U0001f384"] = ":mx_claus_tone1:", - ["\U0001f9d1\U0001f3fc\u200d\U0001f384"] = ":mx_claus_tone2:", - ["\U0001f9d1\U0001f3fd\u200d\U0001f384"] = ":mx_claus_tone3:", - ["\U0001f9d1\U0001f3fe\u200d\U0001f384"] = ":mx_claus_tone4:", - ["\U0001f9d1\U0001f3ff\u200d\U0001f384"] = ":mx_claus_tone5:", - ["\U0001f9d1\u200d\U0001f384"] = ":mx_claus:", - ["\U0001f485\U0001f3fb"] = ":nail_care_tone1:", - ["\U0001f485\U0001f3fc"] = ":nail_care_tone2:", - ["\U0001f485\U0001f3fd"] = ":nail_care_tone3:", - ["\U0001f485\U0001f3fe"] = ":nail_care_tone4:", - ["\U0001f485\U0001f3ff"] = ":nail_care_tone5:", - ["\U0001f485"] = ":nail_care:", - ["\U0001f4db"] = ":name_badge:", - ["\U0001f922"] = ":nauseated_face:", - ["\U0001f9ff"] = ":nazar_amulet:", - ["\U0001f454"] = ":necktie:", - ["\u274e"] = ":negative_squared_cross_mark:", - ["\U0001f913"] = ":nerd:", - ["\U0001faba"] = ":nest_with_eggs:", - ["\U0001fa86"] = ":nesting_dolls:", - ["\U0001f610"] = ":neutral_face:", - ["\U0001f31a"] = ":new_moon_with_face:", - ["\U0001f311"] = ":new_moon:", - ["\U0001f195"] = ":new:", - ["\U0001f4f0"] = ":newspaper:", - ["\U0001f5de\ufe0f"] = ":newspaper2:", - ["\U0001f5de"] = ":newspaper2:", - ["\U0001f196"] = ":ng:", - ["\U0001f303"] = ":night_with_stars:", - ["\u0039\ufe0f\u20e3"] = ":nine:", - ["\u0039\u20e3"] = ":nine:", - ["\U0001f977\U0001f3fb"] = ":ninja_tone1:", - ["\U0001f977\U0001f3fc"] = ":ninja_tone2:", - ["\U0001f977\U0001f3fd"] = ":ninja_tone3:", - ["\U0001f977\U0001f3fe"] = ":ninja_tone4:", - ["\U0001f977\U0001f3ff"] = ":ninja_tone5:", - ["\U0001f977"] = ":ninja:", - ["\U0001f515"] = ":no_bell:", - ["\U0001f6b3"] = ":no_bicycles:", - ["\U0001f6ab"] = ":no_entry_sign:", - ["\u26d4"] = ":no_entry:", - ["\U0001f4f5"] = ":no_mobile_phones:", - ["\U0001f636"] = ":no_mouth:", - ["\U0001f6b7"] = ":no_pedestrians:", - ["\U0001f6ad"] = ":no_smoking:", - ["\U0001f6b1"] = ":non_potable_water:", - ["\U0001f443\U0001f3fb"] = ":nose_tone1:", - ["\U0001f443\U0001f3fc"] = ":nose_tone2:", - ["\U0001f443\U0001f3fd"] = ":nose_tone3:", - ["\U0001f443\U0001f3fe"] = ":nose_tone4:", - ["\U0001f443\U0001f3ff"] = ":nose_tone5:", - ["\U0001f443"] = ":nose:", - ["\U0001f4d4"] = ":notebook_with_decorative_cover:", - ["\U0001f4d3"] = ":notebook:", - ["\U0001f5d2\ufe0f"] = ":notepad_spiral:", - ["\U0001f5d2"] = ":notepad_spiral:", - ["\U0001f3b6"] = ":notes:", - ["\U0001f529"] = ":nut_and_bolt:", - ["\u2b55"] = ":o:", - ["\U0001f17e\ufe0f"] = ":o2:", - ["\U0001f17e"] = ":o2:", - ["\U0001f30a"] = ":ocean:", - ["\U0001f6d1"] = ":octagonal_sign:", - ["\U0001f419"] = ":octopus:", - ["\U0001f362"] = ":oden:", - ["\U0001f9d1\U0001f3fb\u200d\U0001f4bc"] = ":office_worker_tone1:", - ["\U0001f9d1\U0001f3fc\u200d\U0001f4bc"] = ":office_worker_tone2:", - ["\U0001f9d1\U0001f3fd\u200d\U0001f4bc"] = ":office_worker_tone3:", - ["\U0001f9d1\U0001f3fe\u200d\U0001f4bc"] = ":office_worker_tone4:", - ["\U0001f9d1\U0001f3ff\u200d\U0001f4bc"] = ":office_worker_tone5:", - ["\U0001f9d1\u200d\U0001f4bc"] = ":office_worker:", - ["\U0001f3e2"] = ":office:", - ["\U0001f6e2\ufe0f"] = ":oil:", - ["\U0001f6e2"] = ":oil:", - ["\U0001f44c\U0001f3fb"] = ":ok_hand_tone1:", - ["\U0001f44c\U0001f3fc"] = ":ok_hand_tone2:", - ["\U0001f44c\U0001f3fd"] = ":ok_hand_tone3:", - ["\U0001f44c\U0001f3fe"] = ":ok_hand_tone4:", - ["\U0001f44c\U0001f3ff"] = ":ok_hand_tone5:", - ["\U0001f44c"] = ":ok_hand:", - ["\U0001f197"] = ":ok:", - ["\U0001f9d3\U0001f3fb"] = ":older_adult_tone1:", - ["\U0001f9d3\U0001f3fc"] = ":older_adult_tone2:", - ["\U0001f9d3\U0001f3fd"] = ":older_adult_tone3:", - ["\U0001f9d3\U0001f3fe"] = ":older_adult_tone4:", - ["\U0001f9d3\U0001f3ff"] = ":older_adult_tone5:", - ["\U0001f9d3"] = ":older_adult:", - ["\U0001f474\U0001f3fb"] = ":older_man_tone1:", - ["\U0001f474\U0001f3fc"] = ":older_man_tone2:", - ["\U0001f474\U0001f3fd"] = ":older_man_tone3:", - ["\U0001f474\U0001f3fe"] = ":older_man_tone4:", - ["\U0001f474\U0001f3ff"] = ":older_man_tone5:", - ["\U0001f474"] = ":older_man:", - ["\U0001f475\U0001f3fb"] = ":older_woman_tone1:", - ["\U0001f475\U0001f3fc"] = ":older_woman_tone2:", - ["\U0001f475\U0001f3fd"] = ":older_woman_tone3:", - ["\U0001f475\U0001f3fe"] = ":older_woman_tone4:", - ["\U0001f475\U0001f3ff"] = ":older_woman_tone5:", - ["\U0001f475"] = ":older_woman:", - ["\U0001fad2"] = ":olive:", - ["\U0001f549\ufe0f"] = ":om_symbol:", - ["\U0001f549"] = ":om_symbol:", - ["\U0001f51b"] = ":on:", - ["\U0001f698"] = ":oncoming_automobile:", - ["\U0001f68d"] = ":oncoming_bus:", - ["\U0001f694"] = ":oncoming_police_car:", - ["\U0001f696"] = ":oncoming_taxi:", - ["\U0001fa71"] = ":one_piece_swimsuit:", - ["\u0031\ufe0f\u20e3"] = ":one:", - ["\u0031\u20e3"] = ":one:", - ["\U0001f9c5"] = ":onion:", - ["\U0001f4c2"] = ":open_file_folder:", - ["\U0001f450\U0001f3fb"] = ":open_hands_tone1:", - ["\U0001f450\U0001f3fc"] = ":open_hands_tone2:", - ["\U0001f450\U0001f3fd"] = ":open_hands_tone3:", - ["\U0001f450\U0001f3fe"] = ":open_hands_tone4:", - ["\U0001f450\U0001f3ff"] = ":open_hands_tone5:", - ["\U0001f450"] = ":open_hands:", - ["\U0001f62e"] = ":open_mouth:", - ["\u26ce"] = ":ophiuchus:", - ["\U0001f4d9"] = ":orange_book:", - ["\U0001f7e0"] = ":orange_circle:", - ["\U0001f9e1"] = ":orange_heart:", - ["\U0001f7e7"] = ":orange_square:", - ["\U0001f9a7"] = ":orangutan:", - ["\u2626\ufe0f"] = ":orthodox_cross:", - ["\u2626"] = ":orthodox_cross:", - ["\U0001f9a6"] = ":otter:", - ["\U0001f4e4"] = ":outbox_tray:", - ["\U0001f989"] = ":owl:", - ["\U0001f402"] = ":ox:", - ["\U0001f9aa"] = ":oyster:", - ["\U0001f4e6"] = ":package:", - ["\U0001f4c4"] = ":page_facing_up:", - ["\U0001f4c3"] = ":page_with_curl:", - ["\U0001f4df"] = ":pager:", - ["\U0001f58c\ufe0f"] = ":paintbrush:", - ["\U0001f58c"] = ":paintbrush:", - ["\U0001faf3\U0001f3fb"] = ":palm_down_hand_tone1:", - ["\U0001faf3\U0001f3fc"] = ":palm_down_hand_tone2:", - ["\U0001faf3\U0001f3fd"] = ":palm_down_hand_tone3:", - ["\U0001faf3\U0001f3fe"] = ":palm_down_hand_tone4:", - ["\U0001faf3\U0001f3ff"] = ":palm_down_hand_tone5:", - ["\U0001faf3"] = ":palm_down_hand:", - ["\U0001f334"] = ":palm_tree:", - ["\U0001faf4\U0001f3fb"] = ":palm_up_hand_tone1:", - ["\U0001faf4\U0001f3fc"] = ":palm_up_hand_tone2:", - ["\U0001faf4\U0001f3fd"] = ":palm_up_hand_tone3:", - ["\U0001faf4\U0001f3fe"] = ":palm_up_hand_tone4:", - ["\U0001faf4\U0001f3ff"] = ":palm_up_hand_tone5:", - ["\U0001faf4"] = ":palm_up_hand:", - ["\U0001f932\U0001f3fb"] = ":palms_up_together_tone1:", - ["\U0001f932\U0001f3fc"] = ":palms_up_together_tone2:", - ["\U0001f932\U0001f3fd"] = ":palms_up_together_tone3:", - ["\U0001f932\U0001f3fe"] = ":palms_up_together_tone4:", - ["\U0001f932\U0001f3ff"] = ":palms_up_together_tone5:", - ["\U0001f932"] = ":palms_up_together:", - ["\U0001f95e"] = ":pancakes:", - ["\U0001f43c"] = ":panda_face:", - ["\U0001f4ce"] = ":paperclip:", - ["\U0001f587\ufe0f"] = ":paperclips:", - ["\U0001f587"] = ":paperclips:", - ["\U0001fa82"] = ":parachute:", - ["\U0001f3de\ufe0f"] = ":park:", - ["\U0001f3de"] = ":park:", - ["\U0001f17f\ufe0f"] = ":parking:", - ["\U0001f17f"] = ":parking:", - ["\U0001f99c"] = ":parrot:", - ["\u303d\ufe0f"] = ":part_alternation_mark:", - ["\u303d"] = ":part_alternation_mark:", - ["\u26c5"] = ":partly_sunny:", - ["\U0001f973"] = ":partying_face:", - ["\U0001f6c2"] = ":passport_control:", - ["\u23f8\ufe0f"] = ":pause_button:", - ["\u23f8"] = ":pause_button:", - ["\U0001fadb"] = ":pea_pod:", - ["\u262e\ufe0f"] = ":peace:", - ["\u262e"] = ":peace:", - ["\U0001f351"] = ":peach:", - ["\U0001f99a"] = ":peacock:", - ["\U0001f95c"] = ":peanuts:", - ["\U0001f350"] = ":pear:", - ["\U0001f58a\ufe0f"] = ":pen_ballpoint:", - ["\U0001f58a"] = ":pen_ballpoint:", - ["\U0001f58b\ufe0f"] = ":pen_fountain:", - ["\U0001f58b"] = ":pen_fountain:", - ["\U0001f4dd"] = ":pencil:", - ["\u270f\ufe0f"] = ":pencil2:", - ["\u270f"] = ":pencil2:", - ["\U0001f427"] = ":penguin:", - ["\U0001f614"] = ":pensive:", - ["\U0001f9d1\u200d\U0001f91d\u200d\U0001f9d1"] = ":people_holding_hands_tone5_tone4:", - ["\U0001fac2"] = ":people_hugging:", - ["\U0001f46f"] = ":people_with_bunny_ears_partying:", - ["\U0001f93c"] = ":people_wrestling:", - ["\U0001f3ad"] = ":performing_arts:", - ["\U0001f623"] = ":persevere:", - ["\U0001f9d1\u200d\U0001f9b2"] = ":person_bald:", - ["\U0001f6b4\U0001f3fb"] = ":person_biking_tone1:", - ["\U0001f6b4\U0001f3fc"] = ":person_biking_tone2:", - ["\U0001f6b4\U0001f3fd"] = ":person_biking_tone3:", - ["\U0001f6b4\U0001f3fe"] = ":person_biking_tone4:", - ["\U0001f6b4\U0001f3ff"] = ":person_biking_tone5:", - ["\U0001f6b4"] = ":person_biking:", - ["\u26f9\U0001f3fb"] = ":person_bouncing_ball_tone1:", - ["\u26f9\U0001f3fc"] = ":person_bouncing_ball_tone2:", - ["\u26f9\U0001f3fd"] = ":person_bouncing_ball_tone3:", - ["\u26f9\U0001f3fe"] = ":person_bouncing_ball_tone4:", - ["\u26f9\U0001f3ff"] = ":person_bouncing_ball_tone5:", - ["\u26f9\ufe0f"] = ":person_bouncing_ball:", - ["\u26f9"] = ":person_bouncing_ball:", - ["\U0001f647\U0001f3fb"] = ":person_bowing_tone1:", - ["\U0001f647\U0001f3fc"] = ":person_bowing_tone2:", - ["\U0001f647\U0001f3fd"] = ":person_bowing_tone3:", - ["\U0001f647\U0001f3fe"] = ":person_bowing_tone4:", - ["\U0001f647\U0001f3ff"] = ":person_bowing_tone5:", - ["\U0001f647"] = ":person_bowing:", - ["\U0001f9d7\U0001f3fb"] = ":person_climbing_tone1:", - ["\U0001f9d7\U0001f3fc"] = ":person_climbing_tone2:", - ["\U0001f9d7\U0001f3fd"] = ":person_climbing_tone3:", - ["\U0001f9d7\U0001f3fe"] = ":person_climbing_tone4:", - ["\U0001f9d7\U0001f3ff"] = ":person_climbing_tone5:", - ["\U0001f9d7"] = ":person_climbing:", - ["\U0001f9d1\u200d\U0001f9b1"] = ":person_curly_hair:", - ["\U0001f938\U0001f3fb"] = ":person_doing_cartwheel_tone1:", - ["\U0001f938\U0001f3fc"] = ":person_doing_cartwheel_tone2:", - ["\U0001f938\U0001f3fd"] = ":person_doing_cartwheel_tone3:", - ["\U0001f938\U0001f3fe"] = ":person_doing_cartwheel_tone4:", - ["\U0001f938\U0001f3ff"] = ":person_doing_cartwheel_tone5:", - ["\U0001f938"] = ":person_doing_cartwheel:", - ["\U0001f926\U0001f3fb"] = ":person_facepalming_tone1:", - ["\U0001f926\U0001f3fc"] = ":person_facepalming_tone2:", - ["\U0001f926\U0001f3fd"] = ":person_facepalming_tone3:", - ["\U0001f926\U0001f3fe"] = ":person_facepalming_tone4:", - ["\U0001f926\U0001f3ff"] = ":person_facepalming_tone5:", - ["\U0001f926"] = ":person_facepalming:", - ["\U0001f9d1\U0001f3fb\u200d\U0001f37c"] = ":person_feeding_baby_tone1:", - ["\U0001f9d1\U0001f3fc\u200d\U0001f37c"] = ":person_feeding_baby_tone2:", - ["\U0001f9d1\U0001f3fd\u200d\U0001f37c"] = ":person_feeding_baby_tone3:", - ["\U0001f9d1\U0001f3fe\u200d\U0001f37c"] = ":person_feeding_baby_tone4:", - ["\U0001f9d1\U0001f3ff\u200d\U0001f37c"] = ":person_feeding_baby_tone5:", - ["\U0001f9d1\u200d\U0001f37c"] = ":person_feeding_baby:", - ["\U0001f93a"] = ":person_fencing:", - ["\U0001f64d\U0001f3fb"] = ":person_frowning_tone1:", - ["\U0001f64d\U0001f3fc"] = ":person_frowning_tone2:", - ["\U0001f64d\U0001f3fd"] = ":person_frowning_tone3:", - ["\U0001f64d\U0001f3fe"] = ":person_frowning_tone4:", - ["\U0001f64d\U0001f3ff"] = ":person_frowning_tone5:", - ["\U0001f64d"] = ":person_frowning:", - ["\U0001f645\U0001f3fb"] = ":person_gesturing_no_tone1:", - ["\U0001f645\U0001f3fc"] = ":person_gesturing_no_tone2:", - ["\U0001f645\U0001f3fd"] = ":person_gesturing_no_tone3:", - ["\U0001f645\U0001f3fe"] = ":person_gesturing_no_tone4:", - ["\U0001f645\U0001f3ff"] = ":person_gesturing_no_tone5:", - ["\U0001f645"] = ":person_gesturing_no:", - ["\U0001f646\U0001f3fb"] = ":person_gesturing_ok_tone1:", - ["\U0001f646\U0001f3fc"] = ":person_gesturing_ok_tone2:", - ["\U0001f646\U0001f3fd"] = ":person_gesturing_ok_tone3:", - ["\U0001f646\U0001f3fe"] = ":person_gesturing_ok_tone4:", - ["\U0001f646\U0001f3ff"] = ":person_gesturing_ok_tone5:", - ["\U0001f646"] = ":person_gesturing_ok:", - ["\U0001f487\U0001f3fb"] = ":person_getting_haircut_tone1:", - ["\U0001f487\U0001f3fc"] = ":person_getting_haircut_tone2:", - ["\U0001f487\U0001f3fd"] = ":person_getting_haircut_tone3:", - ["\U0001f487\U0001f3fe"] = ":person_getting_haircut_tone4:", - ["\U0001f487\U0001f3ff"] = ":person_getting_haircut_tone5:", - ["\U0001f487"] = ":person_getting_haircut:", - ["\U0001f486\U0001f3fb"] = ":person_getting_massage_tone1:", - ["\U0001f486\U0001f3fc"] = ":person_getting_massage_tone2:", - ["\U0001f486\U0001f3fd"] = ":person_getting_massage_tone3:", - ["\U0001f486\U0001f3fe"] = ":person_getting_massage_tone4:", - ["\U0001f486\U0001f3ff"] = ":person_getting_massage_tone5:", - ["\U0001f486"] = ":person_getting_massage:", - ["\U0001f3cc\U0001f3fb"] = ":person_golfing_tone1:", - ["\U0001f3cc\U0001f3fc"] = ":person_golfing_tone2:", - ["\U0001f3cc\U0001f3fd"] = ":person_golfing_tone3:", - ["\U0001f3cc\U0001f3fe"] = ":person_golfing_tone4:", - ["\U0001f3cc\U0001f3ff"] = ":person_golfing_tone5:", - ["\U0001f3cc\ufe0f"] = ":person_golfing:", - ["\U0001f3cc"] = ":person_golfing:", - ["\U0001f6cc\U0001f3fb"] = ":person_in_bed_tone1:", - ["\U0001f6cc\U0001f3fc"] = ":person_in_bed_tone2:", - ["\U0001f6cc\U0001f3fd"] = ":person_in_bed_tone3:", - ["\U0001f6cc\U0001f3fe"] = ":person_in_bed_tone4:", - ["\U0001f6cc\U0001f3ff"] = ":person_in_bed_tone5:", - ["\U0001f9d8\U0001f3fb"] = ":person_in_lotus_position_tone1:", - ["\U0001f9d8\U0001f3fc"] = ":person_in_lotus_position_tone2:", - ["\U0001f9d8\U0001f3fd"] = ":person_in_lotus_position_tone3:", - ["\U0001f9d8\U0001f3fe"] = ":person_in_lotus_position_tone4:", - ["\U0001f9d8\U0001f3ff"] = ":person_in_lotus_position_tone5:", - ["\U0001f9d8"] = ":person_in_lotus_position:", - ["\U0001f9d1\U0001f3fb\u200d\U0001f9bd\u200d\u27a1\ufe0f"] = ":person_in_manual_wheelchair_facing_right_tone1:", - ["\U0001f9d1\U0001f3fc\u200d\U0001f9bd\u200d\u27a1\ufe0f"] = ":person_in_manual_wheelchair_facing_right_tone2:", - ["\U0001f9d1\U0001f3fd\u200d\U0001f9bd\u200d\u27a1\ufe0f"] = ":person_in_manual_wheelchair_facing_right_tone3:", - ["\U0001f9d1\U0001f3fe\u200d\U0001f9bd\u200d\u27a1\ufe0f"] = ":person_in_manual_wheelchair_facing_right_tone4:", - ["\U0001f9d1\U0001f3ff\u200d\U0001f9bd\u200d\u27a1\ufe0f"] = ":person_in_manual_wheelchair_facing_right_tone5:", - ["\U0001f9d1\u200d\U0001f9bd\u200d\u27a1\ufe0f"] = ":person_in_manual_wheelchair_facing_right:", - ["\U0001f9d1\U0001f3fb\u200d\U0001f9bd"] = ":person_in_manual_wheelchair_tone1:", - ["\U0001f9d1\U0001f3fc\u200d\U0001f9bd"] = ":person_in_manual_wheelchair_tone2:", - ["\U0001f9d1\U0001f3fd\u200d\U0001f9bd"] = ":person_in_manual_wheelchair_tone3:", - ["\U0001f9d1\U0001f3fe\u200d\U0001f9bd"] = ":person_in_manual_wheelchair_tone4:", - ["\U0001f9d1\U0001f3ff\u200d\U0001f9bd"] = ":person_in_manual_wheelchair_tone5:", - ["\U0001f9d1\u200d\U0001f9bd"] = ":person_in_manual_wheelchair:", - ["\U0001f9d1\U0001f3fb\u200d\U0001f9bc\u200d\u27a1\ufe0f"] = ":person_in_motorized_wheelchair_facing_right_tone1:", - ["\U0001f9d1\U0001f3fc\u200d\U0001f9bc\u200d\u27a1\ufe0f"] = ":person_in_motorized_wheelchair_facing_right_tone2:", - ["\U0001f9d1\U0001f3fd\u200d\U0001f9bc\u200d\u27a1\ufe0f"] = ":person_in_motorized_wheelchair_facing_right_tone3:", - ["\U0001f9d1\U0001f3fe\u200d\U0001f9bc\u200d\u27a1\ufe0f"] = ":person_in_motorized_wheelchair_facing_right_tone4:", - ["\U0001f9d1\U0001f3ff\u200d\U0001f9bc\u200d\u27a1\ufe0f"] = ":person_in_motorized_wheelchair_facing_right_tone5:", - ["\U0001f9d1\u200d\U0001f9bc\u200d\u27a1\ufe0f"] = ":person_in_motorized_wheelchair_facing_right:", - ["\U0001f9d1\U0001f3fb\u200d\U0001f9bc"] = ":person_in_motorized_wheelchair_tone1:", - ["\U0001f9d1\U0001f3fc\u200d\U0001f9bc"] = ":person_in_motorized_wheelchair_tone2:", - ["\U0001f9d1\U0001f3fd\u200d\U0001f9bc"] = ":person_in_motorized_wheelchair_tone3:", - ["\U0001f9d1\U0001f3fe\u200d\U0001f9bc"] = ":person_in_motorized_wheelchair_tone4:", - ["\U0001f9d1\U0001f3ff\u200d\U0001f9bc"] = ":person_in_motorized_wheelchair_tone5:", - ["\U0001f9d1\u200d\U0001f9bc"] = ":person_in_motorized_wheelchair:", - ["\U0001f9d6\U0001f3fb"] = ":person_in_steamy_room_tone1:", - ["\U0001f9d6\U0001f3fc"] = ":person_in_steamy_room_tone2:", - ["\U0001f9d6\U0001f3fd"] = ":person_in_steamy_room_tone3:", - ["\U0001f9d6\U0001f3fe"] = ":person_in_steamy_room_tone4:", - ["\U0001f9d6\U0001f3ff"] = ":person_in_steamy_room_tone5:", - ["\U0001f9d6"] = ":person_in_steamy_room:", - ["\U0001f935\U0001f3fb"] = ":person_in_tuxedo_tone1:", - ["\U0001f935\U0001f3fc"] = ":person_in_tuxedo_tone2:", - ["\U0001f935\U0001f3fd"] = ":person_in_tuxedo_tone3:", - ["\U0001f935\U0001f3fe"] = ":person_in_tuxedo_tone4:", - ["\U0001f935\U0001f3ff"] = ":person_in_tuxedo_tone5:", - ["\U0001f935"] = ":person_in_tuxedo:", - ["\U0001f939\U0001f3fb"] = ":person_juggling_tone1:", - ["\U0001f939\U0001f3fc"] = ":person_juggling_tone2:", - ["\U0001f939\U0001f3fd"] = ":person_juggling_tone3:", - ["\U0001f939\U0001f3fe"] = ":person_juggling_tone4:", - ["\U0001f939\U0001f3ff"] = ":person_juggling_tone5:", - ["\U0001f939"] = ":person_juggling:", - ["\U0001f9ce\U0001f3fb\u200d\u27a1\ufe0f"] = ":person_kneeling_facing_right_tone1:", - ["\U0001f9ce\U0001f3fc\u200d\u27a1\ufe0f"] = ":person_kneeling_facing_right_tone2:", - ["\U0001f9ce\U0001f3fd\u200d\u27a1\ufe0f"] = ":person_kneeling_facing_right_tone3:", - ["\U0001f9ce\U0001f3fe\u200d\u27a1\ufe0f"] = ":person_kneeling_facing_right_tone4:", - ["\U0001f9ce\U0001f3ff\u200d\u27a1\ufe0f"] = ":person_kneeling_facing_right_tone5:", - ["\U0001f9ce\u200d\u27a1\ufe0f"] = ":person_kneeling_facing_right:", - ["\U0001f9ce\U0001f3fb"] = ":person_kneeling_tone1:", - ["\U0001f9ce\U0001f3fc"] = ":person_kneeling_tone2:", - ["\U0001f9ce\U0001f3fd"] = ":person_kneeling_tone3:", - ["\U0001f9ce\U0001f3fe"] = ":person_kneeling_tone4:", - ["\U0001f9ce\U0001f3ff"] = ":person_kneeling_tone5:", - ["\U0001f9ce"] = ":person_kneeling:", - ["\U0001f3cb\U0001f3fb"] = ":person_lifting_weights_tone1:", - ["\U0001f3cb\U0001f3fc"] = ":person_lifting_weights_tone2:", - ["\U0001f3cb\U0001f3fd"] = ":person_lifting_weights_tone3:", - ["\U0001f3cb\U0001f3fe"] = ":person_lifting_weights_tone4:", - ["\U0001f3cb\U0001f3ff"] = ":person_lifting_weights_tone5:", - ["\U0001f3cb\ufe0f"] = ":person_lifting_weights:", - ["\U0001f3cb"] = ":person_lifting_weights:", - ["\U0001f6b5\U0001f3fb"] = ":person_mountain_biking_tone1:", - ["\U0001f6b5\U0001f3fc"] = ":person_mountain_biking_tone2:", - ["\U0001f6b5\U0001f3fd"] = ":person_mountain_biking_tone3:", - ["\U0001f6b5\U0001f3fe"] = ":person_mountain_biking_tone4:", - ["\U0001f6b5\U0001f3ff"] = ":person_mountain_biking_tone5:", - ["\U0001f6b5"] = ":person_mountain_biking:", - ["\U0001f93e\U0001f3fb"] = ":person_playing_handball_tone1:", - ["\U0001f93e\U0001f3fc"] = ":person_playing_handball_tone2:", - ["\U0001f93e\U0001f3fd"] = ":person_playing_handball_tone3:", - ["\U0001f93e\U0001f3fe"] = ":person_playing_handball_tone4:", - ["\U0001f93e\U0001f3ff"] = ":person_playing_handball_tone5:", - ["\U0001f93e"] = ":person_playing_handball:", - ["\U0001f93d\U0001f3fb"] = ":person_playing_water_polo_tone1:", - ["\U0001f93d\U0001f3fc"] = ":person_playing_water_polo_tone2:", - ["\U0001f93d\U0001f3fd"] = ":person_playing_water_polo_tone3:", - ["\U0001f93d\U0001f3fe"] = ":person_playing_water_polo_tone4:", - ["\U0001f93d\U0001f3ff"] = ":person_playing_water_polo_tone5:", - ["\U0001f93d"] = ":person_playing_water_polo:", - ["\U0001f64e\U0001f3fb"] = ":person_pouting_tone1:", - ["\U0001f64e\U0001f3fc"] = ":person_pouting_tone2:", - ["\U0001f64e\U0001f3fd"] = ":person_pouting_tone3:", - ["\U0001f64e\U0001f3fe"] = ":person_pouting_tone4:", - ["\U0001f64e\U0001f3ff"] = ":person_pouting_tone5:", - ["\U0001f64e"] = ":person_pouting:", - ["\U0001f64b\U0001f3fb"] = ":person_raising_hand_tone1:", - ["\U0001f64b\U0001f3fc"] = ":person_raising_hand_tone2:", - ["\U0001f64b\U0001f3fd"] = ":person_raising_hand_tone3:", - ["\U0001f64b\U0001f3fe"] = ":person_raising_hand_tone4:", - ["\U0001f64b\U0001f3ff"] = ":person_raising_hand_tone5:", - ["\U0001f64b"] = ":person_raising_hand:", - ["\U0001f9d1\u200d\U0001f9b0"] = ":person_red_hair:", - ["\U0001f6a3\U0001f3fb"] = ":person_rowing_boat_tone1:", - ["\U0001f6a3\U0001f3fc"] = ":person_rowing_boat_tone2:", - ["\U0001f6a3\U0001f3fd"] = ":person_rowing_boat_tone3:", - ["\U0001f6a3\U0001f3fe"] = ":person_rowing_boat_tone4:", - ["\U0001f6a3\U0001f3ff"] = ":person_rowing_boat_tone5:", - ["\U0001f6a3"] = ":person_rowing_boat:", - ["\U0001f3c3\U0001f3fb\u200d\u27a1\ufe0f"] = ":person_running_facing_right_tone1:", - ["\U0001f3c3\U0001f3fc\u200d\u27a1\ufe0f"] = ":person_running_facing_right_tone2:", - ["\U0001f3c3\U0001f3fd\u200d\u27a1\ufe0f"] = ":person_running_facing_right_tone3:", - ["\U0001f3c3\U0001f3fe\u200d\u27a1\ufe0f"] = ":person_running_facing_right_tone4:", - ["\U0001f3c3\U0001f3ff\u200d\u27a1\ufe0f"] = ":person_running_facing_right_tone5:", - ["\U0001f3c3\u200d\u27a1\ufe0f"] = ":person_running_facing_right:", - ["\U0001f3c3\U0001f3fb"] = ":person_running_tone1:", - ["\U0001f3c3\U0001f3fc"] = ":person_running_tone2:", - ["\U0001f3c3\U0001f3fd"] = ":person_running_tone3:", - ["\U0001f3c3\U0001f3fe"] = ":person_running_tone4:", - ["\U0001f3c3\U0001f3ff"] = ":person_running_tone5:", - ["\U0001f3c3"] = ":person_running:", - ["\U0001f937\U0001f3fb"] = ":person_shrugging_tone1:", - ["\U0001f937\U0001f3fc"] = ":person_shrugging_tone2:", - ["\U0001f937\U0001f3fd"] = ":person_shrugging_tone3:", - ["\U0001f937\U0001f3fe"] = ":person_shrugging_tone4:", - ["\U0001f937\U0001f3ff"] = ":person_shrugging_tone5:", - ["\U0001f937"] = ":person_shrugging:", - ["\U0001f9cd\U0001f3fb"] = ":person_standing_tone1:", - ["\U0001f9cd\U0001f3fc"] = ":person_standing_tone2:", - ["\U0001f9cd\U0001f3fd"] = ":person_standing_tone3:", - ["\U0001f9cd\U0001f3fe"] = ":person_standing_tone4:", - ["\U0001f9cd\U0001f3ff"] = ":person_standing_tone5:", - ["\U0001f9cd"] = ":person_standing:", - ["\U0001f3c4\U0001f3fb"] = ":person_surfing_tone1:", - ["\U0001f3c4\U0001f3fc"] = ":person_surfing_tone2:", - ["\U0001f3c4\U0001f3fd"] = ":person_surfing_tone3:", - ["\U0001f3c4\U0001f3fe"] = ":person_surfing_tone4:", - ["\U0001f3c4\U0001f3ff"] = ":person_surfing_tone5:", - ["\U0001f3c4"] = ":person_surfing:", - ["\U0001f3ca\U0001f3fb"] = ":person_swimming_tone1:", - ["\U0001f3ca\U0001f3fc"] = ":person_swimming_tone2:", - ["\U0001f3ca\U0001f3fd"] = ":person_swimming_tone3:", - ["\U0001f3ca\U0001f3fe"] = ":person_swimming_tone4:", - ["\U0001f3ca\U0001f3ff"] = ":person_swimming_tone5:", - ["\U0001f3ca"] = ":person_swimming:", - ["\U0001f481\U0001f3fb"] = ":person_tipping_hand_tone1:", - ["\U0001f481\U0001f3fc"] = ":person_tipping_hand_tone2:", - ["\U0001f481\U0001f3fd"] = ":person_tipping_hand_tone3:", - ["\U0001f481\U0001f3fe"] = ":person_tipping_hand_tone4:", - ["\U0001f481\U0001f3ff"] = ":person_tipping_hand_tone5:", - ["\U0001f481"] = ":person_tipping_hand:", - ["\U0001f9d1\U0001f3fb\u200d\U0001f9b2"] = ":person_tone1_bald:", - ["\U0001f9d1\U0001f3fb\u200d\U0001f9b1"] = ":person_tone1_curly_hair:", - ["\U0001f9d1\U0001f3fb\u200d\U0001f9b0"] = ":person_tone1_red_hair:", - ["\U0001f9d1\U0001f3fb\u200d\U0001f9b3"] = ":person_tone1_white_hair:", - ["\U0001f9d1\U0001f3fc\u200d\U0001f9b2"] = ":person_tone2_bald:", - ["\U0001f9d1\U0001f3fc\u200d\U0001f9b1"] = ":person_tone2_curly_hair:", - ["\U0001f9d1\U0001f3fc\u200d\U0001f9b0"] = ":person_tone2_red_hair:", - ["\U0001f9d1\U0001f3fc\u200d\U0001f9b3"] = ":person_tone2_white_hair:", - ["\U0001f9d1\U0001f3fd\u200d\U0001f9b2"] = ":person_tone3_bald:", - ["\U0001f9d1\U0001f3fd\u200d\U0001f9b1"] = ":person_tone3_curly_hair:", - ["\U0001f9d1\U0001f3fd\u200d\U0001f9b0"] = ":person_tone3_red_hair:", - ["\U0001f9d1\U0001f3fd\u200d\U0001f9b3"] = ":person_tone3_white_hair:", - ["\U0001f9d1\U0001f3fe\u200d\U0001f9b2"] = ":person_tone4_bald:", - ["\U0001f9d1\U0001f3fe\u200d\U0001f9b1"] = ":person_tone4_curly_hair:", - ["\U0001f9d1\U0001f3fe\u200d\U0001f9b0"] = ":person_tone4_red_hair:", - ["\U0001f9d1\U0001f3fe\u200d\U0001f9b3"] = ":person_tone4_white_hair:", - ["\U0001f9d1\U0001f3ff\u200d\U0001f9b2"] = ":person_tone5_bald:", - ["\U0001f9d1\U0001f3ff\u200d\U0001f9b1"] = ":person_tone5_curly_hair:", - ["\U0001f9d1\U0001f3ff\u200d\U0001f9b0"] = ":person_tone5_red_hair:", - ["\U0001f9d1\U0001f3ff\u200d\U0001f9b3"] = ":person_tone5_white_hair:", - ["\U0001f6b6\U0001f3fb\u200d\u27a1\ufe0f"] = ":person_walking_facing_right_tone1:", - ["\U0001f6b6\U0001f3fc\u200d\u27a1\ufe0f"] = ":person_walking_facing_right_tone2:", - ["\U0001f6b6\U0001f3fd\u200d\u27a1\ufe0f"] = ":person_walking_facing_right_tone3:", - ["\U0001f6b6\U0001f3fe\u200d\u27a1\ufe0f"] = ":person_walking_facing_right_tone4:", - ["\U0001f6b6\U0001f3ff\u200d\u27a1\ufe0f"] = ":person_walking_facing_right_tone5:", - ["\U0001f6b6\u200d\u27a1\ufe0f"] = ":person_walking_facing_right:", - ["\U0001f6b6\U0001f3fb"] = ":person_walking_tone1:", - ["\U0001f6b6\U0001f3fc"] = ":person_walking_tone2:", - ["\U0001f6b6\U0001f3fd"] = ":person_walking_tone3:", - ["\U0001f6b6\U0001f3fe"] = ":person_walking_tone4:", - ["\U0001f6b6\U0001f3ff"] = ":person_walking_tone5:", - ["\U0001f6b6"] = ":person_walking:", - ["\U0001f473\U0001f3fb"] = ":person_wearing_turban_tone1:", - ["\U0001f473\U0001f3fc"] = ":person_wearing_turban_tone2:", - ["\U0001f473\U0001f3fd"] = ":person_wearing_turban_tone3:", - ["\U0001f473\U0001f3fe"] = ":person_wearing_turban_tone4:", - ["\U0001f473\U0001f3ff"] = ":person_wearing_turban_tone5:", - ["\U0001f473"] = ":person_wearing_turban:", - ["\U0001f9d1\u200d\U0001f9b3"] = ":person_white_hair:", - ["\U0001fac5\U0001f3fb"] = ":person_with_crown_tone1:", - ["\U0001fac5\U0001f3fc"] = ":person_with_crown_tone2:", - ["\U0001fac5\U0001f3fd"] = ":person_with_crown_tone3:", - ["\U0001fac5\U0001f3fe"] = ":person_with_crown_tone4:", - ["\U0001fac5\U0001f3ff"] = ":person_with_crown_tone5:", - ["\U0001fac5"] = ":person_with_crown:", - ["\U0001f9d1\U0001f3fb\u200d\U0001f9af"] = ":person_with_probing_cane_tone1:", - ["\U0001f9d1\U0001f3fc\u200d\U0001f9af"] = ":person_with_probing_cane_tone2:", - ["\U0001f9d1\U0001f3fd\u200d\U0001f9af"] = ":person_with_probing_cane_tone3:", - ["\U0001f9d1\U0001f3fe\u200d\U0001f9af"] = ":person_with_probing_cane_tone4:", - ["\U0001f9d1\U0001f3ff\u200d\U0001f9af"] = ":person_with_probing_cane_tone5:", - ["\U0001f9d1\u200d\U0001f9af"] = ":person_with_probing_cane:", - ["\U0001f470\U0001f3fb"] = ":person_with_veil_tone1:", - ["\U0001f470\U0001f3fc"] = ":person_with_veil_tone2:", - ["\U0001f470\U0001f3fd"] = ":person_with_veil_tone3:", - ["\U0001f470\U0001f3fe"] = ":person_with_veil_tone4:", - ["\U0001f470\U0001f3ff"] = ":person_with_veil_tone5:", - ["\U0001f470"] = ":person_with_veil:", - ["\U0001f9d1\U0001f3fb\u200d\U0001f9af\u200d\u27a1\ufe0f"] = ":person_with_white_cane_facing_right_tone1:", - ["\U0001f9d1\U0001f3fc\u200d\U0001f9af\u200d\u27a1\ufe0f"] = ":person_with_white_cane_facing_right_tone2:", - ["\U0001f9d1\U0001f3fd\u200d\U0001f9af\u200d\u27a1\ufe0f"] = ":person_with_white_cane_facing_right_tone3:", - ["\U0001f9d1\U0001f3fe\u200d\U0001f9af\u200d\u27a1\ufe0f"] = ":person_with_white_cane_facing_right_tone4:", - ["\U0001f9d1\U0001f3ff\u200d\U0001f9af\u200d\u27a1\ufe0f"] = ":person_with_white_cane_facing_right_tone5:", - ["\U0001f9d1\u200d\U0001f9af\u200d\u27a1\ufe0f"] = ":person_with_white_cane_facing_right:", - ["\U0001f9eb"] = ":petri_dish:", - ["\U0001f426\u200d\U0001f525"] = ":phoenix:", - ["\u26cf\ufe0f"] = ":pick:", - ["\u26cf"] = ":pick:", - ["\U0001f6fb"] = ":pickup_truck:", - ["\U0001f967"] = ":pie:", - ["\U0001f43d"] = ":pig_nose:", - ["\U0001f437"] = ":pig:", - ["\U0001f416"] = ":pig2:", - ["\U0001f48a"] = ":pill:", - ["\U0001f9d1\U0001f3fb\u200d\u2708\ufe0f"] = ":pilot_tone1:", - ["\U0001f9d1\U0001f3fc\u200d\u2708\ufe0f"] = ":pilot_tone2:", - ["\U0001f9d1\U0001f3fd\u200d\u2708\ufe0f"] = ":pilot_tone3:", - ["\U0001f9d1\U0001f3fe\u200d\u2708\ufe0f"] = ":pilot_tone4:", - ["\U0001f9d1\U0001f3ff\u200d\u2708\ufe0f"] = ":pilot_tone5:", - ["\U0001f9d1\u200d\u2708\ufe0f"] = ":pilot:", - ["\U0001fa85"] = ":piñata:", - ["\U0001f90c\U0001f3fb"] = ":pinched_fingers_tone1:", - ["\U0001f90c\U0001f3fc"] = ":pinched_fingers_tone2:", - ["\U0001f90c\U0001f3fd"] = ":pinched_fingers_tone3:", - ["\U0001f90c\U0001f3fe"] = ":pinched_fingers_tone4:", - ["\U0001f90c\U0001f3ff"] = ":pinched_fingers_tone5:", - ["\U0001f90c"] = ":pinched_fingers:", - ["\U0001f90f\U0001f3fb"] = ":pinching_hand_tone1:", - ["\U0001f90f\U0001f3fc"] = ":pinching_hand_tone2:", - ["\U0001f90f\U0001f3fd"] = ":pinching_hand_tone3:", - ["\U0001f90f\U0001f3fe"] = ":pinching_hand_tone4:", - ["\U0001f90f\U0001f3ff"] = ":pinching_hand_tone5:", - ["\U0001f90f"] = ":pinching_hand:", - ["\U0001f34d"] = ":pineapple:", - ["\U0001f3d3"] = ":ping_pong:", - ["\U0001fa77"] = ":pink_heart:", - ["\U0001f3f4\u200d\u2620\ufe0f"] = ":pirate_flag:", - ["\u2653"] = ":pisces:", - ["\U0001f355"] = ":pizza:", - ["\U0001faa7"] = ":placard:", - ["\U0001f6d0"] = ":place_of_worship:", - ["\u23ef\ufe0f"] = ":play_pause:", - ["\u23ef"] = ":play_pause:", - ["\U0001f6dd"] = ":playground_slide:", - ["\U0001f97a"] = ":pleading_face:", - ["\U0001faa0"] = ":plunger:", - ["\U0001f447\U0001f3fb"] = ":point_down_tone1:", - ["\U0001f447\U0001f3fc"] = ":point_down_tone2:", - ["\U0001f447\U0001f3fd"] = ":point_down_tone3:", - ["\U0001f447\U0001f3fe"] = ":point_down_tone4:", - ["\U0001f447\U0001f3ff"] = ":point_down_tone5:", - ["\U0001f447"] = ":point_down:", - ["\U0001f448\U0001f3fb"] = ":point_left_tone1:", - ["\U0001f448\U0001f3fc"] = ":point_left_tone2:", - ["\U0001f448\U0001f3fd"] = ":point_left_tone3:", - ["\U0001f448\U0001f3fe"] = ":point_left_tone4:", - ["\U0001f448\U0001f3ff"] = ":point_left_tone5:", - ["\U0001f448"] = ":point_left:", - ["\U0001f449\U0001f3fb"] = ":point_right_tone1:", - ["\U0001f449\U0001f3fc"] = ":point_right_tone2:", - ["\U0001f449\U0001f3fd"] = ":point_right_tone3:", - ["\U0001f449\U0001f3fe"] = ":point_right_tone4:", - ["\U0001f449\U0001f3ff"] = ":point_right_tone5:", - ["\U0001f449"] = ":point_right:", - ["\U0001f446\U0001f3fb"] = ":point_up_2_tone1:", - ["\U0001f446\U0001f3fc"] = ":point_up_2_tone2:", - ["\U0001f446\U0001f3fd"] = ":point_up_2_tone3:", - ["\U0001f446\U0001f3fe"] = ":point_up_2_tone4:", - ["\U0001f446\U0001f3ff"] = ":point_up_2_tone5:", - ["\U0001f446"] = ":point_up_2:", - ["\u261d\U0001f3fb"] = ":point_up_tone1:", - ["\u261d\U0001f3fc"] = ":point_up_tone2:", - ["\u261d\U0001f3fd"] = ":point_up_tone3:", - ["\u261d\U0001f3fe"] = ":point_up_tone4:", - ["\u261d\U0001f3ff"] = ":point_up_tone5:", - ["\u261d\ufe0f"] = ":point_up:", - ["\u261d"] = ":point_up:", - ["\U0001f43b\u200d\u2744\ufe0f"] = ":polar_bear:", - ["\U0001f693"] = ":police_car:", - ["\U0001f46e\U0001f3fb"] = ":police_officer_tone1:", - ["\U0001f46e\U0001f3fc"] = ":police_officer_tone2:", - ["\U0001f46e\U0001f3fd"] = ":police_officer_tone3:", - ["\U0001f46e\U0001f3fe"] = ":police_officer_tone4:", - ["\U0001f46e\U0001f3ff"] = ":police_officer_tone5:", - ["\U0001f46e"] = ":police_officer:", - ["\U0001f429"] = ":poodle:", - ["\U0001f4a9"] = ":poop:", - ["\U0001f37f"] = ":popcorn:", - ["\U0001f3e3"] = ":post_office:", - ["\U0001f4ef"] = ":postal_horn:", - ["\U0001f4ee"] = ":postbox:", - ["\U0001f6b0"] = ":potable_water:", - ["\U0001f954"] = ":potato:", - ["\U0001fab4"] = ":potted_plant:", - ["\U0001f45d"] = ":pouch:", - ["\U0001f357"] = ":poultry_leg:", - ["\U0001f4b7"] = ":pound:", - ["\U0001fad7"] = ":pouring_liquid:", - ["\U0001f63e"] = ":pouting_cat:", - ["\U0001f64f\U0001f3fb"] = ":pray_tone1:", - ["\U0001f64f\U0001f3fc"] = ":pray_tone2:", - ["\U0001f64f\U0001f3fd"] = ":pray_tone3:", - ["\U0001f64f\U0001f3fe"] = ":pray_tone4:", - ["\U0001f64f\U0001f3ff"] = ":pray_tone5:", - ["\U0001f64f"] = ":pray:", - ["\U0001f4ff"] = ":prayer_beads:", - ["\U0001fac3\U0001f3fb"] = ":pregnant_man_tone1:", - ["\U0001fac3\U0001f3fc"] = ":pregnant_man_tone2:", - ["\U0001fac3\U0001f3fd"] = ":pregnant_man_tone3:", - ["\U0001fac3\U0001f3fe"] = ":pregnant_man_tone4:", - ["\U0001fac3\U0001f3ff"] = ":pregnant_man_tone5:", - ["\U0001fac3"] = ":pregnant_man:", - ["\U0001fac4\U0001f3fb"] = ":pregnant_person_tone1:", - ["\U0001fac4\U0001f3fc"] = ":pregnant_person_tone2:", - ["\U0001fac4\U0001f3fd"] = ":pregnant_person_tone3:", - ["\U0001fac4\U0001f3fe"] = ":pregnant_person_tone4:", - ["\U0001fac4\U0001f3ff"] = ":pregnant_person_tone5:", - ["\U0001fac4"] = ":pregnant_person:", - ["\U0001f930\U0001f3fb"] = ":pregnant_woman_tone1:", - ["\U0001f930\U0001f3fc"] = ":pregnant_woman_tone2:", - ["\U0001f930\U0001f3fd"] = ":pregnant_woman_tone3:", - ["\U0001f930\U0001f3fe"] = ":pregnant_woman_tone4:", - ["\U0001f930\U0001f3ff"] = ":pregnant_woman_tone5:", - ["\U0001f930"] = ":pregnant_woman:", - ["\U0001f968"] = ":pretzel:", - ["\U0001f934\U0001f3fb"] = ":prince_tone1:", - ["\U0001f934\U0001f3fc"] = ":prince_tone2:", - ["\U0001f934\U0001f3fd"] = ":prince_tone3:", - ["\U0001f934\U0001f3fe"] = ":prince_tone4:", - ["\U0001f934\U0001f3ff"] = ":prince_tone5:", - ["\U0001f934"] = ":prince:", - ["\U0001f478\U0001f3fb"] = ":princess_tone1:", - ["\U0001f478\U0001f3fc"] = ":princess_tone2:", - ["\U0001f478\U0001f3fd"] = ":princess_tone3:", - ["\U0001f478\U0001f3fe"] = ":princess_tone4:", - ["\U0001f478\U0001f3ff"] = ":princess_tone5:", - ["\U0001f478"] = ":princess:", - ["\U0001f5a8\ufe0f"] = ":printer:", - ["\U0001f5a8"] = ":printer:", - ["\U0001f9af"] = ":probing_cane:", - ["\U0001f4fd\ufe0f"] = ":projector:", - ["\U0001f4fd"] = ":projector:", - ["\U0001f44a\U0001f3fb"] = ":punch_tone1:", - ["\U0001f44a\U0001f3fc"] = ":punch_tone2:", - ["\U0001f44a\U0001f3fd"] = ":punch_tone3:", - ["\U0001f44a\U0001f3fe"] = ":punch_tone4:", - ["\U0001f44a\U0001f3ff"] = ":punch_tone5:", - ["\U0001f44a"] = ":punch:", - ["\U0001f7e3"] = ":purple_circle:", - ["\U0001f49c"] = ":purple_heart:", - ["\U0001f7ea"] = ":purple_square:", - ["\U0001f45b"] = ":purse:", - ["\U0001f4cc"] = ":pushpin:", - ["\U0001f6ae"] = ":put_litter_in_its_place:", - ["\u2753"] = ":question:", - ["\U0001f430"] = ":rabbit:", - ["\U0001f407"] = ":rabbit2:", - ["\U0001f99d"] = ":raccoon:", - ["\U0001f3ce\ufe0f"] = ":race_car:", - ["\U0001f3ce"] = ":race_car:", - ["\U0001f40e"] = ":racehorse:", - ["\U0001f518"] = ":radio_button:", - ["\U0001f4fb"] = ":radio:", - ["\u2622\ufe0f"] = ":radioactive:", - ["\u2622"] = ":radioactive:", - ["\U0001f621"] = ":rage:", - ["\U0001f683"] = ":railway_car:", - ["\U0001f6e4\ufe0f"] = ":railway_track:", - ["\U0001f6e4"] = ":railway_track:", - ["\U0001f3f3\ufe0f\u200d\U0001f308"] = ":rainbow_flag:", - ["\U0001f308"] = ":rainbow:", - ["\U0001f91a\U0001f3fb"] = ":raised_back_of_hand_tone1:", - ["\U0001f91a\U0001f3fc"] = ":raised_back_of_hand_tone2:", - ["\U0001f91a\U0001f3fd"] = ":raised_back_of_hand_tone3:", - ["\U0001f91a\U0001f3fe"] = ":raised_back_of_hand_tone4:", - ["\U0001f91a\U0001f3ff"] = ":raised_back_of_hand_tone5:", - ["\U0001f91a"] = ":raised_back_of_hand:", - ["\u270b\U0001f3fb"] = ":raised_hand_tone1:", - ["\u270b\U0001f3fc"] = ":raised_hand_tone2:", - ["\u270b\U0001f3fd"] = ":raised_hand_tone3:", - ["\u270b\U0001f3fe"] = ":raised_hand_tone4:", - ["\u270b\U0001f3ff"] = ":raised_hand_tone5:", - ["\u270b"] = ":raised_hand:", - ["\U0001f64c\U0001f3fb"] = ":raised_hands_tone1:", - ["\U0001f64c\U0001f3fc"] = ":raised_hands_tone2:", - ["\U0001f64c\U0001f3fd"] = ":raised_hands_tone3:", - ["\U0001f64c\U0001f3fe"] = ":raised_hands_tone4:", - ["\U0001f64c\U0001f3ff"] = ":raised_hands_tone5:", - ["\U0001f64c"] = ":raised_hands:", - ["\U0001f40f"] = ":ram:", - ["\U0001f35c"] = ":ramen:", - ["\U0001f400"] = ":rat:", - ["\U0001fa92"] = ":razor:", - ["\U0001f9fe"] = ":receipt:", - ["\u23fa\ufe0f"] = ":record_button:", - ["\u23fa"] = ":record_button:", - ["\u267b\ufe0f"] = ":recycle:", - ["\u267b"] = ":recycle:", - ["\U0001f697"] = ":red_car:", - ["\U0001f534"] = ":red_circle:", - ["\U0001f9e7"] = ":red_envelope:", - ["\U0001f7e5"] = ":red_square:", - ["\U0001f1e6"] = ":regional_indicator_a:", - ["\U0001f1e7"] = ":regional_indicator_b:", - ["\U0001f1e8"] = ":regional_indicator_c:", - ["\U0001f1e9"] = ":regional_indicator_d:", - ["\U0001f1ea"] = ":regional_indicator_e:", - ["\U0001f1eb"] = ":regional_indicator_f:", - ["\U0001f1ec"] = ":regional_indicator_g:", - ["\U0001f1ed"] = ":regional_indicator_h:", - ["\U0001f1ee"] = ":regional_indicator_i:", - ["\U0001f1ef"] = ":regional_indicator_j:", - ["\U0001f1f0"] = ":regional_indicator_k:", - ["\U0001f1f1"] = ":regional_indicator_l:", - ["\U0001f1f2"] = ":regional_indicator_m:", - ["\U0001f1f3"] = ":regional_indicator_n:", - ["\U0001f1f4"] = ":regional_indicator_o:", - ["\U0001f1f5"] = ":regional_indicator_p:", - ["\U0001f1f6"] = ":regional_indicator_q:", - ["\U0001f1f7"] = ":regional_indicator_r:", - ["\U0001f1f8"] = ":regional_indicator_s:", - ["\U0001f1f9"] = ":regional_indicator_t:", - ["\U0001f1fa"] = ":regional_indicator_u:", - ["\U0001f1fb"] = ":regional_indicator_v:", - ["\U0001f1fc"] = ":regional_indicator_w:", - ["\U0001f1fd"] = ":regional_indicator_x:", - ["\U0001f1fe"] = ":regional_indicator_y:", - ["\U0001f1ff"] = ":regional_indicator_z:", - ["\u00ae\ufe0f"] = ":registered:", - ["\u00ae"] = ":registered:", - ["\u263a\ufe0f"] = ":relaxed:", - ["\u263a"] = ":relaxed:", - ["\U0001f60c"] = ":relieved:", - ["\U0001f397\ufe0f"] = ":reminder_ribbon:", - ["\U0001f397"] = ":reminder_ribbon:", - ["\U0001f502"] = ":repeat_one:", - ["\U0001f501"] = ":repeat:", - ["\U0001f6bb"] = ":restroom:", - ["\U0001f49e"] = ":revolving_hearts:", - ["\u23ea"] = ":rewind:", - ["\U0001f98f"] = ":rhino:", - ["\U0001f380"] = ":ribbon:", - ["\U0001f359"] = ":rice_ball:", - ["\U0001f358"] = ":rice_cracker:", - ["\U0001f391"] = ":rice_scene:", - ["\U0001f35a"] = ":rice:", - ["\U0001f91c\U0001f3fb"] = ":right_facing_fist_tone1:", - ["\U0001f91c\U0001f3fc"] = ":right_facing_fist_tone2:", - ["\U0001f91c\U0001f3fd"] = ":right_facing_fist_tone3:", - ["\U0001f91c\U0001f3fe"] = ":right_facing_fist_tone4:", - ["\U0001f91c\U0001f3ff"] = ":right_facing_fist_tone5:", - ["\U0001f91c"] = ":right_facing_fist:", - ["\U0001faf1\U0001f3fb"] = ":rightwards_hand_tone1:", - ["\U0001faf1\U0001f3fc"] = ":rightwards_hand_tone2:", - ["\U0001faf1\U0001f3fd"] = ":rightwards_hand_tone3:", - ["\U0001faf1\U0001f3fe"] = ":rightwards_hand_tone4:", - ["\U0001faf1\U0001f3ff"] = ":rightwards_hand_tone5:", - ["\U0001faf1"] = ":rightwards_hand:", - ["\U0001faf8\U0001f3fb"] = ":rightwards_pushing_hand_tone1:", - ["\U0001faf8\U0001f3fc"] = ":rightwards_pushing_hand_tone2:", - ["\U0001faf8\U0001f3fd"] = ":rightwards_pushing_hand_tone3:", - ["\U0001faf8\U0001f3fe"] = ":rightwards_pushing_hand_tone4:", - ["\U0001faf8\U0001f3ff"] = ":rightwards_pushing_hand_tone5:", - ["\U0001faf8"] = ":rightwards_pushing_hand:", - ["\U0001f6df"] = ":ring_buoy:", - ["\U0001f48d"] = ":ring:", - ["\U0001fa90"] = ":ringed_planet:", - ["\U0001f916"] = ":robot:", - ["\U0001faa8"] = ":rock:", - ["\U0001f680"] = ":rocket:", - ["\U0001f923"] = ":rofl:", - ["\U0001f9fb"] = ":roll_of_paper:", - ["\U0001f3a2"] = ":roller_coaster:", - ["\U0001f6fc"] = ":roller_skate:", - ["\U0001f644"] = ":rolling_eyes:", - ["\U0001f413"] = ":rooster:", - ["\U0001f339"] = ":rose:", - ["\U0001f3f5\ufe0f"] = ":rosette:", - ["\U0001f3f5"] = ":rosette:", - ["\U0001f6a8"] = ":rotating_light:", - ["\U0001f4cd"] = ":round_pushpin:", - ["\U0001f3c9"] = ":rugby_football:", - ["\U0001f3bd"] = ":running_shirt_with_sash:", - ["\U0001f202\ufe0f"] = ":sa:", - ["\U0001f202"] = ":sa:", - ["\U0001f9f7"] = ":safety_pin:", - ["\U0001f9ba"] = ":safety_vest:", - ["\u2650"] = ":sagittarius:", - ["\u26f5"] = ":sailboat:", - ["\U0001f376"] = ":sake:", - ["\U0001f957"] = ":salad:", - ["\U0001f9c2"] = ":salt:", - ["\U0001fae1"] = ":saluting_face:", - ["\U0001f461"] = ":sandal:", - ["\U0001f96a"] = ":sandwich:", - ["\U0001f385\U0001f3fb"] = ":santa_tone1:", - ["\U0001f385\U0001f3fc"] = ":santa_tone2:", - ["\U0001f385\U0001f3fd"] = ":santa_tone3:", - ["\U0001f385\U0001f3fe"] = ":santa_tone4:", - ["\U0001f385\U0001f3ff"] = ":santa_tone5:", - ["\U0001f385"] = ":santa:", - ["\U0001f97b"] = ":sari:", - ["\U0001f6f0\ufe0f"] = ":satellite_orbital:", - ["\U0001f6f0"] = ":satellite_orbital:", - ["\U0001f4e1"] = ":satellite:", - ["\U0001f995"] = ":sauropod:", - ["\U0001f3b7"] = ":saxophone:", - ["\u2696\ufe0f"] = ":scales:", - ["\u2696"] = ":scales:", - ["\U0001f9e3"] = ":scarf:", - ["\U0001f392"] = ":school_satchel:", - ["\U0001f3eb"] = ":school:", - ["\U0001f9d1\U0001f3fb\u200d\U0001f52c"] = ":scientist_tone1:", - ["\U0001f9d1\U0001f3fc\u200d\U0001f52c"] = ":scientist_tone2:", - ["\U0001f9d1\U0001f3fd\u200d\U0001f52c"] = ":scientist_tone3:", - ["\U0001f9d1\U0001f3fe\u200d\U0001f52c"] = ":scientist_tone4:", - ["\U0001f9d1\U0001f3ff\u200d\U0001f52c"] = ":scientist_tone5:", - ["\U0001f9d1\u200d\U0001f52c"] = ":scientist:", - ["\u2702\ufe0f"] = ":scissors:", - ["\u2702"] = ":scissors:", - ["\U0001f6f4"] = ":scooter:", - ["\U0001f982"] = ":scorpion:", - ["\u264f"] = ":scorpius:", - ["\U0001f3f4\U000e0067\U000e0062\U000e0073\U000e0063\U000e0074\U000e007f"] = ":scotland:", - ["\U0001f640"] = ":scream_cat:", - ["\U0001f631"] = ":scream:", - ["\U0001fa9b"] = ":screwdriver:", - ["\U0001f4dc"] = ":scroll:", - ["\U0001f9ad"] = ":seal:", - ["\U0001f4ba"] = ":seat:", - ["\U0001f948"] = ":second_place:", - ["\u3299\ufe0f"] = ":secret:", - ["\u3299"] = ":secret:", - ["\U0001f648"] = ":see_no_evil:", - ["\U0001f331"] = ":seedling:", - ["\U0001f933\U0001f3fb"] = ":selfie_tone1:", - ["\U0001f933\U0001f3fc"] = ":selfie_tone2:", - ["\U0001f933\U0001f3fd"] = ":selfie_tone3:", - ["\U0001f933\U0001f3fe"] = ":selfie_tone4:", - ["\U0001f933\U0001f3ff"] = ":selfie_tone5:", - ["\U0001f933"] = ":selfie:", - ["\U0001f415\u200d\U0001f9ba"] = ":service_dog:", - ["\u0037\ufe0f\u20e3"] = ":seven:", - ["\u0037\u20e3"] = ":seven:", - ["\U0001faa1"] = ":sewing_needle:", - ["\U0001fae8"] = ":shaking_face:", - ["\U0001f958"] = ":shallow_pan_of_food:", - ["\u2618\ufe0f"] = ":shamrock:", - ["\u2618"] = ":shamrock:", - ["\U0001f988"] = ":shark:", - ["\U0001f367"] = ":shaved_ice:", - ["\U0001f411"] = ":sheep:", - ["\U0001f41a"] = ":shell:", - ["\U0001f6e1\ufe0f"] = ":shield:", - ["\U0001f6e1"] = ":shield:", - ["\u26e9\ufe0f"] = ":shinto_shrine:", - ["\u26e9"] = ":shinto_shrine:", - ["\U0001f6a2"] = ":ship:", - ["\U0001f455"] = ":shirt:", - ["\U0001f6cd\ufe0f"] = ":shopping_bags:", - ["\U0001f6cd"] = ":shopping_bags:", - ["\U0001f6d2"] = ":shopping_cart:", - ["\U0001fa73"] = ":shorts:", - ["\U0001f6bf"] = ":shower:", - ["\U0001f990"] = ":shrimp:", - ["\U0001f92b"] = ":shushing_face:", - ["\U0001f4f6"] = ":signal_strength:", - ["\U0001f9d1\U0001f3fb\u200d\U0001f3a4"] = ":singer_tone1:", - ["\U0001f9d1\U0001f3fc\u200d\U0001f3a4"] = ":singer_tone2:", - ["\U0001f9d1\U0001f3fd\u200d\U0001f3a4"] = ":singer_tone3:", - ["\U0001f9d1\U0001f3fe\u200d\U0001f3a4"] = ":singer_tone4:", - ["\U0001f9d1\U0001f3ff\u200d\U0001f3a4"] = ":singer_tone5:", - ["\U0001f9d1\u200d\U0001f3a4"] = ":singer:", - ["\U0001f52f"] = ":six_pointed_star:", - ["\u0036\ufe0f\u20e3"] = ":six:", - ["\u0036\u20e3"] = ":six:", - ["\U0001f6f9"] = ":skateboard:", - ["\U0001f3bf"] = ":ski:", - ["\u26f7\ufe0f"] = ":skier:", - ["\u26f7"] = ":skier:", - ["\u2620\ufe0f"] = ":skull_crossbones:", - ["\u2620"] = ":skull_crossbones:", - ["\U0001f480"] = ":skull:", - ["\U0001f9a8"] = ":skunk:", - ["\U0001f6f7"] = ":sled:", - ["\U0001f6cc"] = ":sleeping_accommodation:", - ["\U0001f634"] = ":sleeping:", - ["\U0001f62a"] = ":sleepy:", - ["\U0001f641"] = ":slight_frown:", - ["\U0001f642"] = ":slight_smile:", - ["\U0001f3b0"] = ":slot_machine:", - ["\U0001f9a5"] = ":sloth:", - ["\U0001f539"] = ":small_blue_diamond:", - ["\U0001f538"] = ":small_orange_diamond:", - ["\U0001f53b"] = ":small_red_triangle_down:", - ["\U0001f53a"] = ":small_red_triangle:", - ["\U0001f638"] = ":smile_cat:", - ["\U0001f604"] = ":smile:", - ["\U0001f63a"] = ":smiley_cat:", - ["\U0001f603"] = ":smiley:", - ["\U0001f970"] = ":smiling_face_with_3_hearts:", - ["\U0001f972"] = ":smiling_face_with_tear:", - ["\U0001f608"] = ":smiling_imp:", - ["\U0001f63c"] = ":smirk_cat:", - ["\U0001f60f"] = ":smirk:", - ["\U0001f6ac"] = ":smoking:", - ["\U0001f40c"] = ":snail:", - ["\U0001f40d"] = ":snake:", - ["\U0001f927"] = ":sneezing_face:", - ["\U0001f3c2\U0001f3fb"] = ":snowboarder_tone1:", - ["\U0001f3c2\U0001f3fc"] = ":snowboarder_tone2:", - ["\U0001f3c2\U0001f3fd"] = ":snowboarder_tone3:", - ["\U0001f3c2\U0001f3fe"] = ":snowboarder_tone4:", - ["\U0001f3c2\U0001f3ff"] = ":snowboarder_tone5:", - ["\U0001f3c2"] = ":snowboarder:", - ["\u2744\ufe0f"] = ":snowflake:", - ["\u2744"] = ":snowflake:", - ["\u26c4"] = ":snowman:", - ["\u2603\ufe0f"] = ":snowman2:", - ["\u2603"] = ":snowman2:", - ["\U0001f9fc"] = ":soap:", - ["\U0001f62d"] = ":sob:", - ["\u26bd"] = ":soccer:", - ["\U0001f9e6"] = ":socks:", - ["\U0001f94e"] = ":softball:", - ["\U0001f51c"] = ":soon:", - ["\U0001f198"] = ":sos:", - ["\U0001f509"] = ":sound:", - ["\U0001f47e"] = ":space_invader:", - ["\u2660\ufe0f"] = ":spades:", - ["\u2660"] = ":spades:", - ["\U0001f35d"] = ":spaghetti:", - ["\u2747\ufe0f"] = ":sparkle:", - ["\u2747"] = ":sparkle:", - ["\U0001f387"] = ":sparkler:", - ["\u2728"] = ":sparkles:", - ["\U0001f496"] = ":sparkling_heart:", - ["\U0001f64a"] = ":speak_no_evil:", - ["\U0001f508"] = ":speaker:", - ["\U0001f5e3\ufe0f"] = ":speaking_head:", - ["\U0001f5e3"] = ":speaking_head:", - ["\U0001f4ac"] = ":speech_balloon:", - ["\U0001f5e8\ufe0f"] = ":speech_left:", - ["\U0001f5e8"] = ":speech_left:", - ["\U0001f6a4"] = ":speedboat:", - ["\U0001f578\ufe0f"] = ":spider_web:", - ["\U0001f578"] = ":spider_web:", - ["\U0001f577\ufe0f"] = ":spider:", - ["\U0001f577"] = ":spider:", - ["\U0001f9fd"] = ":sponge:", - ["\U0001f944"] = ":spoon:", - ["\U0001f9f4"] = ":squeeze_bottle:", - ["\U0001f991"] = ":squid:", - ["\U0001f3df\ufe0f"] = ":stadium:", - ["\U0001f3df"] = ":stadium:", - ["\u262a\ufe0f"] = ":star_and_crescent:", - ["\u262a"] = ":star_and_crescent:", - ["\u2721\ufe0f"] = ":star_of_david:", - ["\u2721"] = ":star_of_david:", - ["\U0001f929"] = ":star_struck:", - ["\u2b50"] = ":star:", - ["\U0001f31f"] = ":star2:", - ["\U0001f320"] = ":stars:", - ["\U0001f689"] = ":station:", - ["\U0001f5fd"] = ":statue_of_liberty:", - ["\U0001f682"] = ":steam_locomotive:", - ["\U0001fa7a"] = ":stethoscope:", - ["\U0001f372"] = ":stew:", - ["\u23f9\ufe0f"] = ":stop_button:", - ["\u23f9"] = ":stop_button:", - ["\u23f1\ufe0f"] = ":stopwatch:", - ["\u23f1"] = ":stopwatch:", - ["\U0001f4cf"] = ":straight_ruler:", - ["\U0001f353"] = ":strawberry:", - ["\U0001f61d"] = ":stuck_out_tongue_closed_eyes:", - ["\U0001f61c"] = ":stuck_out_tongue_winking_eye:", - ["\U0001f61b"] = ":stuck_out_tongue:", - ["\U0001f9d1\U0001f3fb\u200d\U0001f393"] = ":student_tone1:", - ["\U0001f9d1\U0001f3fc\u200d\U0001f393"] = ":student_tone2:", - ["\U0001f9d1\U0001f3fd\u200d\U0001f393"] = ":student_tone3:", - ["\U0001f9d1\U0001f3fe\u200d\U0001f393"] = ":student_tone4:", - ["\U0001f9d1\U0001f3ff\u200d\U0001f393"] = ":student_tone5:", - ["\U0001f9d1\u200d\U0001f393"] = ":student:", - ["\U0001f959"] = ":stuffed_flatbread:", - ["\U0001f31e"] = ":sun_with_face:", - ["\U0001f33b"] = ":sunflower:", - ["\U0001f60e"] = ":sunglasses:", - ["\u2600\ufe0f"] = ":sunny:", - ["\u2600"] = ":sunny:", - ["\U0001f304"] = ":sunrise_over_mountains:", - ["\U0001f305"] = ":sunrise:", - ["\U0001f9b8\U0001f3fb"] = ":superhero_tone1:", - ["\U0001f9b8\U0001f3fc"] = ":superhero_tone2:", - ["\U0001f9b8\U0001f3fd"] = ":superhero_tone3:", - ["\U0001f9b8\U0001f3fe"] = ":superhero_tone4:", - ["\U0001f9b8\U0001f3ff"] = ":superhero_tone5:", - ["\U0001f9b8"] = ":superhero:", - ["\U0001f9b9\U0001f3fb"] = ":supervillain_tone1:", - ["\U0001f9b9\U0001f3fc"] = ":supervillain_tone2:", - ["\U0001f9b9\U0001f3fd"] = ":supervillain_tone3:", - ["\U0001f9b9\U0001f3fe"] = ":supervillain_tone4:", - ["\U0001f9b9\U0001f3ff"] = ":supervillain_tone5:", - ["\U0001f9b9"] = ":supervillain:", - ["\U0001f363"] = ":sushi:", - ["\U0001f69f"] = ":suspension_railway:", - ["\U0001f9a2"] = ":swan:", - ["\U0001f4a6"] = ":sweat_drops:", - ["\U0001f605"] = ":sweat_smile:", - ["\U0001f613"] = ":sweat:", - ["\U0001f360"] = ":sweet_potato:", - ["\U0001f523"] = ":symbols:", - ["\U0001f54d"] = ":synagogue:", - ["\U0001f489"] = ":syringe:", - ["\U0001f996"] = ":t_rex:", - ["\U0001f32e"] = ":taco:", - ["\U0001f389"] = ":tada:", - ["\U0001f961"] = ":takeout_box:", - ["\U0001fad4"] = ":tamale:", - ["\U0001f38b"] = ":tanabata_tree:", - ["\U0001f34a"] = ":tangerine:", - ["\u2649"] = ":taurus:", - ["\U0001f695"] = ":taxi:", - ["\U0001f375"] = ":tea:", - ["\U0001f9d1\U0001f3fb\u200d\U0001f3eb"] = ":teacher_tone1:", - ["\U0001f9d1\U0001f3fc\u200d\U0001f3eb"] = ":teacher_tone2:", - ["\U0001f9d1\U0001f3fd\u200d\U0001f3eb"] = ":teacher_tone3:", - ["\U0001f9d1\U0001f3fe\u200d\U0001f3eb"] = ":teacher_tone4:", - ["\U0001f9d1\U0001f3ff\u200d\U0001f3eb"] = ":teacher_tone5:", - ["\U0001f9d1\u200d\U0001f3eb"] = ":teacher:", - ["\U0001fad6"] = ":teapot:", - ["\U0001f9d1\U0001f3fb\u200d\U0001f4bb"] = ":technologist_tone1:", - ["\U0001f9d1\U0001f3fc\u200d\U0001f4bb"] = ":technologist_tone2:", - ["\U0001f9d1\U0001f3fd\u200d\U0001f4bb"] = ":technologist_tone3:", - ["\U0001f9d1\U0001f3fe\u200d\U0001f4bb"] = ":technologist_tone4:", - ["\U0001f9d1\U0001f3ff\u200d\U0001f4bb"] = ":technologist_tone5:", - ["\U0001f9d1\u200d\U0001f4bb"] = ":technologist:", - ["\U0001f9f8"] = ":teddy_bear:", - ["\U0001f4de"] = ":telephone_receiver:", - ["\u260e\ufe0f"] = ":telephone:", - ["\u260e"] = ":telephone:", - ["\U0001f52d"] = ":telescope:", - ["\U0001f3be"] = ":tennis:", - ["\u26fa"] = ":tent:", - ["\U0001f9ea"] = ":test_tube:", - ["\U0001f912"] = ":thermometer_face:", - ["\U0001f321\ufe0f"] = ":thermometer:", - ["\U0001f321"] = ":thermometer:", - ["\U0001f914"] = ":thinking:", - ["\U0001f949"] = ":third_place:", - ["\U0001fa74"] = ":thong_sandal:", - ["\U0001f4ad"] = ":thought_balloon:", - ["\U0001f9f5"] = ":thread:", - ["\u0033\ufe0f\u20e3"] = ":three:", - ["\u0033\u20e3"] = ":three:", - ["\U0001f44e\U0001f3fb"] = ":thumbsdown_tone1:", - ["\U0001f44e\U0001f3fc"] = ":thumbsdown_tone2:", - ["\U0001f44e\U0001f3fd"] = ":thumbsdown_tone3:", - ["\U0001f44e\U0001f3fe"] = ":thumbsdown_tone4:", - ["\U0001f44e\U0001f3ff"] = ":thumbsdown_tone5:", - ["\U0001f44e"] = ":thumbsdown:", - ["\U0001f44d\U0001f3fb"] = ":thumbsup_tone1:", - ["\U0001f44d\U0001f3fc"] = ":thumbsup_tone2:", - ["\U0001f44d\U0001f3fd"] = ":thumbsup_tone3:", - ["\U0001f44d\U0001f3fe"] = ":thumbsup_tone4:", - ["\U0001f44d\U0001f3ff"] = ":thumbsup_tone5:", - ["\U0001f44d"] = ":thumbsup:", - ["\u26c8\ufe0f"] = ":thunder_cloud_rain:", - ["\u26c8"] = ":thunder_cloud_rain:", - ["\U0001f3ab"] = ":ticket:", - ["\U0001f39f\ufe0f"] = ":tickets:", - ["\U0001f39f"] = ":tickets:", - ["\U0001f42f"] = ":tiger:", - ["\U0001f405"] = ":tiger2:", - ["\u23f2\ufe0f"] = ":timer:", - ["\u23f2"] = ":timer:", - ["\U0001f62b"] = ":tired_face:", - ["\u2122\ufe0f"] = ":tm:", - ["\u2122"] = ":tm:", - ["\U0001f6bd"] = ":toilet:", - ["\U0001f5fc"] = ":tokyo_tower:", - ["\U0001f345"] = ":tomato:", - ["\U0001f445"] = ":tongue:", - ["\U0001f9f0"] = ":toolbox:", - ["\U0001f6e0\ufe0f"] = ":tools:", - ["\U0001f6e0"] = ":tools:", - ["\U0001f9b7"] = ":tooth:", - ["\U0001faa5"] = ":toothbrush:", - ["\U0001f51d"] = ":top:", - ["\U0001f3a9"] = ":tophat:", - ["\u23ed\ufe0f"] = ":track_next:", - ["\u23ed"] = ":track_next:", - ["\u23ee\ufe0f"] = ":track_previous:", - ["\u23ee"] = ":track_previous:", - ["\U0001f5b2\ufe0f"] = ":trackball:", - ["\U0001f5b2"] = ":trackball:", - ["\U0001f69c"] = ":tractor:", - ["\U0001f6a5"] = ":traffic_light:", - ["\U0001f68b"] = ":train:", - ["\U0001f686"] = ":train2:", - ["\U0001f68a"] = ":tram:", - ["\U0001f3f3\ufe0f\u200d\u26a7\ufe0f"] = ":transgender_flag:", - ["\u26a7"] = ":transgender_symbol:", - ["\U0001f6a9"] = ":triangular_flag_on_post:", - ["\U0001f4d0"] = ":triangular_ruler:", - ["\U0001f531"] = ":trident:", - ["\U0001f624"] = ":triumph:", - ["\U0001f9cc"] = ":troll:", - ["\U0001f68e"] = ":trolleybus:", - ["\U0001f3c6"] = ":trophy:", - ["\U0001f379"] = ":tropical_drink:", - ["\U0001f420"] = ":tropical_fish:", - ["\U0001f69a"] = ":truck:", - ["\U0001f3ba"] = ":trumpet:", - ["\U0001f337"] = ":tulip:", - ["\U0001f943"] = ":tumbler_glass:", - ["\U0001f983"] = ":turkey:", - ["\U0001f422"] = ":turtle:", - ["\U0001f4fa"] = ":tv:", - ["\U0001f500"] = ":twisted_rightwards_arrows:", - ["\U0001f495"] = ":two_hearts:", - ["\U0001f46c"] = ":two_men_holding_hands:", - ["\u0032\ufe0f\u20e3"] = ":two:", - ["\u0032\u20e3"] = ":two:", - ["\U0001f239"] = ":u5272:", - ["\U0001f234"] = ":u5408:", - ["\U0001f23a"] = ":u55b6:", - ["\U0001f22f"] = ":u6307:", - ["\U0001f237\ufe0f"] = ":u6708:", - ["\U0001f237"] = ":u6708:", - ["\U0001f236"] = ":u6709:", - ["\U0001f235"] = ":u6e80:", - ["\U0001f21a"] = ":u7121:", - ["\U0001f238"] = ":u7533:", - ["\U0001f232"] = ":u7981:", - ["\U0001f233"] = ":u7a7a:", - ["\u2614"] = ":umbrella:", - ["\u2602\ufe0f"] = ":umbrella2:", - ["\u2602"] = ":umbrella2:", - ["\U0001f612"] = ":unamused:", - ["\U0001f51e"] = ":underage:", - ["\U0001f984"] = ":unicorn:", - ["\U0001f1fa\U0001f1f3"] = ":united_nations:", - ["\U0001f513"] = ":unlock:", - ["\U0001f199"] = ":up:", - ["\U0001f643"] = ":upside_down:", - ["\u26b1\ufe0f"] = ":urn:", - ["\u26b1"] = ":urn:", - ["\u270c\U0001f3fb"] = ":v_tone1:", - ["\u270c\U0001f3fc"] = ":v_tone2:", - ["\u270c\U0001f3fd"] = ":v_tone3:", - ["\u270c\U0001f3fe"] = ":v_tone4:", - ["\u270c\U0001f3ff"] = ":v_tone5:", - ["\u270c\ufe0f"] = ":v:", - ["\u270c"] = ":v:", - ["\U0001f9db\U0001f3fb"] = ":vampire_tone1:", - ["\U0001f9db\U0001f3fc"] = ":vampire_tone2:", - ["\U0001f9db\U0001f3fd"] = ":vampire_tone3:", - ["\U0001f9db\U0001f3fe"] = ":vampire_tone4:", - ["\U0001f9db\U0001f3ff"] = ":vampire_tone5:", - ["\U0001f9db"] = ":vampire:", - ["\U0001f6a6"] = ":vertical_traffic_light:", - ["\U0001f4fc"] = ":vhs:", - ["\U0001f4f3"] = ":vibration_mode:", - ["\U0001f4f9"] = ":video_camera:", - ["\U0001f3ae"] = ":video_game:", - ["\U0001f3bb"] = ":violin:", - ["\u264d"] = ":virgo:", - ["\U0001f30b"] = ":volcano:", - ["\U0001f3d0"] = ":volleyball:", - ["\U0001f19a"] = ":vs:", - ["\U0001f596\U0001f3fb"] = ":vulcan_tone1:", - ["\U0001f596\U0001f3fc"] = ":vulcan_tone2:", - ["\U0001f596\U0001f3fd"] = ":vulcan_tone3:", - ["\U0001f596\U0001f3fe"] = ":vulcan_tone4:", - ["\U0001f596\U0001f3ff"] = ":vulcan_tone5:", - ["\U0001f596"] = ":vulcan:", - ["\U0001f9c7"] = ":waffle:", - ["\U0001f3f4\U000e0067\U000e0062\U000e0077\U000e006c\U000e0073\U000e007f"] = ":wales:", - ["\U0001f318"] = ":waning_crescent_moon:", - ["\U0001f316"] = ":waning_gibbous_moon:", - ["\u26a0\ufe0f"] = ":warning:", - ["\u26a0"] = ":warning:", - ["\U0001f5d1\ufe0f"] = ":wastebasket:", - ["\U0001f5d1"] = ":wastebasket:", - ["\u231a"] = ":watch:", - ["\U0001f403"] = ":water_buffalo:", - ["\U0001f349"] = ":watermelon:", - ["\U0001f44b\U0001f3fb"] = ":wave_tone1:", - ["\U0001f44b\U0001f3fc"] = ":wave_tone2:", - ["\U0001f44b\U0001f3fd"] = ":wave_tone3:", - ["\U0001f44b\U0001f3fe"] = ":wave_tone4:", - ["\U0001f44b\U0001f3ff"] = ":wave_tone5:", - ["\U0001f44b"] = ":wave:", - ["\u3030\ufe0f"] = ":wavy_dash:", - ["\u3030"] = ":wavy_dash:", - ["\U0001f312"] = ":waxing_crescent_moon:", - ["\U0001f314"] = ":waxing_gibbous_moon:", - ["\U0001f6be"] = ":wc:", - ["\U0001f629"] = ":weary:", - ["\U0001f492"] = ":wedding:", - ["\U0001f433"] = ":whale:", - ["\U0001f40b"] = ":whale2:", - ["\u2638\ufe0f"] = ":wheel_of_dharma:", - ["\u2638"] = ":wheel_of_dharma:", - ["\U0001f6de"] = ":wheel:", - ["\u267f"] = ":wheelchair:", - ["\u2705"] = ":white_check_mark:", - ["\u26aa"] = ":white_circle:", - ["\U0001f4ae"] = ":white_flower:", - ["\U0001f90d"] = ":white_heart:", - ["\u2b1c"] = ":white_large_square:", - ["\u25fd"] = ":white_medium_small_square:", - ["\u25fb\ufe0f"] = ":white_medium_square:", - ["\u25fb"] = ":white_medium_square:", - ["\u25ab\ufe0f"] = ":white_small_square:", - ["\u25ab"] = ":white_small_square:", - ["\U0001f533"] = ":white_square_button:", - ["\U0001f325\ufe0f"] = ":white_sun_cloud:", - ["\U0001f325"] = ":white_sun_cloud:", - ["\U0001f326\ufe0f"] = ":white_sun_rain_cloud:", - ["\U0001f326"] = ":white_sun_rain_cloud:", - ["\U0001f324\ufe0f"] = ":white_sun_small_cloud:", - ["\U0001f324"] = ":white_sun_small_cloud:", - ["\U0001f940"] = ":wilted_rose:", - ["\U0001f32c\ufe0f"] = ":wind_blowing_face:", - ["\U0001f32c"] = ":wind_blowing_face:", - ["\U0001f390"] = ":wind_chime:", - ["\U0001fa9f"] = ":window:", - ["\U0001f377"] = ":wine_glass:", - ["\U0001fabd"] = ":wing:", - ["\U0001f609"] = ":wink:", - ["\U0001f6dc"] = ":wireless:", - ["\U0001f43a"] = ":wolf:", - ["\U0001f46b"] = ":woman_and_man_holding_hands_tone5_tone4:", - ["\U0001f469\U0001f3fb\u200d\U0001f3a8"] = ":woman_artist_tone1:", - ["\U0001f469\U0001f3fc\u200d\U0001f3a8"] = ":woman_artist_tone2:", - ["\U0001f469\U0001f3fd\u200d\U0001f3a8"] = ":woman_artist_tone3:", - ["\U0001f469\U0001f3fe\u200d\U0001f3a8"] = ":woman_artist_tone4:", - ["\U0001f469\U0001f3ff\u200d\U0001f3a8"] = ":woman_artist_tone5:", - ["\U0001f469\u200d\U0001f3a8"] = ":woman_artist:", - ["\U0001f469\U0001f3fb\u200d\U0001f680"] = ":woman_astronaut_tone1:", - ["\U0001f469\U0001f3fc\u200d\U0001f680"] = ":woman_astronaut_tone2:", - ["\U0001f469\U0001f3fd\u200d\U0001f680"] = ":woman_astronaut_tone3:", - ["\U0001f469\U0001f3fe\u200d\U0001f680"] = ":woman_astronaut_tone4:", - ["\U0001f469\U0001f3ff\u200d\U0001f680"] = ":woman_astronaut_tone5:", - ["\U0001f469\u200d\U0001f680"] = ":woman_astronaut:", - ["\U0001f469\U0001f3fb\u200d\U0001f9b2"] = ":woman_bald_tone1:", - ["\U0001f469\U0001f3fc\u200d\U0001f9b2"] = ":woman_bald_tone2:", - ["\U0001f469\U0001f3fd\u200d\U0001f9b2"] = ":woman_bald_tone3:", - ["\U0001f469\U0001f3fe\u200d\U0001f9b2"] = ":woman_bald_tone4:", - ["\U0001f469\U0001f3ff\u200d\U0001f9b2"] = ":woman_bald_tone5:", - ["\U0001f469\u200d\U0001f9b2"] = ":woman_bald:", - ["\U0001f9d4\u200d\u2640\ufe0f"] = ":woman_beard:", - ["\U0001f6b4\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_biking_tone1:", - ["\U0001f6b4\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_biking_tone2:", - ["\U0001f6b4\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_biking_tone3:", - ["\U0001f6b4\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_biking_tone4:", - ["\U0001f6b4\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_biking_tone5:", - ["\U0001f6b4\u200d\u2640\ufe0f"] = ":woman_biking:", - ["\u26f9\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_bouncing_ball_tone1:", - ["\u26f9\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_bouncing_ball_tone2:", - ["\u26f9\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_bouncing_ball_tone3:", - ["\u26f9\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_bouncing_ball_tone4:", - ["\u26f9\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_bouncing_ball_tone5:", - ["\u26f9\ufe0f\u200d\u2640\ufe0f"] = ":woman_bouncing_ball:", - ["\U0001f647\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_bowing_tone1:", - ["\U0001f647\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_bowing_tone2:", - ["\U0001f647\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_bowing_tone3:", - ["\U0001f647\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_bowing_tone4:", - ["\U0001f647\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_bowing_tone5:", - ["\U0001f647\u200d\u2640\ufe0f"] = ":woman_bowing:", - ["\U0001f938\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_cartwheeling_tone1:", - ["\U0001f938\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_cartwheeling_tone2:", - ["\U0001f938\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_cartwheeling_tone3:", - ["\U0001f938\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_cartwheeling_tone4:", - ["\U0001f938\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_cartwheeling_tone5:", - ["\U0001f938\u200d\u2640\ufe0f"] = ":woman_cartwheeling:", - ["\U0001f9d7\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_climbing_tone1:", - ["\U0001f9d7\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_climbing_tone2:", - ["\U0001f9d7\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_climbing_tone3:", - ["\U0001f9d7\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_climbing_tone4:", - ["\U0001f9d7\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_climbing_tone5:", - ["\U0001f9d7\u200d\u2640\ufe0f"] = ":woman_climbing:", - ["\U0001f477\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_construction_worker_tone1:", - ["\U0001f477\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_construction_worker_tone2:", - ["\U0001f477\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_construction_worker_tone3:", - ["\U0001f477\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_construction_worker_tone4:", - ["\U0001f477\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_construction_worker_tone5:", - ["\U0001f477\u200d\u2640\ufe0f"] = ":woman_construction_worker:", - ["\U0001f469\U0001f3fb\u200d\U0001f373"] = ":woman_cook_tone1:", - ["\U0001f469\U0001f3fc\u200d\U0001f373"] = ":woman_cook_tone2:", - ["\U0001f469\U0001f3fd\u200d\U0001f373"] = ":woman_cook_tone3:", - ["\U0001f469\U0001f3fe\u200d\U0001f373"] = ":woman_cook_tone4:", - ["\U0001f469\U0001f3ff\u200d\U0001f373"] = ":woman_cook_tone5:", - ["\U0001f469\u200d\U0001f373"] = ":woman_cook:", - ["\U0001f469\U0001f3fb\u200d\U0001f9b1"] = ":woman_curly_haired_tone1:", - ["\U0001f469\U0001f3fc\u200d\U0001f9b1"] = ":woman_curly_haired_tone2:", - ["\U0001f469\U0001f3fd\u200d\U0001f9b1"] = ":woman_curly_haired_tone3:", - ["\U0001f469\U0001f3fe\u200d\U0001f9b1"] = ":woman_curly_haired_tone4:", - ["\U0001f469\U0001f3ff\u200d\U0001f9b1"] = ":woman_curly_haired_tone5:", - ["\U0001f469\u200d\U0001f9b1"] = ":woman_curly_haired:", - ["\U0001f575\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_detective_tone1:", - ["\U0001f575\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_detective_tone2:", - ["\U0001f575\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_detective_tone3:", - ["\U0001f575\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_detective_tone4:", - ["\U0001f575\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_detective_tone5:", - ["\U0001f575\ufe0f\u200d\u2640\ufe0f"] = ":woman_detective:", - ["\U0001f9dd\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_elf_tone1:", - ["\U0001f9dd\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_elf_tone2:", - ["\U0001f9dd\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_elf_tone3:", - ["\U0001f9dd\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_elf_tone4:", - ["\U0001f9dd\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_elf_tone5:", - ["\U0001f9dd\u200d\u2640\ufe0f"] = ":woman_elf:", - ["\U0001f926\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_facepalming_tone1:", - ["\U0001f926\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_facepalming_tone2:", - ["\U0001f926\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_facepalming_tone3:", - ["\U0001f926\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_facepalming_tone4:", - ["\U0001f926\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_facepalming_tone5:", - ["\U0001f926\u200d\u2640\ufe0f"] = ":woman_facepalming:", - ["\U0001f469\U0001f3fb\u200d\U0001f3ed"] = ":woman_factory_worker_tone1:", - ["\U0001f469\U0001f3fc\u200d\U0001f3ed"] = ":woman_factory_worker_tone2:", - ["\U0001f469\U0001f3fd\u200d\U0001f3ed"] = ":woman_factory_worker_tone3:", - ["\U0001f469\U0001f3fe\u200d\U0001f3ed"] = ":woman_factory_worker_tone4:", - ["\U0001f469\U0001f3ff\u200d\U0001f3ed"] = ":woman_factory_worker_tone5:", - ["\U0001f469\u200d\U0001f3ed"] = ":woman_factory_worker:", - ["\U0001f9da\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_fairy_tone1:", - ["\U0001f9da\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_fairy_tone2:", - ["\U0001f9da\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_fairy_tone3:", - ["\U0001f9da\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_fairy_tone4:", - ["\U0001f9da\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_fairy_tone5:", - ["\U0001f9da\u200d\u2640\ufe0f"] = ":woman_fairy:", - ["\U0001f469\U0001f3fb\u200d\U0001f33e"] = ":woman_farmer_tone1:", - ["\U0001f469\U0001f3fc\u200d\U0001f33e"] = ":woman_farmer_tone2:", - ["\U0001f469\U0001f3fd\u200d\U0001f33e"] = ":woman_farmer_tone3:", - ["\U0001f469\U0001f3fe\u200d\U0001f33e"] = ":woman_farmer_tone4:", - ["\U0001f469\U0001f3ff\u200d\U0001f33e"] = ":woman_farmer_tone5:", - ["\U0001f469\u200d\U0001f33e"] = ":woman_farmer:", - ["\U0001f469\U0001f3fb\u200d\U0001f37c"] = ":woman_feeding_baby_tone1:", - ["\U0001f469\U0001f3fc\u200d\U0001f37c"] = ":woman_feeding_baby_tone2:", - ["\U0001f469\U0001f3fd\u200d\U0001f37c"] = ":woman_feeding_baby_tone3:", - ["\U0001f469\U0001f3fe\u200d\U0001f37c"] = ":woman_feeding_baby_tone4:", - ["\U0001f469\U0001f3ff\u200d\U0001f37c"] = ":woman_feeding_baby_tone5:", - ["\U0001f469\u200d\U0001f37c"] = ":woman_feeding_baby:", - ["\U0001f469\U0001f3fb\u200d\U0001f692"] = ":woman_firefighter_tone1:", - ["\U0001f469\U0001f3fc\u200d\U0001f692"] = ":woman_firefighter_tone2:", - ["\U0001f469\U0001f3fd\u200d\U0001f692"] = ":woman_firefighter_tone3:", - ["\U0001f469\U0001f3fe\u200d\U0001f692"] = ":woman_firefighter_tone4:", - ["\U0001f469\U0001f3ff\u200d\U0001f692"] = ":woman_firefighter_tone5:", - ["\U0001f469\u200d\U0001f692"] = ":woman_firefighter:", - ["\U0001f64d\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_frowning_tone1:", - ["\U0001f64d\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_frowning_tone2:", - ["\U0001f64d\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_frowning_tone3:", - ["\U0001f64d\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_frowning_tone4:", - ["\U0001f64d\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_frowning_tone5:", - ["\U0001f64d\u200d\u2640\ufe0f"] = ":woman_frowning:", - ["\U0001f9de\u200d\u2640\ufe0f"] = ":woman_genie:", - ["\U0001f645\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_gesturing_no_tone1:", - ["\U0001f645\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_gesturing_no_tone2:", - ["\U0001f645\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_gesturing_no_tone3:", - ["\U0001f645\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_gesturing_no_tone4:", - ["\U0001f645\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_gesturing_no_tone5:", - ["\U0001f645\u200d\u2640\ufe0f"] = ":woman_gesturing_no:", - ["\U0001f646\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_gesturing_ok_tone1:", - ["\U0001f646\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_gesturing_ok_tone2:", - ["\U0001f646\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_gesturing_ok_tone3:", - ["\U0001f646\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_gesturing_ok_tone4:", - ["\U0001f646\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_gesturing_ok_tone5:", - ["\U0001f646\u200d\u2640\ufe0f"] = ":woman_gesturing_ok:", - ["\U0001f486\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_getting_face_massage_tone1:", - ["\U0001f486\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_getting_face_massage_tone2:", - ["\U0001f486\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_getting_face_massage_tone3:", - ["\U0001f486\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_getting_face_massage_tone4:", - ["\U0001f486\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_getting_face_massage_tone5:", - ["\U0001f486\u200d\u2640\ufe0f"] = ":woman_getting_face_massage:", - ["\U0001f487\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_getting_haircut_tone1:", - ["\U0001f487\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_getting_haircut_tone2:", - ["\U0001f487\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_getting_haircut_tone3:", - ["\U0001f487\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_getting_haircut_tone4:", - ["\U0001f487\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_getting_haircut_tone5:", - ["\U0001f487\u200d\u2640\ufe0f"] = ":woman_getting_haircut:", - ["\U0001f3cc\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_golfing_tone1:", - ["\U0001f3cc\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_golfing_tone2:", - ["\U0001f3cc\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_golfing_tone3:", - ["\U0001f3cc\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_golfing_tone4:", - ["\U0001f3cc\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_golfing_tone5:", - ["\U0001f3cc\ufe0f\u200d\u2640\ufe0f"] = ":woman_golfing:", - ["\U0001f482\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_guard_tone1:", - ["\U0001f482\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_guard_tone2:", - ["\U0001f482\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_guard_tone3:", - ["\U0001f482\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_guard_tone4:", - ["\U0001f482\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_guard_tone5:", - ["\U0001f482\u200d\u2640\ufe0f"] = ":woman_guard:", - ["\U0001f469\U0001f3fb\u200d\u2695\ufe0f"] = ":woman_health_worker_tone1:", - ["\U0001f469\U0001f3fc\u200d\u2695\ufe0f"] = ":woman_health_worker_tone2:", - ["\U0001f469\U0001f3fd\u200d\u2695\ufe0f"] = ":woman_health_worker_tone3:", - ["\U0001f469\U0001f3fe\u200d\u2695\ufe0f"] = ":woman_health_worker_tone4:", - ["\U0001f469\U0001f3ff\u200d\u2695\ufe0f"] = ":woman_health_worker_tone5:", - ["\U0001f469\u200d\u2695\ufe0f"] = ":woman_health_worker:", - ["\U0001f9d8\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_in_lotus_position_tone1:", - ["\U0001f9d8\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_in_lotus_position_tone2:", - ["\U0001f9d8\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_in_lotus_position_tone3:", - ["\U0001f9d8\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_in_lotus_position_tone4:", - ["\U0001f9d8\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_in_lotus_position_tone5:", - ["\U0001f9d8\u200d\u2640\ufe0f"] = ":woman_in_lotus_position:", - ["\U0001f469\U0001f3fb\u200d\U0001f9bd\u200d\u27a1\ufe0f"] = ":woman_in_manual_wheelchair_facing_right_tone1:", - ["\U0001f469\U0001f3fc\u200d\U0001f9bd\u200d\u27a1\ufe0f"] = ":woman_in_manual_wheelchair_facing_right_tone2:", - ["\U0001f469\U0001f3fd\u200d\U0001f9bd\u200d\u27a1\ufe0f"] = ":woman_in_manual_wheelchair_facing_right_tone3:", - ["\U0001f469\U0001f3fe\u200d\U0001f9bd\u200d\u27a1\ufe0f"] = ":woman_in_manual_wheelchair_facing_right_tone4:", - ["\U0001f469\U0001f3ff\u200d\U0001f9bd\u200d\u27a1\ufe0f"] = ":woman_in_manual_wheelchair_facing_right_tone5:", - ["\U0001f469\u200d\U0001f9bd\u200d\u27a1\ufe0f"] = ":woman_in_manual_wheelchair_facing_right:", - ["\U0001f469\U0001f3fb\u200d\U0001f9bd"] = ":woman_in_manual_wheelchair_tone1:", - ["\U0001f469\U0001f3fc\u200d\U0001f9bd"] = ":woman_in_manual_wheelchair_tone2:", - ["\U0001f469\U0001f3fd\u200d\U0001f9bd"] = ":woman_in_manual_wheelchair_tone3:", - ["\U0001f469\U0001f3fe\u200d\U0001f9bd"] = ":woman_in_manual_wheelchair_tone4:", - ["\U0001f469\U0001f3ff\u200d\U0001f9bd"] = ":woman_in_manual_wheelchair_tone5:", - ["\U0001f469\u200d\U0001f9bd"] = ":woman_in_manual_wheelchair:", - ["\U0001f469\U0001f3fb\u200d\U0001f9bc\u200d\u27a1\ufe0f"] = ":woman_in_motorized_wheelchair_facing_right_tone1:", - ["\U0001f469\U0001f3fc\u200d\U0001f9bc\u200d\u27a1\ufe0f"] = ":woman_in_motorized_wheelchair_facing_right_tone2:", - ["\U0001f469\U0001f3fd\u200d\U0001f9bc\u200d\u27a1\ufe0f"] = ":woman_in_motorized_wheelchair_facing_right_tone3:", - ["\U0001f469\U0001f3fe\u200d\U0001f9bc\u200d\u27a1\ufe0f"] = ":woman_in_motorized_wheelchair_facing_right_tone4:", - ["\U0001f469\U0001f3ff\u200d\U0001f9bc\u200d\u27a1\ufe0f"] = ":woman_in_motorized_wheelchair_facing_right_tone5:", - ["\U0001f469\u200d\U0001f9bc\u200d\u27a1\ufe0f"] = ":woman_in_motorized_wheelchair_facing_right:", - ["\U0001f469\U0001f3fb\u200d\U0001f9bc"] = ":woman_in_motorized_wheelchair_tone1:", - ["\U0001f469\U0001f3fc\u200d\U0001f9bc"] = ":woman_in_motorized_wheelchair_tone2:", - ["\U0001f469\U0001f3fd\u200d\U0001f9bc"] = ":woman_in_motorized_wheelchair_tone3:", - ["\U0001f469\U0001f3fe\u200d\U0001f9bc"] = ":woman_in_motorized_wheelchair_tone4:", - ["\U0001f469\U0001f3ff\u200d\U0001f9bc"] = ":woman_in_motorized_wheelchair_tone5:", - ["\U0001f469\u200d\U0001f9bc"] = ":woman_in_motorized_wheelchair:", - ["\U0001f9d6\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_in_steamy_room_tone1:", - ["\U0001f9d6\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_in_steamy_room_tone2:", - ["\U0001f9d6\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_in_steamy_room_tone3:", - ["\U0001f9d6\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_in_steamy_room_tone4:", - ["\U0001f9d6\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_in_steamy_room_tone5:", - ["\U0001f9d6\u200d\u2640\ufe0f"] = ":woman_in_steamy_room:", - ["\U0001f935\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_in_tuxedo_tone1:", - ["\U0001f935\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_in_tuxedo_tone2:", - ["\U0001f935\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_in_tuxedo_tone3:", - ["\U0001f935\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_in_tuxedo_tone4:", - ["\U0001f935\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_in_tuxedo_tone5:", - ["\U0001f935\u200d\u2640\ufe0f"] = ":woman_in_tuxedo:", - ["\U0001f469\U0001f3fb\u200d\u2696\ufe0f"] = ":woman_judge_tone1:", - ["\U0001f469\U0001f3fc\u200d\u2696\ufe0f"] = ":woman_judge_tone2:", - ["\U0001f469\U0001f3fd\u200d\u2696\ufe0f"] = ":woman_judge_tone3:", - ["\U0001f469\U0001f3fe\u200d\u2696\ufe0f"] = ":woman_judge_tone4:", - ["\U0001f469\U0001f3ff\u200d\u2696\ufe0f"] = ":woman_judge_tone5:", - ["\U0001f469\u200d\u2696\ufe0f"] = ":woman_judge:", - ["\U0001f939\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_juggling_tone1:", - ["\U0001f939\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_juggling_tone2:", - ["\U0001f939\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_juggling_tone3:", - ["\U0001f939\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_juggling_tone4:", - ["\U0001f939\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_juggling_tone5:", - ["\U0001f939\u200d\u2640\ufe0f"] = ":woman_juggling:", - ["\U0001f9ce\U0001f3fb\u200d\u2640\ufe0f\u200d\u27a1\ufe0f"] = ":woman_kneeling_facing_right_tone1:", - ["\U0001f9ce\U0001f3fc\u200d\u2640\ufe0f\u200d\u27a1\ufe0f"] = ":woman_kneeling_facing_right_tone2:", - ["\U0001f9ce\U0001f3fd\u200d\u2640\ufe0f\u200d\u27a1\ufe0f"] = ":woman_kneeling_facing_right_tone3:", - ["\U0001f9ce\U0001f3fe\u200d\u2640\ufe0f\u200d\u27a1\ufe0f"] = ":woman_kneeling_facing_right_tone4:", - ["\U0001f9ce\U0001f3ff\u200d\u2640\ufe0f\u200d\u27a1\ufe0f"] = ":woman_kneeling_facing_right_tone5:", - ["\U0001f9ce\u200d\u2640\ufe0f\u200d\u27a1\ufe0f"] = ":woman_kneeling_facing_right:", - ["\U0001f9ce\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_kneeling_tone1:", - ["\U0001f9ce\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_kneeling_tone2:", - ["\U0001f9ce\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_kneeling_tone3:", - ["\U0001f9ce\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_kneeling_tone4:", - ["\U0001f9ce\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_kneeling_tone5:", - ["\U0001f9ce\u200d\u2640\ufe0f"] = ":woman_kneeling:", - ["\U0001f3cb\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_lifting_weights_tone1:", - ["\U0001f3cb\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_lifting_weights_tone2:", - ["\U0001f3cb\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_lifting_weights_tone3:", - ["\U0001f3cb\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_lifting_weights_tone4:", - ["\U0001f3cb\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_lifting_weights_tone5:", - ["\U0001f3cb\ufe0f\u200d\u2640\ufe0f"] = ":woman_lifting_weights:", - ["\U0001f9d9\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_mage_tone1:", - ["\U0001f9d9\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_mage_tone2:", - ["\U0001f9d9\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_mage_tone3:", - ["\U0001f9d9\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_mage_tone4:", - ["\U0001f9d9\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_mage_tone5:", - ["\U0001f9d9\u200d\u2640\ufe0f"] = ":woman_mage:", - ["\U0001f469\U0001f3fb\u200d\U0001f527"] = ":woman_mechanic_tone1:", - ["\U0001f469\U0001f3fc\u200d\U0001f527"] = ":woman_mechanic_tone2:", - ["\U0001f469\U0001f3fd\u200d\U0001f527"] = ":woman_mechanic_tone3:", - ["\U0001f469\U0001f3fe\u200d\U0001f527"] = ":woman_mechanic_tone4:", - ["\U0001f469\U0001f3ff\u200d\U0001f527"] = ":woman_mechanic_tone5:", - ["\U0001f469\u200d\U0001f527"] = ":woman_mechanic:", - ["\U0001f6b5\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_mountain_biking_tone1:", - ["\U0001f6b5\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_mountain_biking_tone2:", - ["\U0001f6b5\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_mountain_biking_tone3:", - ["\U0001f6b5\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_mountain_biking_tone4:", - ["\U0001f6b5\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_mountain_biking_tone5:", - ["\U0001f6b5\u200d\u2640\ufe0f"] = ":woman_mountain_biking:", - ["\U0001f469\U0001f3fb\u200d\U0001f4bc"] = ":woman_office_worker_tone1:", - ["\U0001f469\U0001f3fc\u200d\U0001f4bc"] = ":woman_office_worker_tone2:", - ["\U0001f469\U0001f3fd\u200d\U0001f4bc"] = ":woman_office_worker_tone3:", - ["\U0001f469\U0001f3fe\u200d\U0001f4bc"] = ":woman_office_worker_tone4:", - ["\U0001f469\U0001f3ff\u200d\U0001f4bc"] = ":woman_office_worker_tone5:", - ["\U0001f469\u200d\U0001f4bc"] = ":woman_office_worker:", - ["\U0001f469\U0001f3fb\u200d\u2708\ufe0f"] = ":woman_pilot_tone1:", - ["\U0001f469\U0001f3fc\u200d\u2708\ufe0f"] = ":woman_pilot_tone2:", - ["\U0001f469\U0001f3fd\u200d\u2708\ufe0f"] = ":woman_pilot_tone3:", - ["\U0001f469\U0001f3fe\u200d\u2708\ufe0f"] = ":woman_pilot_tone4:", - ["\U0001f469\U0001f3ff\u200d\u2708\ufe0f"] = ":woman_pilot_tone5:", - ["\U0001f469\u200d\u2708\ufe0f"] = ":woman_pilot:", - ["\U0001f93e\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_playing_handball_tone1:", - ["\U0001f93e\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_playing_handball_tone2:", - ["\U0001f93e\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_playing_handball_tone3:", - ["\U0001f93e\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_playing_handball_tone4:", - ["\U0001f93e\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_playing_handball_tone5:", - ["\U0001f93e\u200d\u2640\ufe0f"] = ":woman_playing_handball:", - ["\U0001f93d\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_playing_water_polo_tone1:", - ["\U0001f93d\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_playing_water_polo_tone2:", - ["\U0001f93d\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_playing_water_polo_tone3:", - ["\U0001f93d\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_playing_water_polo_tone4:", - ["\U0001f93d\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_playing_water_polo_tone5:", - ["\U0001f93d\u200d\u2640\ufe0f"] = ":woman_playing_water_polo:", - ["\U0001f46e\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_police_officer_tone1:", - ["\U0001f46e\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_police_officer_tone2:", - ["\U0001f46e\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_police_officer_tone3:", - ["\U0001f46e\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_police_officer_tone4:", - ["\U0001f46e\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_police_officer_tone5:", - ["\U0001f46e\u200d\u2640\ufe0f"] = ":woman_police_officer:", - ["\U0001f64e\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_pouting_tone1:", - ["\U0001f64e\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_pouting_tone2:", - ["\U0001f64e\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_pouting_tone3:", - ["\U0001f64e\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_pouting_tone4:", - ["\U0001f64e\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_pouting_tone5:", - ["\U0001f64e\u200d\u2640\ufe0f"] = ":woman_pouting:", - ["\U0001f64b\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_raising_hand_tone1:", - ["\U0001f64b\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_raising_hand_tone2:", - ["\U0001f64b\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_raising_hand_tone3:", - ["\U0001f64b\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_raising_hand_tone4:", - ["\U0001f64b\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_raising_hand_tone5:", - ["\U0001f64b\u200d\u2640\ufe0f"] = ":woman_raising_hand:", - ["\U0001f469\U0001f3fb\u200d\U0001f9b0"] = ":woman_red_haired_tone1:", - ["\U0001f469\U0001f3fc\u200d\U0001f9b0"] = ":woman_red_haired_tone2:", - ["\U0001f469\U0001f3fd\u200d\U0001f9b0"] = ":woman_red_haired_tone3:", - ["\U0001f469\U0001f3fe\u200d\U0001f9b0"] = ":woman_red_haired_tone4:", - ["\U0001f469\U0001f3ff\u200d\U0001f9b0"] = ":woman_red_haired_tone5:", - ["\U0001f469\u200d\U0001f9b0"] = ":woman_red_haired:", - ["\U0001f6a3\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_rowing_boat_tone1:", - ["\U0001f6a3\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_rowing_boat_tone2:", - ["\U0001f6a3\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_rowing_boat_tone3:", - ["\U0001f6a3\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_rowing_boat_tone4:", - ["\U0001f6a3\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_rowing_boat_tone5:", - ["\U0001f6a3\u200d\u2640\ufe0f"] = ":woman_rowing_boat:", - ["\U0001f3c3\U0001f3fb\u200d\u2640\ufe0f\u200d\u27a1\ufe0f"] = ":woman_running_facing_right_tone1:", - ["\U0001f3c3\U0001f3fc\u200d\u2640\ufe0f\u200d\u27a1\ufe0f"] = ":woman_running_facing_right_tone2:", - ["\U0001f3c3\U0001f3fd\u200d\u2640\ufe0f\u200d\u27a1\ufe0f"] = ":woman_running_facing_right_tone3:", - ["\U0001f3c3\U0001f3fe\u200d\u2640\ufe0f\u200d\u27a1\ufe0f"] = ":woman_running_facing_right_tone4:", - ["\U0001f3c3\U0001f3ff\u200d\u2640\ufe0f\u200d\u27a1\ufe0f"] = ":woman_running_facing_right_tone5:", - ["\U0001f3c3\u200d\u2640\ufe0f\u200d\u27a1\ufe0f"] = ":woman_running_facing_right:", - ["\U0001f3c3\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_running_tone1:", - ["\U0001f3c3\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_running_tone2:", - ["\U0001f3c3\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_running_tone3:", - ["\U0001f3c3\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_running_tone4:", - ["\U0001f3c3\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_running_tone5:", - ["\U0001f3c3\u200d\u2640\ufe0f"] = ":woman_running:", - ["\U0001f469\U0001f3fb\u200d\U0001f52c"] = ":woman_scientist_tone1:", - ["\U0001f469\U0001f3fc\u200d\U0001f52c"] = ":woman_scientist_tone2:", - ["\U0001f469\U0001f3fd\u200d\U0001f52c"] = ":woman_scientist_tone3:", - ["\U0001f469\U0001f3fe\u200d\U0001f52c"] = ":woman_scientist_tone4:", - ["\U0001f469\U0001f3ff\u200d\U0001f52c"] = ":woman_scientist_tone5:", - ["\U0001f469\u200d\U0001f52c"] = ":woman_scientist:", - ["\U0001f937\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_shrugging_tone1:", - ["\U0001f937\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_shrugging_tone2:", - ["\U0001f937\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_shrugging_tone3:", - ["\U0001f937\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_shrugging_tone4:", - ["\U0001f937\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_shrugging_tone5:", - ["\U0001f937\u200d\u2640\ufe0f"] = ":woman_shrugging:", - ["\U0001f469\U0001f3fb\u200d\U0001f3a4"] = ":woman_singer_tone1:", - ["\U0001f469\U0001f3fc\u200d\U0001f3a4"] = ":woman_singer_tone2:", - ["\U0001f469\U0001f3fd\u200d\U0001f3a4"] = ":woman_singer_tone3:", - ["\U0001f469\U0001f3fe\u200d\U0001f3a4"] = ":woman_singer_tone4:", - ["\U0001f469\U0001f3ff\u200d\U0001f3a4"] = ":woman_singer_tone5:", - ["\U0001f469\u200d\U0001f3a4"] = ":woman_singer:", - ["\U0001f9cd\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_standing_tone1:", - ["\U0001f9cd\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_standing_tone2:", - ["\U0001f9cd\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_standing_tone3:", - ["\U0001f9cd\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_standing_tone4:", - ["\U0001f9cd\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_standing_tone5:", - ["\U0001f9cd\u200d\u2640\ufe0f"] = ":woman_standing:", - ["\U0001f469\U0001f3fb\u200d\U0001f393"] = ":woman_student_tone1:", - ["\U0001f469\U0001f3fc\u200d\U0001f393"] = ":woman_student_tone2:", - ["\U0001f469\U0001f3fd\u200d\U0001f393"] = ":woman_student_tone3:", - ["\U0001f469\U0001f3fe\u200d\U0001f393"] = ":woman_student_tone4:", - ["\U0001f469\U0001f3ff\u200d\U0001f393"] = ":woman_student_tone5:", - ["\U0001f469\u200d\U0001f393"] = ":woman_student:", - ["\U0001f9b8\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_superhero_tone1:", - ["\U0001f9b8\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_superhero_tone2:", - ["\U0001f9b8\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_superhero_tone3:", - ["\U0001f9b8\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_superhero_tone4:", - ["\U0001f9b8\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_superhero_tone5:", - ["\U0001f9b8\u200d\u2640\ufe0f"] = ":woman_superhero:", - ["\U0001f9b9\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_supervillain_tone1:", - ["\U0001f9b9\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_supervillain_tone2:", - ["\U0001f9b9\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_supervillain_tone3:", - ["\U0001f9b9\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_supervillain_tone4:", - ["\U0001f9b9\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_supervillain_tone5:", - ["\U0001f9b9\u200d\u2640\ufe0f"] = ":woman_supervillain:", - ["\U0001f3c4\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_surfing_tone1:", - ["\U0001f3c4\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_surfing_tone2:", - ["\U0001f3c4\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_surfing_tone3:", - ["\U0001f3c4\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_surfing_tone4:", - ["\U0001f3c4\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_surfing_tone5:", - ["\U0001f3c4\u200d\u2640\ufe0f"] = ":woman_surfing:", - ["\U0001f3ca\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_swimming_tone1:", - ["\U0001f3ca\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_swimming_tone2:", - ["\U0001f3ca\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_swimming_tone3:", - ["\U0001f3ca\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_swimming_tone4:", - ["\U0001f3ca\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_swimming_tone5:", - ["\U0001f3ca\u200d\u2640\ufe0f"] = ":woman_swimming:", - ["\U0001f469\U0001f3fb\u200d\U0001f3eb"] = ":woman_teacher_tone1:", - ["\U0001f469\U0001f3fc\u200d\U0001f3eb"] = ":woman_teacher_tone2:", - ["\U0001f469\U0001f3fd\u200d\U0001f3eb"] = ":woman_teacher_tone3:", - ["\U0001f469\U0001f3fe\u200d\U0001f3eb"] = ":woman_teacher_tone4:", - ["\U0001f469\U0001f3ff\u200d\U0001f3eb"] = ":woman_teacher_tone5:", - ["\U0001f469\u200d\U0001f3eb"] = ":woman_teacher:", - ["\U0001f469\U0001f3fb\u200d\U0001f4bb"] = ":woman_technologist_tone1:", - ["\U0001f469\U0001f3fc\u200d\U0001f4bb"] = ":woman_technologist_tone2:", - ["\U0001f469\U0001f3fd\u200d\U0001f4bb"] = ":woman_technologist_tone3:", - ["\U0001f469\U0001f3fe\u200d\U0001f4bb"] = ":woman_technologist_tone4:", - ["\U0001f469\U0001f3ff\u200d\U0001f4bb"] = ":woman_technologist_tone5:", - ["\U0001f469\u200d\U0001f4bb"] = ":woman_technologist:", - ["\U0001f481\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_tipping_hand_tone1:", - ["\U0001f481\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_tipping_hand_tone2:", - ["\U0001f481\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_tipping_hand_tone3:", - ["\U0001f481\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_tipping_hand_tone4:", - ["\U0001f481\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_tipping_hand_tone5:", - ["\U0001f481\u200d\u2640\ufe0f"] = ":woman_tipping_hand:", - ["\U0001f9d4\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_tone1_beard:", - ["\U0001f469\U0001f3fb"] = ":woman_tone1:", - ["\U0001f9d4\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_tone2_beard:", - ["\U0001f469\U0001f3fc"] = ":woman_tone2:", - ["\U0001f9d4\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_tone3_beard:", - ["\U0001f469\U0001f3fd"] = ":woman_tone3:", - ["\U0001f9d4\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_tone4_beard:", - ["\U0001f469\U0001f3fe"] = ":woman_tone4:", - ["\U0001f9d4\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_tone5_beard:", - ["\U0001f469\U0001f3ff"] = ":woman_tone5:", - ["\U0001f9db\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_vampire_tone1:", - ["\U0001f9db\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_vampire_tone2:", - ["\U0001f9db\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_vampire_tone3:", - ["\U0001f9db\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_vampire_tone4:", - ["\U0001f9db\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_vampire_tone5:", - ["\U0001f9db\u200d\u2640\ufe0f"] = ":woman_vampire:", - ["\U0001f6b6\U0001f3fb\u200d\u2640\ufe0f\u200d\u27a1\ufe0f"] = ":woman_walking_facing_right_tone1:", - ["\U0001f6b6\U0001f3fc\u200d\u2640\ufe0f\u200d\u27a1\ufe0f"] = ":woman_walking_facing_right_tone2:", - ["\U0001f6b6\U0001f3fd\u200d\u2640\ufe0f\u200d\u27a1\ufe0f"] = ":woman_walking_facing_right_tone3:", - ["\U0001f6b6\U0001f3fe\u200d\u2640\ufe0f\u200d\u27a1\ufe0f"] = ":woman_walking_facing_right_tone4:", - ["\U0001f6b6\U0001f3ff\u200d\u2640\ufe0f\u200d\u27a1\ufe0f"] = ":woman_walking_facing_right_tone5:", - ["\U0001f6b6\u200d\u2640\ufe0f\u200d\u27a1\ufe0f"] = ":woman_walking_facing_right:", - ["\U0001f6b6\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_walking_tone1:", - ["\U0001f6b6\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_walking_tone2:", - ["\U0001f6b6\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_walking_tone3:", - ["\U0001f6b6\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_walking_tone4:", - ["\U0001f6b6\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_walking_tone5:", - ["\U0001f6b6\u200d\u2640\ufe0f"] = ":woman_walking:", - ["\U0001f473\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_wearing_turban_tone1:", - ["\U0001f473\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_wearing_turban_tone2:", - ["\U0001f473\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_wearing_turban_tone3:", - ["\U0001f473\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_wearing_turban_tone4:", - ["\U0001f473\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_wearing_turban_tone5:", - ["\U0001f473\u200d\u2640\ufe0f"] = ":woman_wearing_turban:", - ["\U0001f469\U0001f3fb\u200d\U0001f9b3"] = ":woman_white_haired_tone1:", - ["\U0001f469\U0001f3fc\u200d\U0001f9b3"] = ":woman_white_haired_tone2:", - ["\U0001f469\U0001f3fd\u200d\U0001f9b3"] = ":woman_white_haired_tone3:", - ["\U0001f469\U0001f3fe\u200d\U0001f9b3"] = ":woman_white_haired_tone4:", - ["\U0001f469\U0001f3ff\u200d\U0001f9b3"] = ":woman_white_haired_tone5:", - ["\U0001f469\u200d\U0001f9b3"] = ":woman_white_haired:", - ["\U0001f9d5\U0001f3fb"] = ":woman_with_headscarf_tone1:", - ["\U0001f9d5\U0001f3fc"] = ":woman_with_headscarf_tone2:", - ["\U0001f9d5\U0001f3fd"] = ":woman_with_headscarf_tone3:", - ["\U0001f9d5\U0001f3fe"] = ":woman_with_headscarf_tone4:", - ["\U0001f9d5\U0001f3ff"] = ":woman_with_headscarf_tone5:", - ["\U0001f9d5"] = ":woman_with_headscarf:", - ["\U0001f469\U0001f3fb\u200d\U0001f9af"] = ":woman_with_probing_cane_tone1:", - ["\U0001f469\U0001f3fc\u200d\U0001f9af"] = ":woman_with_probing_cane_tone2:", - ["\U0001f469\U0001f3fd\u200d\U0001f9af"] = ":woman_with_probing_cane_tone3:", - ["\U0001f469\U0001f3fe\u200d\U0001f9af"] = ":woman_with_probing_cane_tone4:", - ["\U0001f469\U0001f3ff\u200d\U0001f9af"] = ":woman_with_probing_cane_tone5:", - ["\U0001f469\u200d\U0001f9af"] = ":woman_with_probing_cane:", - ["\U0001f470\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_with_veil_tone1:", - ["\U0001f470\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_with_veil_tone2:", - ["\U0001f470\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_with_veil_tone3:", - ["\U0001f470\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_with_veil_tone4:", - ["\U0001f470\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_with_veil_tone5:", - ["\U0001f470\u200d\u2640\ufe0f"] = ":woman_with_veil:", - ["\U0001f469\U0001f3fb\u200d\U0001f9af\u200d\u27a1\ufe0f"] = ":woman_with_white_cane_facing_right_tone1:", - ["\U0001f469\U0001f3fc\u200d\U0001f9af\u200d\u27a1\ufe0f"] = ":woman_with_white_cane_facing_right_tone2:", - ["\U0001f469\U0001f3fd\u200d\U0001f9af\u200d\u27a1\ufe0f"] = ":woman_with_white_cane_facing_right_tone3:", - ["\U0001f469\U0001f3fe\u200d\U0001f9af\u200d\u27a1\ufe0f"] = ":woman_with_white_cane_facing_right_tone4:", - ["\U0001f469\U0001f3ff\u200d\U0001f9af\u200d\u27a1\ufe0f"] = ":woman_with_white_cane_facing_right_tone5:", - ["\U0001f469\u200d\U0001f9af\u200d\u27a1\ufe0f"] = ":woman_with_white_cane_facing_right:", - ["\U0001f9df\u200d\u2640\ufe0f"] = ":woman_zombie:", - ["\U0001f469"] = ":woman:", - ["\U0001f45a"] = ":womans_clothes:", - ["\U0001f97f"] = ":womans_flat_shoe:", - ["\U0001f452"] = ":womans_hat:", - ["\U0001f46d"] = ":women_holding_hands_tone5_tone4:", - ["\U0001f46f\u200d\u2640\ufe0f"] = ":women_with_bunny_ears_partying:", - ["\U0001f93c\u200d\u2640\ufe0f"] = ":women_wrestling:", - ["\U0001f6ba"] = ":womens:", - ["\U0001fab5"] = ":wood:", - ["\U0001f974"] = ":woozy_face:", - ["\U0001fab1"] = ":worm:", - ["\U0001f61f"] = ":worried:", - ["\U0001f527"] = ":wrench:", - ["\u270d\U0001f3fb"] = ":writing_hand_tone1:", - ["\u270d\U0001f3fc"] = ":writing_hand_tone2:", - ["\u270d\U0001f3fd"] = ":writing_hand_tone3:", - ["\u270d\U0001f3fe"] = ":writing_hand_tone4:", - ["\u270d\U0001f3ff"] = ":writing_hand_tone5:", - ["\u270d\ufe0f"] = ":writing_hand:", - ["\u270d"] = ":writing_hand:", - ["\U0001fa7b"] = ":x_ray:", - ["\u274c"] = ":x:", - ["\U0001f9f6"] = ":yarn:", - ["\U0001f971"] = ":yawning_face:", - ["\U0001f7e1"] = ":yellow_circle:", - ["\U0001f49b"] = ":yellow_heart:", - ["\U0001f7e8"] = ":yellow_square:", - ["\U0001f4b4"] = ":yen:", - ["\u262f\ufe0f"] = ":yin_yang:", - ["\u262f"] = ":yin_yang:", - ["\U0001fa80"] = ":yo_yo:", - ["\U0001f60b"] = ":yum:", - ["\U0001f92a"] = ":zany_face:", - ["\u26a1"] = ":zap:", - ["\U0001f993"] = ":zebra:", - ["\u0030\ufe0f\u20e3"] = ":zero:", - ["\u0030\u20e3"] = ":zero:", - ["\U0001f910"] = ":zipper_mouth:", - ["\U0001f9df"] = ":zombie:", - ["\U0001f4a4"] = ":zzz:", - }.ToFrozenDictionary(); - #endregion - } -} +using System.Collections.Frozen; +using System.Collections.Generic; + +namespace DSharpPlus.Entities; + +public partial class DiscordEmoji +{ + /// + /// Gets a mapping of :name: => unicode. + /// + private static FrozenDictionary UnicodeEmojis { get; } + + /// + /// Gets a mapping of unicode => :name:. + /// + private static FrozenDictionary DiscordNameLookup { get; } + + static DiscordEmoji() + { + #region Generated Emoji Map + // Generated by Discord Emoji Map generator by Emzi0767 + // Generated from: + // version 2025-04-07T09.04.02.807+00:00 + // url https://static.emzi0767.com/misc/discordEmojiMap.min.json + // definition count 3,799 + + UnicodeEmojis = new Dictionary + { + [":100:"] = "\U0001f4af", + [":1234:"] = "\U0001f522", + [":input_numbers:"] = "\U0001f522", + [":8ball:"] = "\U0001f3b1", + [":a:"] = "\U0001f170\ufe0f", + [":ab:"] = "\U0001f18e", + [":abacus:"] = "\U0001f9ee", + [":abc:"] = "\U0001f524", + [":abcd:"] = "\U0001f521", + [":accept:"] = "\U0001f251", + [":accordion:"] = "\U0001fa97", + [":adhesive_bandage:"] = "\U0001fa79", + [":adult:"] = "\U0001f9d1", + [":person:"] = "\U0001f9d1", + [":adult_tone1:"] = "\U0001f9d1\U0001f3fb", + [":adult_light_skin_tone:"] = "\U0001f9d1\U0001f3fb", + [":adult::skin-tone-1:"] = "\U0001f9d1\U0001f3fb", + [":person::skin-tone-1:"] = "\U0001f9d1\U0001f3fb", + [":adult_tone2:"] = "\U0001f9d1\U0001f3fc", + [":adult_medium_light_skin_tone:"] = "\U0001f9d1\U0001f3fc", + [":adult::skin-tone-2:"] = "\U0001f9d1\U0001f3fc", + [":person::skin-tone-2:"] = "\U0001f9d1\U0001f3fc", + [":adult_tone3:"] = "\U0001f9d1\U0001f3fd", + [":adult_medium_skin_tone:"] = "\U0001f9d1\U0001f3fd", + [":adult::skin-tone-3:"] = "\U0001f9d1\U0001f3fd", + [":person::skin-tone-3:"] = "\U0001f9d1\U0001f3fd", + [":adult_tone4:"] = "\U0001f9d1\U0001f3fe", + [":adult_medium_dark_skin_tone:"] = "\U0001f9d1\U0001f3fe", + [":adult::skin-tone-4:"] = "\U0001f9d1\U0001f3fe", + [":person::skin-tone-4:"] = "\U0001f9d1\U0001f3fe", + [":adult_tone5:"] = "\U0001f9d1\U0001f3ff", + [":adult_dark_skin_tone:"] = "\U0001f9d1\U0001f3ff", + [":adult::skin-tone-5:"] = "\U0001f9d1\U0001f3ff", + [":person::skin-tone-5:"] = "\U0001f9d1\U0001f3ff", + [":aerial_tramway:"] = "\U0001f6a1", + [":airplane:"] = "\u2708\ufe0f", + [":airplane_arriving:"] = "\U0001f6ec", + [":airplane_departure:"] = "\U0001f6eb", + [":airplane_small:"] = "\U0001f6e9\ufe0f", + [":small_airplane:"] = "\U0001f6e9\ufe0f", + [":alarm_clock:"] = "\u23f0", + [":alembic:"] = "\u2697\ufe0f", + [":alien:"] = "\U0001f47d", + [":ambulance:"] = "\U0001f691", + [":amphora:"] = "\U0001f3fa", + [":anatomical_heart:"] = "\U0001fac0", + [":anchor:"] = "\u2693", + [":angel:"] = "\U0001f47c", + [":baby_angel:"] = "\U0001f47c", + [":angel_tone1:"] = "\U0001f47c\U0001f3fb", + [":angel::skin-tone-1:"] = "\U0001f47c\U0001f3fb", + [":baby_angel::skin-tone-1:"] = "\U0001f47c\U0001f3fb", + [":angel_tone2:"] = "\U0001f47c\U0001f3fc", + [":angel::skin-tone-2:"] = "\U0001f47c\U0001f3fc", + [":baby_angel::skin-tone-2:"] = "\U0001f47c\U0001f3fc", + [":angel_tone3:"] = "\U0001f47c\U0001f3fd", + [":angel::skin-tone-3:"] = "\U0001f47c\U0001f3fd", + [":baby_angel::skin-tone-3:"] = "\U0001f47c\U0001f3fd", + [":angel_tone4:"] = "\U0001f47c\U0001f3fe", + [":angel::skin-tone-4:"] = "\U0001f47c\U0001f3fe", + [":baby_angel::skin-tone-4:"] = "\U0001f47c\U0001f3fe", + [":angel_tone5:"] = "\U0001f47c\U0001f3ff", + [":angel::skin-tone-5:"] = "\U0001f47c\U0001f3ff", + [":baby_angel::skin-tone-5:"] = "\U0001f47c\U0001f3ff", + [":anger:"] = "\U0001f4a2", + [":anger_right:"] = "\U0001f5ef\ufe0f", + [":right_anger_bubble:"] = "\U0001f5ef\ufe0f", + [":angry:"] = "\U0001f620", + [":angry_face:"] = "\U0001f620", + [">:("] = "\U0001f620", + [">:-("] = "\U0001f620", + [">=("] = "\U0001f620", + [">=-("] = "\U0001f620", + [":anguished:"] = "\U0001f627", + [":ant:"] = "\U0001f41c", + [":apple:"] = "\U0001f34e", + [":red_apple:"] = "\U0001f34e", + [":aquarius:"] = "\u2652", + [":aries:"] = "\u2648", + [":arrow_backward:"] = "\u25c0\ufe0f", + [":arrow_double_down:"] = "\u23ec", + [":arrow_double_up:"] = "\u23eb", + [":arrow_down:"] = "\u2b07\ufe0f", + [":down_arrow:"] = "\u2b07\ufe0f", + [":arrow_down_small:"] = "\U0001f53d", + [":arrow_forward:"] = "\u25b6\ufe0f", + [":arrow_heading_down:"] = "\u2935\ufe0f", + [":arrow_heading_up:"] = "\u2934\ufe0f", + [":arrow_left:"] = "\u2b05\ufe0f", + [":left_arrow:"] = "\u2b05\ufe0f", + [":arrow_lower_left:"] = "\u2199\ufe0f", + [":arrow_lower_right:"] = "\u2198\ufe0f", + [":arrow_right:"] = "\u27a1\ufe0f", + [":right_arrow:"] = "\u27a1\ufe0f", + [":arrow_right_hook:"] = "\u21aa\ufe0f", + [":arrow_up:"] = "\u2b06\ufe0f", + [":up_arrow:"] = "\u2b06\ufe0f", + [":arrow_up_down:"] = "\u2195\ufe0f", + [":up_down_arrow:"] = "\u2195\ufe0f", + [":arrow_up_small:"] = "\U0001f53c", + [":arrow_upper_left:"] = "\u2196\ufe0f", + [":up_left_arrow:"] = "\u2196\ufe0f", + [":arrow_upper_right:"] = "\u2197\ufe0f", + [":arrows_clockwise:"] = "\U0001f503", + [":arrows_counterclockwise:"] = "\U0001f504", + [":art:"] = "\U0001f3a8", + [":articulated_lorry:"] = "\U0001f69b", + [":artist:"] = "\U0001f9d1\u200d\U0001f3a8", + [":artist_tone1:"] = "\U0001f9d1\U0001f3fb\u200d\U0001f3a8", + [":artist_light_skin_tone:"] = "\U0001f9d1\U0001f3fb\u200d\U0001f3a8", + [":artist::skin-tone-1:"] = "\U0001f9d1\U0001f3fb\u200d\U0001f3a8", + [":artist_tone2:"] = "\U0001f9d1\U0001f3fc\u200d\U0001f3a8", + [":artist_medium_light_skin_tone:"] = "\U0001f9d1\U0001f3fc\u200d\U0001f3a8", + [":artist::skin-tone-2:"] = "\U0001f9d1\U0001f3fc\u200d\U0001f3a8", + [":artist_tone3:"] = "\U0001f9d1\U0001f3fd\u200d\U0001f3a8", + [":artist_medium_skin_tone:"] = "\U0001f9d1\U0001f3fd\u200d\U0001f3a8", + [":artist::skin-tone-3:"] = "\U0001f9d1\U0001f3fd\u200d\U0001f3a8", + [":artist_tone4:"] = "\U0001f9d1\U0001f3fe\u200d\U0001f3a8", + [":artist_medium_dark_skin_tone:"] = "\U0001f9d1\U0001f3fe\u200d\U0001f3a8", + [":artist::skin-tone-4:"] = "\U0001f9d1\U0001f3fe\u200d\U0001f3a8", + [":artist_tone5:"] = "\U0001f9d1\U0001f3ff\u200d\U0001f3a8", + [":artist_dark_skin_tone:"] = "\U0001f9d1\U0001f3ff\u200d\U0001f3a8", + [":artist::skin-tone-5:"] = "\U0001f9d1\U0001f3ff\u200d\U0001f3a8", + [":asterisk:"] = "\u002a\ufe0f\u20e3", + [":keycap_asterisk:"] = "\u002a\ufe0f\u20e3", + [":astonished:"] = "\U0001f632", + [":astronaut:"] = "\U0001f9d1\u200d\U0001f680", + [":astronaut_tone1:"] = "\U0001f9d1\U0001f3fb\u200d\U0001f680", + [":astronaut_light_skin_tone:"] = "\U0001f9d1\U0001f3fb\u200d\U0001f680", + [":astronaut::skin-tone-1:"] = "\U0001f9d1\U0001f3fb\u200d\U0001f680", + [":astronaut_tone2:"] = "\U0001f9d1\U0001f3fc\u200d\U0001f680", + [":astronaut_medium_light_skin_tone:"] = "\U0001f9d1\U0001f3fc\u200d\U0001f680", + [":astronaut::skin-tone-2:"] = "\U0001f9d1\U0001f3fc\u200d\U0001f680", + [":astronaut_tone3:"] = "\U0001f9d1\U0001f3fd\u200d\U0001f680", + [":astronaut_medium_skin_tone:"] = "\U0001f9d1\U0001f3fd\u200d\U0001f680", + [":astronaut::skin-tone-3:"] = "\U0001f9d1\U0001f3fd\u200d\U0001f680", + [":astronaut_tone4:"] = "\U0001f9d1\U0001f3fe\u200d\U0001f680", + [":astronaut_medium_dark_skin_tone:"] = "\U0001f9d1\U0001f3fe\u200d\U0001f680", + [":astronaut::skin-tone-4:"] = "\U0001f9d1\U0001f3fe\u200d\U0001f680", + [":astronaut_tone5:"] = "\U0001f9d1\U0001f3ff\u200d\U0001f680", + [":astronaut_dark_skin_tone:"] = "\U0001f9d1\U0001f3ff\u200d\U0001f680", + [":astronaut::skin-tone-5:"] = "\U0001f9d1\U0001f3ff\u200d\U0001f680", + [":athletic_shoe:"] = "\U0001f45f", + [":running_shoe:"] = "\U0001f45f", + [":atm:"] = "\U0001f3e7", + [":atom:"] = "\u269b\ufe0f", + [":atom_symbol:"] = "\u269b\ufe0f", + [":auto_rickshaw:"] = "\U0001f6fa", + [":avocado:"] = "\U0001f951", + [":axe:"] = "\U0001fa93", + [":b:"] = "\U0001f171\ufe0f", + [":baby:"] = "\U0001f476", + [":baby_bottle:"] = "\U0001f37c", + [":baby_chick:"] = "\U0001f424", + [":baby_symbol:"] = "\U0001f6bc", + [":baby_tone1:"] = "\U0001f476\U0001f3fb", + [":baby::skin-tone-1:"] = "\U0001f476\U0001f3fb", + [":baby_tone2:"] = "\U0001f476\U0001f3fc", + [":baby::skin-tone-2:"] = "\U0001f476\U0001f3fc", + [":baby_tone3:"] = "\U0001f476\U0001f3fd", + [":baby::skin-tone-3:"] = "\U0001f476\U0001f3fd", + [":baby_tone4:"] = "\U0001f476\U0001f3fe", + [":baby::skin-tone-4:"] = "\U0001f476\U0001f3fe", + [":baby_tone5:"] = "\U0001f476\U0001f3ff", + [":baby::skin-tone-5:"] = "\U0001f476\U0001f3ff", + [":back:"] = "\U0001f519", + [":back_arrow:"] = "\U0001f519", + [":bacon:"] = "\U0001f953", + [":badger:"] = "\U0001f9a1", + [":badminton:"] = "\U0001f3f8", + [":bagel:"] = "\U0001f96f", + [":baggage_claim:"] = "\U0001f6c4", + [":ballet_shoes:"] = "\U0001fa70", + [":balloon:"] = "\U0001f388", + [":ballot_box:"] = "\U0001f5f3\ufe0f", + [":ballot_box_with_ballot:"] = "\U0001f5f3\ufe0f", + [":ballot_box_with_check:"] = "\u2611\ufe0f", + [":bamboo:"] = "\U0001f38d", + [":banana:"] = "\U0001f34c", + [":bangbang:"] = "\u203c\ufe0f", + [":banjo:"] = "\U0001fa95", + [":bank:"] = "\U0001f3e6", + [":bar_chart:"] = "\U0001f4ca", + [":barber:"] = "\U0001f488", + [":barber_pole:"] = "\U0001f488", + [":baseball:"] = "\u26be", + [":basket:"] = "\U0001f9fa", + [":basketball:"] = "\U0001f3c0", + [":bat:"] = "\U0001f987", + [":bath:"] = "\U0001f6c0", + [":bath_tone1:"] = "\U0001f6c0\U0001f3fb", + [":bath::skin-tone-1:"] = "\U0001f6c0\U0001f3fb", + [":bath_tone2:"] = "\U0001f6c0\U0001f3fc", + [":bath::skin-tone-2:"] = "\U0001f6c0\U0001f3fc", + [":bath_tone3:"] = "\U0001f6c0\U0001f3fd", + [":bath::skin-tone-3:"] = "\U0001f6c0\U0001f3fd", + [":bath_tone4:"] = "\U0001f6c0\U0001f3fe", + [":bath::skin-tone-4:"] = "\U0001f6c0\U0001f3fe", + [":bath_tone5:"] = "\U0001f6c0\U0001f3ff", + [":bath::skin-tone-5:"] = "\U0001f6c0\U0001f3ff", + [":bathtub:"] = "\U0001f6c1", + [":battery:"] = "\U0001f50b", + [":beach:"] = "\U0001f3d6\ufe0f", + [":beach_with_umbrella:"] = "\U0001f3d6\ufe0f", + [":beach_umbrella:"] = "\u26f1\ufe0f", + [":umbrella_on_ground:"] = "\u26f1\ufe0f", + [":beans:"] = "\U0001fad8", + [":bear:"] = "\U0001f43b", + [":bearded_person:"] = "\U0001f9d4", + [":person_beard:"] = "\U0001f9d4", + [":bearded_person_tone1:"] = "\U0001f9d4\U0001f3fb", + [":bearded_person_light_skin_tone:"] = "\U0001f9d4\U0001f3fb", + [":bearded_person::skin-tone-1:"] = "\U0001f9d4\U0001f3fb", + [":person_beard::skin-tone-1:"] = "\U0001f9d4\U0001f3fb", + [":bearded_person_tone2:"] = "\U0001f9d4\U0001f3fc", + [":bearded_person_medium_light_skin_tone:"] = "\U0001f9d4\U0001f3fc", + [":bearded_person::skin-tone-2:"] = "\U0001f9d4\U0001f3fc", + [":person_beard::skin-tone-2:"] = "\U0001f9d4\U0001f3fc", + [":bearded_person_tone3:"] = "\U0001f9d4\U0001f3fd", + [":bearded_person_medium_skin_tone:"] = "\U0001f9d4\U0001f3fd", + [":bearded_person::skin-tone-3:"] = "\U0001f9d4\U0001f3fd", + [":person_beard::skin-tone-3:"] = "\U0001f9d4\U0001f3fd", + [":bearded_person_tone4:"] = "\U0001f9d4\U0001f3fe", + [":bearded_person_medium_dark_skin_tone:"] = "\U0001f9d4\U0001f3fe", + [":bearded_person::skin-tone-4:"] = "\U0001f9d4\U0001f3fe", + [":person_beard::skin-tone-4:"] = "\U0001f9d4\U0001f3fe", + [":bearded_person_tone5:"] = "\U0001f9d4\U0001f3ff", + [":bearded_person_dark_skin_tone:"] = "\U0001f9d4\U0001f3ff", + [":bearded_person::skin-tone-5:"] = "\U0001f9d4\U0001f3ff", + [":person_beard::skin-tone-5:"] = "\U0001f9d4\U0001f3ff", + [":beaver:"] = "\U0001f9ab", + [":bed:"] = "\U0001f6cf\ufe0f", + [":bee:"] = "\U0001f41d", + [":honeybee:"] = "\U0001f41d", + [":beer:"] = "\U0001f37a", + [":beer_mug:"] = "\U0001f37a", + [":beers:"] = "\U0001f37b", + [":beetle:"] = "\U0001fab2", + [":beginner:"] = "\U0001f530", + [":bell:"] = "\U0001f514", + [":bell_pepper:"] = "\U0001fad1", + [":bellhop:"] = "\U0001f6ce\ufe0f", + [":bellhop_bell:"] = "\U0001f6ce\ufe0f", + [":bento:"] = "\U0001f371", + [":bento_box:"] = "\U0001f371", + [":beverage_box:"] = "\U0001f9c3", + [":bike:"] = "\U0001f6b2", + [":bicycle:"] = "\U0001f6b2", + [":bikini:"] = "\U0001f459", + [":billed_cap:"] = "\U0001f9e2", + [":biohazard:"] = "\u2623\ufe0f", + [":biohazard_sign:"] = "\u2623\ufe0f", + [":bird:"] = "\U0001f426", + [":birthday:"] = "\U0001f382", + [":birthday_cake:"] = "\U0001f382", + [":bison:"] = "\U0001f9ac", + [":biting_lip:"] = "\U0001fae6", + [":black_bird:"] = "\U0001f426\u200d\u2b1b", + [":black_cat:"] = "\U0001f408\u200d\u2b1b", + [":black_circle:"] = "\u26ab", + [":black_heart:"] = "\U0001f5a4", + [":black_joker:"] = "\U0001f0cf", + [":joker:"] = "\U0001f0cf", + [":black_large_square:"] = "\u2b1b", + [":black_medium_small_square:"] = "\u25fe", + [":black_medium_square:"] = "\u25fc\ufe0f", + [":black_nib:"] = "\u2712\ufe0f", + [":black_small_square:"] = "\u25aa\ufe0f", + [":black_square_button:"] = "\U0001f532", + [":blond_haired_man:"] = "\U0001f471\u200d\u2642\ufe0f", + [":blond_haired_man_tone1:"] = "\U0001f471\U0001f3fb\u200d\u2642\ufe0f", + [":blond_haired_man_light_skin_tone:"] = "\U0001f471\U0001f3fb\u200d\u2642\ufe0f", + [":blond_haired_man::skin-tone-1:"] = "\U0001f471\U0001f3fb\u200d\u2642\ufe0f", + [":blond_haired_man_tone2:"] = "\U0001f471\U0001f3fc\u200d\u2642\ufe0f", + [":blond_haired_man_medium_light_skin_tone:"] = "\U0001f471\U0001f3fc\u200d\u2642\ufe0f", + [":blond_haired_man::skin-tone-2:"] = "\U0001f471\U0001f3fc\u200d\u2642\ufe0f", + [":blond_haired_man_tone3:"] = "\U0001f471\U0001f3fd\u200d\u2642\ufe0f", + [":blond_haired_man_medium_skin_tone:"] = "\U0001f471\U0001f3fd\u200d\u2642\ufe0f", + [":blond_haired_man::skin-tone-3:"] = "\U0001f471\U0001f3fd\u200d\u2642\ufe0f", + [":blond_haired_man_tone4:"] = "\U0001f471\U0001f3fe\u200d\u2642\ufe0f", + [":blond_haired_man_medium_dark_skin_tone:"] = "\U0001f471\U0001f3fe\u200d\u2642\ufe0f", + [":blond_haired_man::skin-tone-4:"] = "\U0001f471\U0001f3fe\u200d\u2642\ufe0f", + [":blond_haired_man_tone5:"] = "\U0001f471\U0001f3ff\u200d\u2642\ufe0f", + [":blond_haired_man_dark_skin_tone:"] = "\U0001f471\U0001f3ff\u200d\u2642\ufe0f", + [":blond_haired_man::skin-tone-5:"] = "\U0001f471\U0001f3ff\u200d\u2642\ufe0f", + [":blond_haired_person:"] = "\U0001f471", + [":person_with_blond_hair:"] = "\U0001f471", + [":blond_haired_person_tone1:"] = "\U0001f471\U0001f3fb", + [":person_with_blond_hair_tone1:"] = "\U0001f471\U0001f3fb", + [":blond_haired_person::skin-tone-1:"] = "\U0001f471\U0001f3fb", + [":person_with_blond_hair::skin-tone-1:"] = "\U0001f471\U0001f3fb", + [":blond_haired_person_tone2:"] = "\U0001f471\U0001f3fc", + [":person_with_blond_hair_tone2:"] = "\U0001f471\U0001f3fc", + [":blond_haired_person::skin-tone-2:"] = "\U0001f471\U0001f3fc", + [":person_with_blond_hair::skin-tone-2:"] = "\U0001f471\U0001f3fc", + [":blond_haired_person_tone3:"] = "\U0001f471\U0001f3fd", + [":person_with_blond_hair_tone3:"] = "\U0001f471\U0001f3fd", + [":blond_haired_person::skin-tone-3:"] = "\U0001f471\U0001f3fd", + [":person_with_blond_hair::skin-tone-3:"] = "\U0001f471\U0001f3fd", + [":blond_haired_person_tone4:"] = "\U0001f471\U0001f3fe", + [":person_with_blond_hair_tone4:"] = "\U0001f471\U0001f3fe", + [":blond_haired_person::skin-tone-4:"] = "\U0001f471\U0001f3fe", + [":person_with_blond_hair::skin-tone-4:"] = "\U0001f471\U0001f3fe", + [":blond_haired_person_tone5:"] = "\U0001f471\U0001f3ff", + [":person_with_blond_hair_tone5:"] = "\U0001f471\U0001f3ff", + [":blond_haired_person::skin-tone-5:"] = "\U0001f471\U0001f3ff", + [":person_with_blond_hair::skin-tone-5:"] = "\U0001f471\U0001f3ff", + [":blond_haired_woman:"] = "\U0001f471\u200d\u2640\ufe0f", + [":blond_haired_woman_tone1:"] = "\U0001f471\U0001f3fb\u200d\u2640\ufe0f", + [":blond_haired_woman_light_skin_tone:"] = "\U0001f471\U0001f3fb\u200d\u2640\ufe0f", + [":blond_haired_woman::skin-tone-1:"] = "\U0001f471\U0001f3fb\u200d\u2640\ufe0f", + [":blond_haired_woman_tone2:"] = "\U0001f471\U0001f3fc\u200d\u2640\ufe0f", + [":blond_haired_woman_medium_light_skin_tone:"] = "\U0001f471\U0001f3fc\u200d\u2640\ufe0f", + [":blond_haired_woman::skin-tone-2:"] = "\U0001f471\U0001f3fc\u200d\u2640\ufe0f", + [":blond_haired_woman_tone3:"] = "\U0001f471\U0001f3fd\u200d\u2640\ufe0f", + [":blond_haired_woman_medium_skin_tone:"] = "\U0001f471\U0001f3fd\u200d\u2640\ufe0f", + [":blond_haired_woman::skin-tone-3:"] = "\U0001f471\U0001f3fd\u200d\u2640\ufe0f", + [":blond_haired_woman_tone4:"] = "\U0001f471\U0001f3fe\u200d\u2640\ufe0f", + [":blond_haired_woman_medium_dark_skin_tone:"] = "\U0001f471\U0001f3fe\u200d\u2640\ufe0f", + [":blond_haired_woman::skin-tone-4:"] = "\U0001f471\U0001f3fe\u200d\u2640\ufe0f", + [":blond_haired_woman_tone5:"] = "\U0001f471\U0001f3ff\u200d\u2640\ufe0f", + [":blond_haired_woman_dark_skin_tone:"] = "\U0001f471\U0001f3ff\u200d\u2640\ufe0f", + [":blond_haired_woman::skin-tone-5:"] = "\U0001f471\U0001f3ff\u200d\u2640\ufe0f", + [":blossom:"] = "\U0001f33c", + [":blowfish:"] = "\U0001f421", + [":blue_book:"] = "\U0001f4d8", + [":blue_car:"] = "\U0001f699", + [":blue_circle:"] = "\U0001f535", + [":blue_heart:"] = "\U0001f499", + [":blue_square:"] = "\U0001f7e6", + [":blueberries:"] = "\U0001fad0", + [":blush:"] = "\U0001f60a", + [":\")"] = "\U0001f60a", + [":-\")"] = "\U0001f60a", + ["=\")"] = "\U0001f60a", + ["=-\")"] = "\U0001f60a", + [":boar:"] = "\U0001f417", + [":bomb:"] = "\U0001f4a3", + [":bone:"] = "\U0001f9b4", + [":book:"] = "\U0001f4d6", + [":open_book:"] = "\U0001f4d6", + [":bookmark:"] = "\U0001f516", + [":bookmark_tabs:"] = "\U0001f4d1", + [":books:"] = "\U0001f4da", + [":boom:"] = "\U0001f4a5", + [":collision:"] = "\U0001f4a5", + [":boomerang:"] = "\U0001fa83", + [":boot:"] = "\U0001f462", + [":womans_boot:"] = "\U0001f462", + [":bouquet:"] = "\U0001f490", + [":bow_and_arrow:"] = "\U0001f3f9", + [":archery:"] = "\U0001f3f9", + [":bowl_with_spoon:"] = "\U0001f963", + [":bowling:"] = "\U0001f3b3", + [":boxing_glove:"] = "\U0001f94a", + [":boxing_gloves:"] = "\U0001f94a", + [":boy:"] = "\U0001f466", + [":boy_tone1:"] = "\U0001f466\U0001f3fb", + [":boy::skin-tone-1:"] = "\U0001f466\U0001f3fb", + [":boy_tone2:"] = "\U0001f466\U0001f3fc", + [":boy::skin-tone-2:"] = "\U0001f466\U0001f3fc", + [":boy_tone3:"] = "\U0001f466\U0001f3fd", + [":boy::skin-tone-3:"] = "\U0001f466\U0001f3fd", + [":boy_tone4:"] = "\U0001f466\U0001f3fe", + [":boy::skin-tone-4:"] = "\U0001f466\U0001f3fe", + [":boy_tone5:"] = "\U0001f466\U0001f3ff", + [":boy::skin-tone-5:"] = "\U0001f466\U0001f3ff", + [":brain:"] = "\U0001f9e0", + [":bread:"] = "\U0001f35e", + [":breast_feeding:"] = "\U0001f931", + [":breast_feeding_tone1:"] = "\U0001f931\U0001f3fb", + [":breast_feeding_light_skin_tone:"] = "\U0001f931\U0001f3fb", + [":breast_feeding::skin-tone-1:"] = "\U0001f931\U0001f3fb", + [":breast_feeding_tone2:"] = "\U0001f931\U0001f3fc", + [":breast_feeding_medium_light_skin_tone:"] = "\U0001f931\U0001f3fc", + [":breast_feeding::skin-tone-2:"] = "\U0001f931\U0001f3fc", + [":breast_feeding_tone3:"] = "\U0001f931\U0001f3fd", + [":breast_feeding_medium_skin_tone:"] = "\U0001f931\U0001f3fd", + [":breast_feeding::skin-tone-3:"] = "\U0001f931\U0001f3fd", + [":breast_feeding_tone4:"] = "\U0001f931\U0001f3fe", + [":breast_feeding_medium_dark_skin_tone:"] = "\U0001f931\U0001f3fe", + [":breast_feeding::skin-tone-4:"] = "\U0001f931\U0001f3fe", + [":breast_feeding_tone5:"] = "\U0001f931\U0001f3ff", + [":breast_feeding_dark_skin_tone:"] = "\U0001f931\U0001f3ff", + [":breast_feeding::skin-tone-5:"] = "\U0001f931\U0001f3ff", + [":bricks:"] = "\U0001f9f1", + [":brick:"] = "\U0001f9f1", + [":bridge_at_night:"] = "\U0001f309", + [":briefcase:"] = "\U0001f4bc", + [":briefs:"] = "\U0001fa72", + [":broccoli:"] = "\U0001f966", + [":broken_chain:"] = "\u26d3\ufe0f\u200d\U0001f4a5", + [":broken_heart:"] = "\U0001f494", + [" + { + ["\U0001f4af"] = ":100:", + ["\U0001f522"] = ":1234:", + ["\U0001f3b1"] = ":8ball:", + ["\U0001f170\ufe0f"] = ":a:", + ["\U0001f170"] = ":a:", + ["\U0001f18e"] = ":ab:", + ["\U0001f9ee"] = ":abacus:", + ["\U0001f524"] = ":abc:", + ["\U0001f521"] = ":abcd:", + ["\U0001f251"] = ":accept:", + ["\U0001fa97"] = ":accordion:", + ["\U0001fa79"] = ":adhesive_bandage:", + ["\U0001f9d1\U0001f3fb"] = ":adult_tone1:", + ["\U0001f9d1\U0001f3fc"] = ":adult_tone2:", + ["\U0001f9d1\U0001f3fd"] = ":adult_tone3:", + ["\U0001f9d1\U0001f3fe"] = ":adult_tone4:", + ["\U0001f9d1\U0001f3ff"] = ":adult_tone5:", + ["\U0001f9d1"] = ":adult:", + ["\U0001f6a1"] = ":aerial_tramway:", + ["\U0001f6ec"] = ":airplane_arriving:", + ["\U0001f6eb"] = ":airplane_departure:", + ["\U0001f6e9\ufe0f"] = ":airplane_small:", + ["\U0001f6e9"] = ":airplane_small:", + ["\u2708\ufe0f"] = ":airplane:", + ["\u2708"] = ":airplane:", + ["\u23f0"] = ":alarm_clock:", + ["\u2697\ufe0f"] = ":alembic:", + ["\u2697"] = ":alembic:", + ["\U0001f47d"] = ":alien:", + ["\U0001f691"] = ":ambulance:", + ["\U0001f3fa"] = ":amphora:", + ["\U0001fac0"] = ":anatomical_heart:", + ["\u2693"] = ":anchor:", + ["\U0001f47c\U0001f3fb"] = ":angel_tone1:", + ["\U0001f47c\U0001f3fc"] = ":angel_tone2:", + ["\U0001f47c\U0001f3fd"] = ":angel_tone3:", + ["\U0001f47c\U0001f3fe"] = ":angel_tone4:", + ["\U0001f47c\U0001f3ff"] = ":angel_tone5:", + ["\U0001f47c"] = ":angel:", + ["\U0001f5ef\ufe0f"] = ":anger_right:", + ["\U0001f5ef"] = ":anger_right:", + ["\U0001f4a2"] = ":anger:", + ["\U0001f620"] = ":angry:", + ["\U0001f627"] = ":anguished:", + ["\U0001f41c"] = ":ant:", + ["\U0001f34e"] = ":apple:", + ["\u2652"] = ":aquarius:", + ["\u2648"] = ":aries:", + ["\u25c0\ufe0f"] = ":arrow_backward:", + ["\u25c0"] = ":arrow_backward:", + ["\u23ec"] = ":arrow_double_down:", + ["\u23eb"] = ":arrow_double_up:", + ["\U0001f53d"] = ":arrow_down_small:", + ["\u2b07\ufe0f"] = ":arrow_down:", + ["\u2b07"] = ":arrow_down:", + ["\u25b6\ufe0f"] = ":arrow_forward:", + ["\u25b6"] = ":arrow_forward:", + ["\u2935\ufe0f"] = ":arrow_heading_down:", + ["\u2935"] = ":arrow_heading_down:", + ["\u2934\ufe0f"] = ":arrow_heading_up:", + ["\u2934"] = ":arrow_heading_up:", + ["\u2b05\ufe0f"] = ":arrow_left:", + ["\u2b05"] = ":arrow_left:", + ["\u2199\ufe0f"] = ":arrow_lower_left:", + ["\u2199"] = ":arrow_lower_left:", + ["\u2198\ufe0f"] = ":arrow_lower_right:", + ["\u2198"] = ":arrow_lower_right:", + ["\u21aa\ufe0f"] = ":arrow_right_hook:", + ["\u21aa"] = ":arrow_right_hook:", + ["\u27a1\ufe0f"] = ":arrow_right:", + ["\u27a1"] = ":arrow_right:", + ["\u2195\ufe0f"] = ":arrow_up_down:", + ["\u2195"] = ":arrow_up_down:", + ["\U0001f53c"] = ":arrow_up_small:", + ["\u2b06\ufe0f"] = ":arrow_up:", + ["\u2b06"] = ":arrow_up:", + ["\u2196\ufe0f"] = ":arrow_upper_left:", + ["\u2196"] = ":arrow_upper_left:", + ["\u2197\ufe0f"] = ":arrow_upper_right:", + ["\u2197"] = ":arrow_upper_right:", + ["\U0001f503"] = ":arrows_clockwise:", + ["\U0001f504"] = ":arrows_counterclockwise:", + ["\U0001f3a8"] = ":art:", + ["\U0001f69b"] = ":articulated_lorry:", + ["\U0001f9d1\U0001f3fb\u200d\U0001f3a8"] = ":artist_tone1:", + ["\U0001f9d1\U0001f3fc\u200d\U0001f3a8"] = ":artist_tone2:", + ["\U0001f9d1\U0001f3fd\u200d\U0001f3a8"] = ":artist_tone3:", + ["\U0001f9d1\U0001f3fe\u200d\U0001f3a8"] = ":artist_tone4:", + ["\U0001f9d1\U0001f3ff\u200d\U0001f3a8"] = ":artist_tone5:", + ["\U0001f9d1\u200d\U0001f3a8"] = ":artist:", + ["\u002a\ufe0f\u20e3"] = ":asterisk:", + ["\u002a\u20e3"] = ":asterisk:", + ["\U0001f632"] = ":astonished:", + ["\U0001f9d1\U0001f3fb\u200d\U0001f680"] = ":astronaut_tone1:", + ["\U0001f9d1\U0001f3fc\u200d\U0001f680"] = ":astronaut_tone2:", + ["\U0001f9d1\U0001f3fd\u200d\U0001f680"] = ":astronaut_tone3:", + ["\U0001f9d1\U0001f3fe\u200d\U0001f680"] = ":astronaut_tone4:", + ["\U0001f9d1\U0001f3ff\u200d\U0001f680"] = ":astronaut_tone5:", + ["\U0001f9d1\u200d\U0001f680"] = ":astronaut:", + ["\U0001f45f"] = ":athletic_shoe:", + ["\U0001f3e7"] = ":atm:", + ["\u269b\ufe0f"] = ":atom:", + ["\u269b"] = ":atom:", + ["\U0001f6fa"] = ":auto_rickshaw:", + ["\U0001f951"] = ":avocado:", + ["\U0001fa93"] = ":axe:", + ["\U0001f171\ufe0f"] = ":b:", + ["\U0001f171"] = ":b:", + ["\U0001f37c"] = ":baby_bottle:", + ["\U0001f424"] = ":baby_chick:", + ["\U0001f6bc"] = ":baby_symbol:", + ["\U0001f476\U0001f3fb"] = ":baby_tone1:", + ["\U0001f476\U0001f3fc"] = ":baby_tone2:", + ["\U0001f476\U0001f3fd"] = ":baby_tone3:", + ["\U0001f476\U0001f3fe"] = ":baby_tone4:", + ["\U0001f476\U0001f3ff"] = ":baby_tone5:", + ["\U0001f476"] = ":baby:", + ["\U0001f519"] = ":back:", + ["\U0001f953"] = ":bacon:", + ["\U0001f9a1"] = ":badger:", + ["\U0001f3f8"] = ":badminton:", + ["\U0001f96f"] = ":bagel:", + ["\U0001f6c4"] = ":baggage_claim:", + ["\U0001fa70"] = ":ballet_shoes:", + ["\U0001f388"] = ":balloon:", + ["\u2611\ufe0f"] = ":ballot_box_with_check:", + ["\u2611"] = ":ballot_box_with_check:", + ["\U0001f5f3\ufe0f"] = ":ballot_box:", + ["\U0001f5f3"] = ":ballot_box:", + ["\U0001f38d"] = ":bamboo:", + ["\U0001f34c"] = ":banana:", + ["\u203c\ufe0f"] = ":bangbang:", + ["\u203c"] = ":bangbang:", + ["\U0001fa95"] = ":banjo:", + ["\U0001f3e6"] = ":bank:", + ["\U0001f4ca"] = ":bar_chart:", + ["\U0001f488"] = ":barber:", + ["\u26be"] = ":baseball:", + ["\U0001f9fa"] = ":basket:", + ["\U0001f3c0"] = ":basketball:", + ["\U0001f987"] = ":bat:", + ["\U0001f6c0\U0001f3fb"] = ":bath_tone1:", + ["\U0001f6c0\U0001f3fc"] = ":bath_tone2:", + ["\U0001f6c0\U0001f3fd"] = ":bath_tone3:", + ["\U0001f6c0\U0001f3fe"] = ":bath_tone4:", + ["\U0001f6c0\U0001f3ff"] = ":bath_tone5:", + ["\U0001f6c0"] = ":bath:", + ["\U0001f6c1"] = ":bathtub:", + ["\U0001f50b"] = ":battery:", + ["\u26f1\ufe0f"] = ":beach_umbrella:", + ["\u26f1"] = ":beach_umbrella:", + ["\U0001f3d6\ufe0f"] = ":beach:", + ["\U0001f3d6"] = ":beach:", + ["\U0001fad8"] = ":beans:", + ["\U0001f43b"] = ":bear:", + ["\U0001f9d4\U0001f3fb"] = ":bearded_person_tone1:", + ["\U0001f9d4\U0001f3fc"] = ":bearded_person_tone2:", + ["\U0001f9d4\U0001f3fd"] = ":bearded_person_tone3:", + ["\U0001f9d4\U0001f3fe"] = ":bearded_person_tone4:", + ["\U0001f9d4\U0001f3ff"] = ":bearded_person_tone5:", + ["\U0001f9d4"] = ":bearded_person:", + ["\U0001f9ab"] = ":beaver:", + ["\U0001f6cf\ufe0f"] = ":bed:", + ["\U0001f6cf"] = ":bed:", + ["\U0001f41d"] = ":bee:", + ["\U0001f37a"] = ":beer:", + ["\U0001f37b"] = ":beers:", + ["\U0001fab2"] = ":beetle:", + ["\U0001f530"] = ":beginner:", + ["\U0001fad1"] = ":bell_pepper:", + ["\U0001f514"] = ":bell:", + ["\U0001f6ce\ufe0f"] = ":bellhop:", + ["\U0001f6ce"] = ":bellhop:", + ["\U0001f371"] = ":bento:", + ["\U0001f9c3"] = ":beverage_box:", + ["\U0001f6b2"] = ":bike:", + ["\U0001f459"] = ":bikini:", + ["\U0001f9e2"] = ":billed_cap:", + ["\u2623\ufe0f"] = ":biohazard:", + ["\u2623"] = ":biohazard:", + ["\U0001f426"] = ":bird:", + ["\U0001f382"] = ":birthday:", + ["\U0001f9ac"] = ":bison:", + ["\U0001fae6"] = ":biting_lip:", + ["\U0001f426\u200d\u2b1b"] = ":black_bird:", + ["\U0001f408\u200d\u2b1b"] = ":black_cat:", + ["\u26ab"] = ":black_circle:", + ["\U0001f5a4"] = ":black_heart:", + ["\U0001f0cf"] = ":black_joker:", + ["\u2b1b"] = ":black_large_square:", + ["\u25fe"] = ":black_medium_small_square:", + ["\u25fc\ufe0f"] = ":black_medium_square:", + ["\u25fc"] = ":black_medium_square:", + ["\u2712\ufe0f"] = ":black_nib:", + ["\u2712"] = ":black_nib:", + ["\u25aa\ufe0f"] = ":black_small_square:", + ["\u25aa"] = ":black_small_square:", + ["\U0001f532"] = ":black_square_button:", + ["\U0001f471\U0001f3fb\u200d\u2642\ufe0f"] = ":blond_haired_man_tone1:", + ["\U0001f471\U0001f3fc\u200d\u2642\ufe0f"] = ":blond_haired_man_tone2:", + ["\U0001f471\U0001f3fd\u200d\u2642\ufe0f"] = ":blond_haired_man_tone3:", + ["\U0001f471\U0001f3fe\u200d\u2642\ufe0f"] = ":blond_haired_man_tone4:", + ["\U0001f471\U0001f3ff\u200d\u2642\ufe0f"] = ":blond_haired_man_tone5:", + ["\U0001f471\u200d\u2642\ufe0f"] = ":blond_haired_man:", + ["\U0001f471\U0001f3fb"] = ":blond_haired_person_tone1:", + ["\U0001f471\U0001f3fc"] = ":blond_haired_person_tone2:", + ["\U0001f471\U0001f3fd"] = ":blond_haired_person_tone3:", + ["\U0001f471\U0001f3fe"] = ":blond_haired_person_tone4:", + ["\U0001f471\U0001f3ff"] = ":blond_haired_person_tone5:", + ["\U0001f471"] = ":blond_haired_person:", + ["\U0001f471\U0001f3fb\u200d\u2640\ufe0f"] = ":blond_haired_woman_tone1:", + ["\U0001f471\U0001f3fc\u200d\u2640\ufe0f"] = ":blond_haired_woman_tone2:", + ["\U0001f471\U0001f3fd\u200d\u2640\ufe0f"] = ":blond_haired_woman_tone3:", + ["\U0001f471\U0001f3fe\u200d\u2640\ufe0f"] = ":blond_haired_woman_tone4:", + ["\U0001f471\U0001f3ff\u200d\u2640\ufe0f"] = ":blond_haired_woman_tone5:", + ["\U0001f471\u200d\u2640\ufe0f"] = ":blond_haired_woman:", + ["\U0001f33c"] = ":blossom:", + ["\U0001f421"] = ":blowfish:", + ["\U0001f4d8"] = ":blue_book:", + ["\U0001f699"] = ":blue_car:", + ["\U0001f535"] = ":blue_circle:", + ["\U0001f499"] = ":blue_heart:", + ["\U0001f7e6"] = ":blue_square:", + ["\U0001fad0"] = ":blueberries:", + ["\U0001f60a"] = ":blush:", + ["\U0001f417"] = ":boar:", + ["\U0001f4a3"] = ":bomb:", + ["\U0001f9b4"] = ":bone:", + ["\U0001f4d6"] = ":book:", + ["\U0001f4d1"] = ":bookmark_tabs:", + ["\U0001f516"] = ":bookmark:", + ["\U0001f4da"] = ":books:", + ["\U0001f4a5"] = ":boom:", + ["\U0001fa83"] = ":boomerang:", + ["\U0001f462"] = ":boot:", + ["\U0001f490"] = ":bouquet:", + ["\U0001f3f9"] = ":bow_and_arrow:", + ["\U0001f963"] = ":bowl_with_spoon:", + ["\U0001f3b3"] = ":bowling:", + ["\U0001f94a"] = ":boxing_glove:", + ["\U0001f466\U0001f3fb"] = ":boy_tone1:", + ["\U0001f466\U0001f3fc"] = ":boy_tone2:", + ["\U0001f466\U0001f3fd"] = ":boy_tone3:", + ["\U0001f466\U0001f3fe"] = ":boy_tone4:", + ["\U0001f466\U0001f3ff"] = ":boy_tone5:", + ["\U0001f466"] = ":boy:", + ["\U0001f9e0"] = ":brain:", + ["\U0001f35e"] = ":bread:", + ["\U0001f931\U0001f3fb"] = ":breast_feeding_tone1:", + ["\U0001f931\U0001f3fc"] = ":breast_feeding_tone2:", + ["\U0001f931\U0001f3fd"] = ":breast_feeding_tone3:", + ["\U0001f931\U0001f3fe"] = ":breast_feeding_tone4:", + ["\U0001f931\U0001f3ff"] = ":breast_feeding_tone5:", + ["\U0001f931"] = ":breast_feeding:", + ["\U0001f9f1"] = ":bricks:", + ["\U0001f309"] = ":bridge_at_night:", + ["\U0001f4bc"] = ":briefcase:", + ["\U0001fa72"] = ":briefs:", + ["\U0001f966"] = ":broccoli:", + ["\u26d3\ufe0f\u200d\U0001f4a5"] = ":broken_chain:", + ["\U0001f494"] = ":broken_heart:", + ["\U0001f9f9"] = ":broom:", + ["\U0001f7e4"] = ":brown_circle:", + ["\U0001f90e"] = ":brown_heart:", + ["\U0001f344\u200d\U0001f7eb"] = ":brown_mushroom:", + ["\U0001f7eb"] = ":brown_square:", + ["\U0001f9cb"] = ":bubble_tea:", + ["\U0001fae7"] = ":bubbles:", + ["\U0001faa3"] = ":bucket:", + ["\U0001f41b"] = ":bug:", + ["\U0001f4a1"] = ":bulb:", + ["\U0001f685"] = ":bullettrain_front:", + ["\U0001f684"] = ":bullettrain_side:", + ["\U0001f32f"] = ":burrito:", + ["\U0001f68c"] = ":bus:", + ["\U0001f68f"] = ":busstop:", + ["\U0001f464"] = ":bust_in_silhouette:", + ["\U0001f465"] = ":busts_in_silhouette:", + ["\U0001f9c8"] = ":butter:", + ["\U0001f98b"] = ":butterfly:", + ["\U0001f335"] = ":cactus:", + ["\U0001f370"] = ":cake:", + ["\U0001f5d3\ufe0f"] = ":calendar_spiral:", + ["\U0001f5d3"] = ":calendar_spiral:", + ["\U0001f4c6"] = ":calendar:", + ["\U0001f919\U0001f3fb"] = ":call_me_tone1:", + ["\U0001f919\U0001f3fc"] = ":call_me_tone2:", + ["\U0001f919\U0001f3fd"] = ":call_me_tone3:", + ["\U0001f919\U0001f3fe"] = ":call_me_tone4:", + ["\U0001f919\U0001f3ff"] = ":call_me_tone5:", + ["\U0001f919"] = ":call_me:", + ["\U0001f4f2"] = ":calling:", + ["\U0001f42b"] = ":camel:", + ["\U0001f4f8"] = ":camera_with_flash:", + ["\U0001f4f7"] = ":camera:", + ["\U0001f3d5\ufe0f"] = ":camping:", + ["\U0001f3d5"] = ":camping:", + ["\u264b"] = ":cancer:", + ["\U0001f56f\ufe0f"] = ":candle:", + ["\U0001f56f"] = ":candle:", + ["\U0001f36c"] = ":candy:", + ["\U0001f96b"] = ":canned_food:", + ["\U0001f6f6"] = ":canoe:", + ["\U0001f520"] = ":capital_abcd:", + ["\u2651"] = ":capricorn:", + ["\U0001f5c3\ufe0f"] = ":card_box:", + ["\U0001f5c3"] = ":card_box:", + ["\U0001f4c7"] = ":card_index:", + ["\U0001f3a0"] = ":carousel_horse:", + ["\U0001fa9a"] = ":carpentry_saw:", + ["\U0001f955"] = ":carrot:", + ["\U0001f431"] = ":cat:", + ["\U0001f408"] = ":cat2:", + ["\U0001f4bf"] = ":cd:", + ["\u26d3\ufe0f"] = ":chains:", + ["\u26d3"] = ":chains:", + ["\U0001fa91"] = ":chair:", + ["\U0001f942"] = ":champagne_glass:", + ["\U0001f37e"] = ":champagne:", + ["\U0001f4c9"] = ":chart_with_downwards_trend:", + ["\U0001f4c8"] = ":chart_with_upwards_trend:", + ["\U0001f4b9"] = ":chart:", + ["\U0001f3c1"] = ":checkered_flag:", + ["\U0001f9c0"] = ":cheese:", + ["\U0001f352"] = ":cherries:", + ["\U0001f338"] = ":cherry_blossom:", + ["\u265f\ufe0f"] = ":chess_pawn:", + ["\u265f"] = ":chess_pawn:", + ["\U0001f330"] = ":chestnut:", + ["\U0001f414"] = ":chicken:", + ["\U0001f9d2\U0001f3fb"] = ":child_tone1:", + ["\U0001f9d2\U0001f3fc"] = ":child_tone2:", + ["\U0001f9d2\U0001f3fd"] = ":child_tone3:", + ["\U0001f9d2\U0001f3fe"] = ":child_tone4:", + ["\U0001f9d2\U0001f3ff"] = ":child_tone5:", + ["\U0001f9d2"] = ":child:", + ["\U0001f6b8"] = ":children_crossing:", + ["\U0001f43f\ufe0f"] = ":chipmunk:", + ["\U0001f43f"] = ":chipmunk:", + ["\U0001f36b"] = ":chocolate_bar:", + ["\U0001f962"] = ":chopsticks:", + ["\U0001f384"] = ":christmas_tree:", + ["\u26ea"] = ":church:", + ["\U0001f3a6"] = ":cinema:", + ["\U0001f3aa"] = ":circus_tent:", + ["\U0001f306"] = ":city_dusk:", + ["\U0001f307"] = ":city_sunset:", + ["\U0001f3d9\ufe0f"] = ":cityscape:", + ["\U0001f3d9"] = ":cityscape:", + ["\U0001f191"] = ":cl:", + ["\U0001f44f\U0001f3fb"] = ":clap_tone1:", + ["\U0001f44f\U0001f3fc"] = ":clap_tone2:", + ["\U0001f44f\U0001f3fd"] = ":clap_tone3:", + ["\U0001f44f\U0001f3fe"] = ":clap_tone4:", + ["\U0001f44f\U0001f3ff"] = ":clap_tone5:", + ["\U0001f44f"] = ":clap:", + ["\U0001f3ac"] = ":clapper:", + ["\U0001f3db\ufe0f"] = ":classical_building:", + ["\U0001f3db"] = ":classical_building:", + ["\U0001f4cb"] = ":clipboard:", + ["\U0001f570\ufe0f"] = ":clock:", + ["\U0001f570"] = ":clock:", + ["\U0001f550"] = ":clock1:", + ["\U0001f559"] = ":clock10:", + ["\U0001f565"] = ":clock1030:", + ["\U0001f55a"] = ":clock11:", + ["\U0001f566"] = ":clock1130:", + ["\U0001f55b"] = ":clock12:", + ["\U0001f567"] = ":clock1230:", + ["\U0001f55c"] = ":clock130:", + ["\U0001f551"] = ":clock2:", + ["\U0001f55d"] = ":clock230:", + ["\U0001f552"] = ":clock3:", + ["\U0001f55e"] = ":clock330:", + ["\U0001f553"] = ":clock4:", + ["\U0001f55f"] = ":clock430:", + ["\U0001f554"] = ":clock5:", + ["\U0001f560"] = ":clock530:", + ["\U0001f555"] = ":clock6:", + ["\U0001f561"] = ":clock630:", + ["\U0001f556"] = ":clock7:", + ["\U0001f562"] = ":clock730:", + ["\U0001f557"] = ":clock8:", + ["\U0001f563"] = ":clock830:", + ["\U0001f558"] = ":clock9:", + ["\U0001f564"] = ":clock930:", + ["\U0001f4d5"] = ":closed_book:", + ["\U0001f510"] = ":closed_lock_with_key:", + ["\U0001f302"] = ":closed_umbrella:", + ["\U0001f329\ufe0f"] = ":cloud_lightning:", + ["\U0001f329"] = ":cloud_lightning:", + ["\U0001f327\ufe0f"] = ":cloud_rain:", + ["\U0001f327"] = ":cloud_rain:", + ["\U0001f328\ufe0f"] = ":cloud_snow:", + ["\U0001f328"] = ":cloud_snow:", + ["\U0001f32a\ufe0f"] = ":cloud_tornado:", + ["\U0001f32a"] = ":cloud_tornado:", + ["\u2601\ufe0f"] = ":cloud:", + ["\u2601"] = ":cloud:", + ["\U0001f921"] = ":clown:", + ["\u2663\ufe0f"] = ":clubs:", + ["\u2663"] = ":clubs:", + ["\U0001f9e5"] = ":coat:", + ["\U0001fab3"] = ":cockroach:", + ["\U0001f378"] = ":cocktail:", + ["\U0001f965"] = ":coconut:", + ["\u2615"] = ":coffee:", + ["\u26b0\ufe0f"] = ":coffin:", + ["\u26b0"] = ":coffin:", + ["\U0001fa99"] = ":coin:", + ["\U0001f976"] = ":cold_face:", + ["\U0001f630"] = ":cold_sweat:", + ["\u2604\ufe0f"] = ":comet:", + ["\u2604"] = ":comet:", + ["\U0001f9ed"] = ":compass:", + ["\U0001f5dc\ufe0f"] = ":compression:", + ["\U0001f5dc"] = ":compression:", + ["\U0001f4bb"] = ":computer:", + ["\U0001f38a"] = ":confetti_ball:", + ["\U0001f616"] = ":confounded:", + ["\U0001f615"] = ":confused:", + ["\u3297\ufe0f"] = ":congratulations:", + ["\u3297"] = ":congratulations:", + ["\U0001f3d7\ufe0f"] = ":construction_site:", + ["\U0001f3d7"] = ":construction_site:", + ["\U0001f477\U0001f3fb"] = ":construction_worker_tone1:", + ["\U0001f477\U0001f3fc"] = ":construction_worker_tone2:", + ["\U0001f477\U0001f3fd"] = ":construction_worker_tone3:", + ["\U0001f477\U0001f3fe"] = ":construction_worker_tone4:", + ["\U0001f477\U0001f3ff"] = ":construction_worker_tone5:", + ["\U0001f477"] = ":construction_worker:", + ["\U0001f6a7"] = ":construction:", + ["\U0001f39b\ufe0f"] = ":control_knobs:", + ["\U0001f39b"] = ":control_knobs:", + ["\U0001f3ea"] = ":convenience_store:", + ["\U0001f9d1\U0001f3fb\u200d\U0001f373"] = ":cook_tone1:", + ["\U0001f9d1\U0001f3fc\u200d\U0001f373"] = ":cook_tone2:", + ["\U0001f9d1\U0001f3fd\u200d\U0001f373"] = ":cook_tone3:", + ["\U0001f9d1\U0001f3fe\u200d\U0001f373"] = ":cook_tone4:", + ["\U0001f9d1\U0001f3ff\u200d\U0001f373"] = ":cook_tone5:", + ["\U0001f9d1\u200d\U0001f373"] = ":cook:", + ["\U0001f36a"] = ":cookie:", + ["\U0001f373"] = ":cooking:", + ["\U0001f192"] = ":cool:", + ["\u00a9\ufe0f"] = ":copyright:", + ["\u00a9"] = ":copyright:", + ["\U0001fab8"] = ":coral:", + ["\U0001f33d"] = ":corn:", + ["\U0001f6cb\ufe0f"] = ":couch:", + ["\U0001f6cb"] = ":couch:", + ["\U0001f468\u200d\u2764\ufe0f\u200d\U0001f468"] = ":couple_with_heart_man_man_tone5_tone4:", + ["\U0001f491"] = ":couple_with_heart_tone5:", + ["\U0001f469\u200d\u2764\ufe0f\u200d\U0001f468"] = ":couple_with_heart_woman_man_tone5_tone4:", + ["\U0001f469\u200d\u2764\ufe0f\u200d\U0001f469"] = ":couple_ww:", + ["\U0001f42e"] = ":cow:", + ["\U0001f404"] = ":cow2:", + ["\U0001f920"] = ":cowboy:", + ["\U0001f980"] = ":crab:", + ["\U0001f58d\ufe0f"] = ":crayon:", + ["\U0001f58d"] = ":crayon:", + ["\U0001f4b3"] = ":credit_card:", + ["\U0001f319"] = ":crescent_moon:", + ["\U0001f3cf"] = ":cricket_game:", + ["\U0001f997"] = ":cricket:", + ["\U0001f40a"] = ":crocodile:", + ["\U0001f950"] = ":croissant:", + ["\u271d\ufe0f"] = ":cross:", + ["\u271d"] = ":cross:", + ["\U0001f38c"] = ":crossed_flags:", + ["\u2694\ufe0f"] = ":crossed_swords:", + ["\u2694"] = ":crossed_swords:", + ["\U0001f451"] = ":crown:", + ["\U0001f6f3\ufe0f"] = ":cruise_ship:", + ["\U0001f6f3"] = ":cruise_ship:", + ["\U0001fa7c"] = ":crutch:", + ["\U0001f622"] = ":cry:", + ["\U0001f63f"] = ":crying_cat_face:", + ["\U0001f52e"] = ":crystal_ball:", + ["\U0001f952"] = ":cucumber:", + ["\U0001f964"] = ":cup_with_straw:", + ["\U0001f9c1"] = ":cupcake:", + ["\U0001f498"] = ":cupid:", + ["\U0001f94c"] = ":curling_stone:", + ["\u27b0"] = ":curly_loop:", + ["\U0001f4b1"] = ":currency_exchange:", + ["\U0001f35b"] = ":curry:", + ["\U0001f36e"] = ":custard:", + ["\U0001f6c3"] = ":customs:", + ["\U0001f969"] = ":cut_of_meat:", + ["\U0001f300"] = ":cyclone:", + ["\U0001f5e1\ufe0f"] = ":dagger:", + ["\U0001f5e1"] = ":dagger:", + ["\U0001f483\U0001f3fb"] = ":dancer_tone1:", + ["\U0001f483\U0001f3fc"] = ":dancer_tone2:", + ["\U0001f483\U0001f3fd"] = ":dancer_tone3:", + ["\U0001f483\U0001f3fe"] = ":dancer_tone4:", + ["\U0001f483\U0001f3ff"] = ":dancer_tone5:", + ["\U0001f483"] = ":dancer:", + ["\U0001f361"] = ":dango:", + ["\U0001f576\ufe0f"] = ":dark_sunglasses:", + ["\U0001f576"] = ":dark_sunglasses:", + ["\U0001f3af"] = ":dart:", + ["\U0001f4a8"] = ":dash:", + ["\U0001f4c5"] = ":date:", + ["\U0001f9cf\U0001f3fb\u200d\u2642\ufe0f"] = ":deaf_man_tone1:", + ["\U0001f9cf\U0001f3fc\u200d\u2642\ufe0f"] = ":deaf_man_tone2:", + ["\U0001f9cf\U0001f3fd\u200d\u2642\ufe0f"] = ":deaf_man_tone3:", + ["\U0001f9cf\U0001f3fe\u200d\u2642\ufe0f"] = ":deaf_man_tone4:", + ["\U0001f9cf\U0001f3ff\u200d\u2642\ufe0f"] = ":deaf_man_tone5:", + ["\U0001f9cf\u200d\u2642\ufe0f"] = ":deaf_man:", + ["\U0001f9cf\U0001f3fb"] = ":deaf_person_tone1:", + ["\U0001f9cf\U0001f3fc"] = ":deaf_person_tone2:", + ["\U0001f9cf\U0001f3fd"] = ":deaf_person_tone3:", + ["\U0001f9cf\U0001f3fe"] = ":deaf_person_tone4:", + ["\U0001f9cf\U0001f3ff"] = ":deaf_person_tone5:", + ["\U0001f9cf"] = ":deaf_person:", + ["\U0001f9cf\U0001f3fb\u200d\u2640\ufe0f"] = ":deaf_woman_tone1:", + ["\U0001f9cf\U0001f3fc\u200d\u2640\ufe0f"] = ":deaf_woman_tone2:", + ["\U0001f9cf\U0001f3fd\u200d\u2640\ufe0f"] = ":deaf_woman_tone3:", + ["\U0001f9cf\U0001f3fe\u200d\u2640\ufe0f"] = ":deaf_woman_tone4:", + ["\U0001f9cf\U0001f3ff\u200d\u2640\ufe0f"] = ":deaf_woman_tone5:", + ["\U0001f9cf\u200d\u2640\ufe0f"] = ":deaf_woman:", + ["\U0001f333"] = ":deciduous_tree:", + ["\U0001f98c"] = ":deer:", + ["\U0001f3ec"] = ":department_store:", + ["\U0001f3dc\ufe0f"] = ":desert:", + ["\U0001f3dc"] = ":desert:", + ["\U0001f5a5\ufe0f"] = ":desktop:", + ["\U0001f5a5"] = ":desktop:", + ["\U0001f575\U0001f3fb"] = ":detective_tone1:", + ["\U0001f575\U0001f3fc"] = ":detective_tone2:", + ["\U0001f575\U0001f3fd"] = ":detective_tone3:", + ["\U0001f575\U0001f3fe"] = ":detective_tone4:", + ["\U0001f575\U0001f3ff"] = ":detective_tone5:", + ["\U0001f575\ufe0f"] = ":detective:", + ["\U0001f575"] = ":detective:", + ["\U0001f4a0"] = ":diamond_shape_with_a_dot_inside:", + ["\u2666\ufe0f"] = ":diamonds:", + ["\u2666"] = ":diamonds:", + ["\U0001f625"] = ":disappointed_relieved:", + ["\U0001f61e"] = ":disappointed:", + ["\U0001f978"] = ":disguised_face:", + ["\U0001f5c2\ufe0f"] = ":dividers:", + ["\U0001f5c2"] = ":dividers:", + ["\U0001f93f"] = ":diving_mask:", + ["\U0001fa94"] = ":diya_lamp:", + ["\U0001f635"] = ":dizzy_face:", + ["\U0001f4ab"] = ":dizzy:", + ["\U0001f9ec"] = ":dna:", + ["\U0001f6af"] = ":do_not_litter:", + ["\U0001f9a4"] = ":dodo:", + ["\U0001f436"] = ":dog:", + ["\U0001f415"] = ":dog2:", + ["\U0001f4b5"] = ":dollar:", + ["\U0001f38e"] = ":dolls:", + ["\U0001f42c"] = ":dolphin:", + ["\U0001facf"] = ":donkey:", + ["\U0001f6aa"] = ":door:", + ["\U0001fae5"] = ":dotted_line_face:", + ["\U0001f369"] = ":doughnut:", + ["\U0001f54a\ufe0f"] = ":dove:", + ["\U0001f54a"] = ":dove:", + ["\U0001f432"] = ":dragon_face:", + ["\U0001f409"] = ":dragon:", + ["\U0001f457"] = ":dress:", + ["\U0001f42a"] = ":dromedary_camel:", + ["\U0001f924"] = ":drooling_face:", + ["\U0001fa78"] = ":drop_of_blood:", + ["\U0001f4a7"] = ":droplet:", + ["\U0001f941"] = ":drum:", + ["\U0001f986"] = ":duck:", + ["\U0001f95f"] = ":dumpling:", + ["\U0001f4c0"] = ":dvd:", + ["\U0001f4e7"] = ":e_mail:", + ["\U0001f985"] = ":eagle:", + ["\U0001f33e"] = ":ear_of_rice:", + ["\U0001f442\U0001f3fb"] = ":ear_tone1:", + ["\U0001f442\U0001f3fc"] = ":ear_tone2:", + ["\U0001f442\U0001f3fd"] = ":ear_tone3:", + ["\U0001f442\U0001f3fe"] = ":ear_tone4:", + ["\U0001f442\U0001f3ff"] = ":ear_tone5:", + ["\U0001f9bb\U0001f3fb"] = ":ear_with_hearing_aid_tone1:", + ["\U0001f9bb\U0001f3fc"] = ":ear_with_hearing_aid_tone2:", + ["\U0001f9bb\U0001f3fd"] = ":ear_with_hearing_aid_tone3:", + ["\U0001f9bb\U0001f3fe"] = ":ear_with_hearing_aid_tone4:", + ["\U0001f9bb\U0001f3ff"] = ":ear_with_hearing_aid_tone5:", + ["\U0001f9bb"] = ":ear_with_hearing_aid:", + ["\U0001f442"] = ":ear:", + ["\U0001f30d"] = ":earth_africa:", + ["\U0001f30e"] = ":earth_americas:", + ["\U0001f30f"] = ":earth_asia:", + ["\U0001f95a"] = ":egg:", + ["\U0001f346"] = ":eggplant:", + ["\u2734\ufe0f"] = ":eight_pointed_black_star:", + ["\u2734"] = ":eight_pointed_black_star:", + ["\u2733\ufe0f"] = ":eight_spoked_asterisk:", + ["\u2733"] = ":eight_spoked_asterisk:", + ["\u0038\ufe0f\u20e3"] = ":eight:", + ["\u0038\u20e3"] = ":eight:", + ["\u23cf\ufe0f"] = ":eject:", + ["\u23cf"] = ":eject:", + ["\U0001f50c"] = ":electric_plug:", + ["\U0001f418"] = ":elephant:", + ["\U0001f6d7"] = ":elevator:", + ["\U0001f9dd\U0001f3fb"] = ":elf_tone1:", + ["\U0001f9dd\U0001f3fc"] = ":elf_tone2:", + ["\U0001f9dd\U0001f3fd"] = ":elf_tone3:", + ["\U0001f9dd\U0001f3fe"] = ":elf_tone4:", + ["\U0001f9dd\U0001f3ff"] = ":elf_tone5:", + ["\U0001f9dd"] = ":elf:", + ["\U0001fab9"] = ":empty_nest:", + ["\U0001f51a"] = ":end:", + ["\U0001f3f4\U000e0067\U000e0062\U000e0065\U000e006e\U000e0067\U000e007f"] = ":england:", + ["\U0001f4e9"] = ":envelope_with_arrow:", + ["\u2709\ufe0f"] = ":envelope:", + ["\u2709"] = ":envelope:", + ["\U0001f4b6"] = ":euro:", + ["\U0001f3f0"] = ":european_castle:", + ["\U0001f3e4"] = ":european_post_office:", + ["\U0001f332"] = ":evergreen_tree:", + ["\u2757"] = ":exclamation:", + ["\U0001f92f"] = ":exploding_head:", + ["\U0001f611"] = ":expressionless:", + ["\U0001f441\u200d\U0001f5e8"] = ":eye_in_speech_bubble:", + ["\U0001f441\ufe0f"] = ":eye:", + ["\U0001f441"] = ":eye:", + ["\U0001f453"] = ":eyeglasses:", + ["\U0001f440"] = ":eyes:", + ["\U0001f62e\u200d\U0001f4a8"] = ":face_exhaling:", + ["\U0001f979"] = ":face_holding_back_tears:", + ["\U0001f636\u200d\U0001f32b\ufe0f"] = ":face_in_clouds:", + ["\U0001f92e"] = ":face_vomiting:", + ["\U0001fae4"] = ":face_with_diagonal_mouth:", + ["\U0001f92d"] = ":face_with_hand_over_mouth:", + ["\U0001f9d0"] = ":face_with_monocle:", + ["\U0001fae2"] = ":face_with_open_eyes_and_hand_over_mouth:", + ["\U0001fae3"] = ":face_with_peeking_eye:", + ["\U0001f928"] = ":face_with_raised_eyebrow:", + ["\U0001f635\u200d\U0001f4ab"] = ":face_with_spiral_eyes:", + ["\U0001f92c"] = ":face_with_symbols_over_mouth:", + ["\U0001f9d1\U0001f3fb\u200d\U0001f3ed"] = ":factory_worker_tone1:", + ["\U0001f9d1\U0001f3fc\u200d\U0001f3ed"] = ":factory_worker_tone2:", + ["\U0001f9d1\U0001f3fd\u200d\U0001f3ed"] = ":factory_worker_tone3:", + ["\U0001f9d1\U0001f3fe\u200d\U0001f3ed"] = ":factory_worker_tone4:", + ["\U0001f9d1\U0001f3ff\u200d\U0001f3ed"] = ":factory_worker_tone5:", + ["\U0001f9d1\u200d\U0001f3ed"] = ":factory_worker:", + ["\U0001f3ed"] = ":factory:", + ["\U0001f9da\U0001f3fb"] = ":fairy_tone1:", + ["\U0001f9da\U0001f3fc"] = ":fairy_tone2:", + ["\U0001f9da\U0001f3fd"] = ":fairy_tone3:", + ["\U0001f9da\U0001f3fe"] = ":fairy_tone4:", + ["\U0001f9da\U0001f3ff"] = ":fairy_tone5:", + ["\U0001f9da"] = ":fairy:", + ["\U0001f9c6"] = ":falafel:", + ["\U0001f342"] = ":fallen_leaf:", + ["\U0001f9d1\u200d\U0001f9d1\u200d\U0001f9d2\u200d\U0001f9d2"] = ":family_adult_adult_child_child:", + ["\U0001f9d1\u200d\U0001f9d1\u200d\U0001f9d2"] = ":family_adult_adult_child:", + ["\U0001f9d1\u200d\U0001f9d2\u200d\U0001f9d2"] = ":family_adult_child_child:", + ["\U0001f9d1\u200d\U0001f9d2"] = ":family_adult_child:", + ["\U0001f468\u200d\U0001f466\u200d\U0001f466"] = ":family_man_boy_boy:", + ["\U0001f468\u200d\U0001f466"] = ":family_man_boy:", + ["\U0001f468\u200d\U0001f467\u200d\U0001f466"] = ":family_man_girl_boy:", + ["\U0001f468\u200d\U0001f467\u200d\U0001f467"] = ":family_man_girl_girl:", + ["\U0001f468\u200d\U0001f467"] = ":family_man_girl:", + ["\U0001f468\u200d\U0001f469\u200d\U0001f466"] = ":family_man_woman_boy:", + ["\U0001f468\u200d\U0001f468\u200d\U0001f466"] = ":family_mmb:", + ["\U0001f468\u200d\U0001f468\u200d\U0001f466\u200d\U0001f466"] = ":family_mmbb:", + ["\U0001f468\u200d\U0001f468\u200d\U0001f467"] = ":family_mmg:", + ["\U0001f468\u200d\U0001f468\u200d\U0001f467\u200d\U0001f466"] = ":family_mmgb:", + ["\U0001f468\u200d\U0001f468\u200d\U0001f467\u200d\U0001f467"] = ":family_mmgg:", + ["\U0001f468\u200d\U0001f469\u200d\U0001f466\u200d\U0001f466"] = ":family_mwbb:", + ["\U0001f468\u200d\U0001f469\u200d\U0001f467"] = ":family_mwg:", + ["\U0001f468\u200d\U0001f469\u200d\U0001f467\u200d\U0001f466"] = ":family_mwgb:", + ["\U0001f468\u200d\U0001f469\u200d\U0001f467\u200d\U0001f467"] = ":family_mwgg:", + ["\U0001f469\u200d\U0001f466\u200d\U0001f466"] = ":family_woman_boy_boy:", + ["\U0001f469\u200d\U0001f466"] = ":family_woman_boy:", + ["\U0001f469\u200d\U0001f467\u200d\U0001f466"] = ":family_woman_girl_boy:", + ["\U0001f469\u200d\U0001f467\u200d\U0001f467"] = ":family_woman_girl_girl:", + ["\U0001f469\u200d\U0001f467"] = ":family_woman_girl:", + ["\U0001f469\u200d\U0001f469\u200d\U0001f466"] = ":family_wwb:", + ["\U0001f469\u200d\U0001f469\u200d\U0001f466\u200d\U0001f466"] = ":family_wwbb:", + ["\U0001f469\u200d\U0001f469\u200d\U0001f467"] = ":family_wwg:", + ["\U0001f469\u200d\U0001f469\u200d\U0001f467\u200d\U0001f466"] = ":family_wwgb:", + ["\U0001f469\u200d\U0001f469\u200d\U0001f467\u200d\U0001f467"] = ":family_wwgg:", + ["\U0001f46a"] = ":family:", + ["\U0001f9d1\U0001f3fb\u200d\U0001f33e"] = ":farmer_tone1:", + ["\U0001f9d1\U0001f3fc\u200d\U0001f33e"] = ":farmer_tone2:", + ["\U0001f9d1\U0001f3fd\u200d\U0001f33e"] = ":farmer_tone3:", + ["\U0001f9d1\U0001f3fe\u200d\U0001f33e"] = ":farmer_tone4:", + ["\U0001f9d1\U0001f3ff\u200d\U0001f33e"] = ":farmer_tone5:", + ["\U0001f9d1\u200d\U0001f33e"] = ":farmer:", + ["\u23e9"] = ":fast_forward:", + ["\U0001f4e0"] = ":fax:", + ["\U0001f628"] = ":fearful:", + ["\U0001fab6"] = ":feather:", + ["\U0001f43e"] = ":feet:", + ["\u2640\ufe0f"] = ":female_sign:", + ["\u2640"] = ":female_sign:", + ["\U0001f3a1"] = ":ferris_wheel:", + ["\u26f4\ufe0f"] = ":ferry:", + ["\u26f4"] = ":ferry:", + ["\U0001f3d1"] = ":field_hockey:", + ["\U0001f5c4\ufe0f"] = ":file_cabinet:", + ["\U0001f5c4"] = ":file_cabinet:", + ["\U0001f4c1"] = ":file_folder:", + ["\U0001f39e\ufe0f"] = ":film_frames:", + ["\U0001f39e"] = ":film_frames:", + ["\U0001f91e\U0001f3fb"] = ":fingers_crossed_tone1:", + ["\U0001f91e\U0001f3fc"] = ":fingers_crossed_tone2:", + ["\U0001f91e\U0001f3fd"] = ":fingers_crossed_tone3:", + ["\U0001f91e\U0001f3fe"] = ":fingers_crossed_tone4:", + ["\U0001f91e\U0001f3ff"] = ":fingers_crossed_tone5:", + ["\U0001f91e"] = ":fingers_crossed:", + ["\U0001f692"] = ":fire_engine:", + ["\U0001f9ef"] = ":fire_extinguisher:", + ["\U0001f525"] = ":fire:", + ["\U0001f9e8"] = ":firecracker:", + ["\U0001f9d1\U0001f3fb\u200d\U0001f692"] = ":firefighter_tone1:", + ["\U0001f9d1\U0001f3fc\u200d\U0001f692"] = ":firefighter_tone2:", + ["\U0001f9d1\U0001f3fd\u200d\U0001f692"] = ":firefighter_tone3:", + ["\U0001f9d1\U0001f3fe\u200d\U0001f692"] = ":firefighter_tone4:", + ["\U0001f9d1\U0001f3ff\u200d\U0001f692"] = ":firefighter_tone5:", + ["\U0001f9d1\u200d\U0001f692"] = ":firefighter:", + ["\U0001f386"] = ":fireworks:", + ["\U0001f947"] = ":first_place:", + ["\U0001f31b"] = ":first_quarter_moon_with_face:", + ["\U0001f313"] = ":first_quarter_moon:", + ["\U0001f365"] = ":fish_cake:", + ["\U0001f41f"] = ":fish:", + ["\U0001f3a3"] = ":fishing_pole_and_fish:", + ["\u270a\U0001f3fb"] = ":fist_tone1:", + ["\u270a\U0001f3fc"] = ":fist_tone2:", + ["\u270a\U0001f3fd"] = ":fist_tone3:", + ["\u270a\U0001f3fe"] = ":fist_tone4:", + ["\u270a\U0001f3ff"] = ":fist_tone5:", + ["\u270a"] = ":fist:", + ["\u0035\ufe0f\u20e3"] = ":five:", + ["\u0035\u20e3"] = ":five:", + ["\U0001f1e6\U0001f1e8"] = ":flag_ac:", + ["\U0001f1e6\U0001f1e9"] = ":flag_ad:", + ["\U0001f1e6\U0001f1ea"] = ":flag_ae:", + ["\U0001f1e6\U0001f1eb"] = ":flag_af:", + ["\U0001f1e6\U0001f1ec"] = ":flag_ag:", + ["\U0001f1e6\U0001f1ee"] = ":flag_ai:", + ["\U0001f1e6\U0001f1f1"] = ":flag_al:", + ["\U0001f1e6\U0001f1f2"] = ":flag_am:", + ["\U0001f1e6\U0001f1f4"] = ":flag_ao:", + ["\U0001f1e6\U0001f1f6"] = ":flag_aq:", + ["\U0001f1e6\U0001f1f7"] = ":flag_ar:", + ["\U0001f1e6\U0001f1f8"] = ":flag_as:", + ["\U0001f1e6\U0001f1f9"] = ":flag_at:", + ["\U0001f1e6\U0001f1fa"] = ":flag_au:", + ["\U0001f1e6\U0001f1fc"] = ":flag_aw:", + ["\U0001f1e6\U0001f1fd"] = ":flag_ax:", + ["\U0001f1e6\U0001f1ff"] = ":flag_az:", + ["\U0001f1e7\U0001f1e6"] = ":flag_ba:", + ["\U0001f1e7\U0001f1e7"] = ":flag_bb:", + ["\U0001f1e7\U0001f1e9"] = ":flag_bd:", + ["\U0001f1e7\U0001f1ea"] = ":flag_be:", + ["\U0001f1e7\U0001f1eb"] = ":flag_bf:", + ["\U0001f1e7\U0001f1ec"] = ":flag_bg:", + ["\U0001f1e7\U0001f1ed"] = ":flag_bh:", + ["\U0001f1e7\U0001f1ee"] = ":flag_bi:", + ["\U0001f1e7\U0001f1ef"] = ":flag_bj:", + ["\U0001f1e7\U0001f1f1"] = ":flag_bl:", + ["\U0001f3f4"] = ":flag_black:", + ["\U0001f1e7\U0001f1f2"] = ":flag_bm:", + ["\U0001f1e7\U0001f1f3"] = ":flag_bn:", + ["\U0001f1e7\U0001f1f4"] = ":flag_bo:", + ["\U0001f1e7\U0001f1f6"] = ":flag_bq:", + ["\U0001f1e7\U0001f1f7"] = ":flag_br:", + ["\U0001f1e7\U0001f1f8"] = ":flag_bs:", + ["\U0001f1e7\U0001f1f9"] = ":flag_bt:", + ["\U0001f1e7\U0001f1fb"] = ":flag_bv:", + ["\U0001f1e7\U0001f1fc"] = ":flag_bw:", + ["\U0001f1e7\U0001f1fe"] = ":flag_by:", + ["\U0001f1e7\U0001f1ff"] = ":flag_bz:", + ["\U0001f1e8\U0001f1e6"] = ":flag_ca:", + ["\U0001f1e8\U0001f1e8"] = ":flag_cc:", + ["\U0001f1e8\U0001f1e9"] = ":flag_cd:", + ["\U0001f1e8\U0001f1eb"] = ":flag_cf:", + ["\U0001f1e8\U0001f1ec"] = ":flag_cg:", + ["\U0001f1e8\U0001f1ed"] = ":flag_ch:", + ["\U0001f1e8\U0001f1ee"] = ":flag_ci:", + ["\U0001f1e8\U0001f1f0"] = ":flag_ck:", + ["\U0001f1e8\U0001f1f1"] = ":flag_cl:", + ["\U0001f1e8\U0001f1f2"] = ":flag_cm:", + ["\U0001f1e8\U0001f1f3"] = ":flag_cn:", + ["\U0001f1e8\U0001f1f4"] = ":flag_co:", + ["\U0001f1e8\U0001f1f5"] = ":flag_cp:", + ["\U0001f1e8\U0001f1f7"] = ":flag_cr:", + ["\U0001f1e8\U0001f1fa"] = ":flag_cu:", + ["\U0001f1e8\U0001f1fb"] = ":flag_cv:", + ["\U0001f1e8\U0001f1fc"] = ":flag_cw:", + ["\U0001f1e8\U0001f1fd"] = ":flag_cx:", + ["\U0001f1e8\U0001f1fe"] = ":flag_cy:", + ["\U0001f1e8\U0001f1ff"] = ":flag_cz:", + ["\U0001f1e9\U0001f1ea"] = ":flag_de:", + ["\U0001f1e9\U0001f1ec"] = ":flag_dg:", + ["\U0001f1e9\U0001f1ef"] = ":flag_dj:", + ["\U0001f1e9\U0001f1f0"] = ":flag_dk:", + ["\U0001f1e9\U0001f1f2"] = ":flag_dm:", + ["\U0001f1e9\U0001f1f4"] = ":flag_do:", + ["\U0001f1e9\U0001f1ff"] = ":flag_dz:", + ["\U0001f1ea\U0001f1e6"] = ":flag_ea:", + ["\U0001f1ea\U0001f1e8"] = ":flag_ec:", + ["\U0001f1ea\U0001f1ea"] = ":flag_ee:", + ["\U0001f1ea\U0001f1ec"] = ":flag_eg:", + ["\U0001f1ea\U0001f1ed"] = ":flag_eh:", + ["\U0001f1ea\U0001f1f7"] = ":flag_er:", + ["\U0001f1ea\U0001f1f8"] = ":flag_es:", + ["\U0001f1ea\U0001f1f9"] = ":flag_et:", + ["\U0001f1ea\U0001f1fa"] = ":flag_eu:", + ["\U0001f1eb\U0001f1ee"] = ":flag_fi:", + ["\U0001f1eb\U0001f1ef"] = ":flag_fj:", + ["\U0001f1eb\U0001f1f0"] = ":flag_fk:", + ["\U0001f1eb\U0001f1f2"] = ":flag_fm:", + ["\U0001f1eb\U0001f1f4"] = ":flag_fo:", + ["\U0001f1eb\U0001f1f7"] = ":flag_fr:", + ["\U0001f1ec\U0001f1e6"] = ":flag_ga:", + ["\U0001f1ec\U0001f1e7"] = ":flag_gb:", + ["\U0001f1ec\U0001f1e9"] = ":flag_gd:", + ["\U0001f1ec\U0001f1ea"] = ":flag_ge:", + ["\U0001f1ec\U0001f1eb"] = ":flag_gf:", + ["\U0001f1ec\U0001f1ec"] = ":flag_gg:", + ["\U0001f1ec\U0001f1ed"] = ":flag_gh:", + ["\U0001f1ec\U0001f1ee"] = ":flag_gi:", + ["\U0001f1ec\U0001f1f1"] = ":flag_gl:", + ["\U0001f1ec\U0001f1f2"] = ":flag_gm:", + ["\U0001f1ec\U0001f1f3"] = ":flag_gn:", + ["\U0001f1ec\U0001f1f5"] = ":flag_gp:", + ["\U0001f1ec\U0001f1f6"] = ":flag_gq:", + ["\U0001f1ec\U0001f1f7"] = ":flag_gr:", + ["\U0001f1ec\U0001f1f8"] = ":flag_gs:", + ["\U0001f1ec\U0001f1f9"] = ":flag_gt:", + ["\U0001f1ec\U0001f1fa"] = ":flag_gu:", + ["\U0001f1ec\U0001f1fc"] = ":flag_gw:", + ["\U0001f1ec\U0001f1fe"] = ":flag_gy:", + ["\U0001f1ed\U0001f1f0"] = ":flag_hk:", + ["\U0001f1ed\U0001f1f2"] = ":flag_hm:", + ["\U0001f1ed\U0001f1f3"] = ":flag_hn:", + ["\U0001f1ed\U0001f1f7"] = ":flag_hr:", + ["\U0001f1ed\U0001f1f9"] = ":flag_ht:", + ["\U0001f1ed\U0001f1fa"] = ":flag_hu:", + ["\U0001f1ee\U0001f1e8"] = ":flag_ic:", + ["\U0001f1ee\U0001f1e9"] = ":flag_id:", + ["\U0001f1ee\U0001f1ea"] = ":flag_ie:", + ["\U0001f1ee\U0001f1f1"] = ":flag_il:", + ["\U0001f1ee\U0001f1f2"] = ":flag_im:", + ["\U0001f1ee\U0001f1f3"] = ":flag_in:", + ["\U0001f1ee\U0001f1f4"] = ":flag_io:", + ["\U0001f1ee\U0001f1f6"] = ":flag_iq:", + ["\U0001f1ee\U0001f1f7"] = ":flag_ir:", + ["\U0001f1ee\U0001f1f8"] = ":flag_is:", + ["\U0001f1ee\U0001f1f9"] = ":flag_it:", + ["\U0001f1ef\U0001f1ea"] = ":flag_je:", + ["\U0001f1ef\U0001f1f2"] = ":flag_jm:", + ["\U0001f1ef\U0001f1f4"] = ":flag_jo:", + ["\U0001f1ef\U0001f1f5"] = ":flag_jp:", + ["\U0001f1f0\U0001f1ea"] = ":flag_ke:", + ["\U0001f1f0\U0001f1ec"] = ":flag_kg:", + ["\U0001f1f0\U0001f1ed"] = ":flag_kh:", + ["\U0001f1f0\U0001f1ee"] = ":flag_ki:", + ["\U0001f1f0\U0001f1f2"] = ":flag_km:", + ["\U0001f1f0\U0001f1f3"] = ":flag_kn:", + ["\U0001f1f0\U0001f1f5"] = ":flag_kp:", + ["\U0001f1f0\U0001f1f7"] = ":flag_kr:", + ["\U0001f1f0\U0001f1fc"] = ":flag_kw:", + ["\U0001f1f0\U0001f1fe"] = ":flag_ky:", + ["\U0001f1f0\U0001f1ff"] = ":flag_kz:", + ["\U0001f1f1\U0001f1e6"] = ":flag_la:", + ["\U0001f1f1\U0001f1e7"] = ":flag_lb:", + ["\U0001f1f1\U0001f1e8"] = ":flag_lc:", + ["\U0001f1f1\U0001f1ee"] = ":flag_li:", + ["\U0001f1f1\U0001f1f0"] = ":flag_lk:", + ["\U0001f1f1\U0001f1f7"] = ":flag_lr:", + ["\U0001f1f1\U0001f1f8"] = ":flag_ls:", + ["\U0001f1f1\U0001f1f9"] = ":flag_lt:", + ["\U0001f1f1\U0001f1fa"] = ":flag_lu:", + ["\U0001f1f1\U0001f1fb"] = ":flag_lv:", + ["\U0001f1f1\U0001f1fe"] = ":flag_ly:", + ["\U0001f1f2\U0001f1e6"] = ":flag_ma:", + ["\U0001f1f2\U0001f1e8"] = ":flag_mc:", + ["\U0001f1f2\U0001f1e9"] = ":flag_md:", + ["\U0001f1f2\U0001f1ea"] = ":flag_me:", + ["\U0001f1f2\U0001f1eb"] = ":flag_mf:", + ["\U0001f1f2\U0001f1ec"] = ":flag_mg:", + ["\U0001f1f2\U0001f1ed"] = ":flag_mh:", + ["\U0001f1f2\U0001f1f0"] = ":flag_mk:", + ["\U0001f1f2\U0001f1f1"] = ":flag_ml:", + ["\U0001f1f2\U0001f1f2"] = ":flag_mm:", + ["\U0001f1f2\U0001f1f3"] = ":flag_mn:", + ["\U0001f1f2\U0001f1f4"] = ":flag_mo:", + ["\U0001f1f2\U0001f1f5"] = ":flag_mp:", + ["\U0001f1f2\U0001f1f6"] = ":flag_mq:", + ["\U0001f1f2\U0001f1f7"] = ":flag_mr:", + ["\U0001f1f2\U0001f1f8"] = ":flag_ms:", + ["\U0001f1f2\U0001f1f9"] = ":flag_mt:", + ["\U0001f1f2\U0001f1fa"] = ":flag_mu:", + ["\U0001f1f2\U0001f1fb"] = ":flag_mv:", + ["\U0001f1f2\U0001f1fc"] = ":flag_mw:", + ["\U0001f1f2\U0001f1fd"] = ":flag_mx:", + ["\U0001f1f2\U0001f1fe"] = ":flag_my:", + ["\U0001f1f2\U0001f1ff"] = ":flag_mz:", + ["\U0001f1f3\U0001f1e6"] = ":flag_na:", + ["\U0001f1f3\U0001f1e8"] = ":flag_nc:", + ["\U0001f1f3\U0001f1ea"] = ":flag_ne:", + ["\U0001f1f3\U0001f1eb"] = ":flag_nf:", + ["\U0001f1f3\U0001f1ec"] = ":flag_ng:", + ["\U0001f1f3\U0001f1ee"] = ":flag_ni:", + ["\U0001f1f3\U0001f1f1"] = ":flag_nl:", + ["\U0001f1f3\U0001f1f4"] = ":flag_no:", + ["\U0001f1f3\U0001f1f5"] = ":flag_np:", + ["\U0001f1f3\U0001f1f7"] = ":flag_nr:", + ["\U0001f1f3\U0001f1fa"] = ":flag_nu:", + ["\U0001f1f3\U0001f1ff"] = ":flag_nz:", + ["\U0001f1f4\U0001f1f2"] = ":flag_om:", + ["\U0001f1f5\U0001f1e6"] = ":flag_pa:", + ["\U0001f1f5\U0001f1ea"] = ":flag_pe:", + ["\U0001f1f5\U0001f1eb"] = ":flag_pf:", + ["\U0001f1f5\U0001f1ec"] = ":flag_pg:", + ["\U0001f1f5\U0001f1ed"] = ":flag_ph:", + ["\U0001f1f5\U0001f1f0"] = ":flag_pk:", + ["\U0001f1f5\U0001f1f1"] = ":flag_pl:", + ["\U0001f1f5\U0001f1f2"] = ":flag_pm:", + ["\U0001f1f5\U0001f1f3"] = ":flag_pn:", + ["\U0001f1f5\U0001f1f7"] = ":flag_pr:", + ["\U0001f1f5\U0001f1f8"] = ":flag_ps:", + ["\U0001f1f5\U0001f1f9"] = ":flag_pt:", + ["\U0001f1f5\U0001f1fc"] = ":flag_pw:", + ["\U0001f1f5\U0001f1fe"] = ":flag_py:", + ["\U0001f1f6\U0001f1e6"] = ":flag_qa:", + ["\U0001f1f7\U0001f1ea"] = ":flag_re:", + ["\U0001f1f7\U0001f1f4"] = ":flag_ro:", + ["\U0001f1f7\U0001f1f8"] = ":flag_rs:", + ["\U0001f1f7\U0001f1fa"] = ":flag_ru:", + ["\U0001f1f7\U0001f1fc"] = ":flag_rw:", + ["\U0001f1f8\U0001f1e6"] = ":flag_sa:", + ["\U0001f1f8\U0001f1e7"] = ":flag_sb:", + ["\U0001f1f8\U0001f1e8"] = ":flag_sc:", + ["\U0001f1f8\U0001f1e9"] = ":flag_sd:", + ["\U0001f1f8\U0001f1ea"] = ":flag_se:", + ["\U0001f1f8\U0001f1ec"] = ":flag_sg:", + ["\U0001f1f8\U0001f1ed"] = ":flag_sh:", + ["\U0001f1f8\U0001f1ee"] = ":flag_si:", + ["\U0001f1f8\U0001f1ef"] = ":flag_sj:", + ["\U0001f1f8\U0001f1f0"] = ":flag_sk:", + ["\U0001f1f8\U0001f1f1"] = ":flag_sl:", + ["\U0001f1f8\U0001f1f2"] = ":flag_sm:", + ["\U0001f1f8\U0001f1f3"] = ":flag_sn:", + ["\U0001f1f8\U0001f1f4"] = ":flag_so:", + ["\U0001f1f8\U0001f1f7"] = ":flag_sr:", + ["\U0001f1f8\U0001f1f8"] = ":flag_ss:", + ["\U0001f1f8\U0001f1f9"] = ":flag_st:", + ["\U0001f1f8\U0001f1fb"] = ":flag_sv:", + ["\U0001f1f8\U0001f1fd"] = ":flag_sx:", + ["\U0001f1f8\U0001f1fe"] = ":flag_sy:", + ["\U0001f1f8\U0001f1ff"] = ":flag_sz:", + ["\U0001f1f9\U0001f1e6"] = ":flag_ta:", + ["\U0001f1f9\U0001f1e8"] = ":flag_tc:", + ["\U0001f1f9\U0001f1e9"] = ":flag_td:", + ["\U0001f1f9\U0001f1eb"] = ":flag_tf:", + ["\U0001f1f9\U0001f1ec"] = ":flag_tg:", + ["\U0001f1f9\U0001f1ed"] = ":flag_th:", + ["\U0001f1f9\U0001f1ef"] = ":flag_tj:", + ["\U0001f1f9\U0001f1f0"] = ":flag_tk:", + ["\U0001f1f9\U0001f1f1"] = ":flag_tl:", + ["\U0001f1f9\U0001f1f2"] = ":flag_tm:", + ["\U0001f1f9\U0001f1f3"] = ":flag_tn:", + ["\U0001f1f9\U0001f1f4"] = ":flag_to:", + ["\U0001f1f9\U0001f1f7"] = ":flag_tr:", + ["\U0001f1f9\U0001f1f9"] = ":flag_tt:", + ["\U0001f1f9\U0001f1fb"] = ":flag_tv:", + ["\U0001f1f9\U0001f1fc"] = ":flag_tw:", + ["\U0001f1f9\U0001f1ff"] = ":flag_tz:", + ["\U0001f1fa\U0001f1e6"] = ":flag_ua:", + ["\U0001f1fa\U0001f1ec"] = ":flag_ug:", + ["\U0001f1fa\U0001f1f2"] = ":flag_um:", + ["\U0001f1fa\U0001f1f8"] = ":flag_us:", + ["\U0001f1fa\U0001f1fe"] = ":flag_uy:", + ["\U0001f1fa\U0001f1ff"] = ":flag_uz:", + ["\U0001f1fb\U0001f1e6"] = ":flag_va:", + ["\U0001f1fb\U0001f1e8"] = ":flag_vc:", + ["\U0001f1fb\U0001f1ea"] = ":flag_ve:", + ["\U0001f1fb\U0001f1ec"] = ":flag_vg:", + ["\U0001f1fb\U0001f1ee"] = ":flag_vi:", + ["\U0001f1fb\U0001f1f3"] = ":flag_vn:", + ["\U0001f1fb\U0001f1fa"] = ":flag_vu:", + ["\U0001f1fc\U0001f1eb"] = ":flag_wf:", + ["\U0001f3f3\ufe0f"] = ":flag_white:", + ["\U0001f3f3"] = ":flag_white:", + ["\U0001f1fc\U0001f1f8"] = ":flag_ws:", + ["\U0001f1fd\U0001f1f0"] = ":flag_xk:", + ["\U0001f1fe\U0001f1ea"] = ":flag_ye:", + ["\U0001f1fe\U0001f1f9"] = ":flag_yt:", + ["\U0001f1ff\U0001f1e6"] = ":flag_za:", + ["\U0001f1ff\U0001f1f2"] = ":flag_zm:", + ["\U0001f1ff\U0001f1fc"] = ":flag_zw:", + ["\U0001f38f"] = ":flags:", + ["\U0001f9a9"] = ":flamingo:", + ["\U0001f526"] = ":flashlight:", + ["\U0001fad3"] = ":flatbread:", + ["\u269c\ufe0f"] = ":fleur_de_lis:", + ["\u269c"] = ":fleur_de_lis:", + ["\U0001f4be"] = ":floppy_disk:", + ["\U0001f3b4"] = ":flower_playing_cards:", + ["\U0001f633"] = ":flushed:", + ["\U0001fa88"] = ":flute:", + ["\U0001fab0"] = ":fly:", + ["\U0001f94f"] = ":flying_disc:", + ["\U0001f6f8"] = ":flying_saucer:", + ["\U0001f32b\ufe0f"] = ":fog:", + ["\U0001f32b"] = ":fog:", + ["\U0001f301"] = ":foggy:", + ["\U0001faad"] = ":folding_hand_fan:", + ["\U0001fad5"] = ":fondue:", + ["\U0001f9b6\U0001f3fb"] = ":foot_tone1:", + ["\U0001f9b6\U0001f3fc"] = ":foot_tone2:", + ["\U0001f9b6\U0001f3fd"] = ":foot_tone3:", + ["\U0001f9b6\U0001f3fe"] = ":foot_tone4:", + ["\U0001f9b6\U0001f3ff"] = ":foot_tone5:", + ["\U0001f9b6"] = ":foot:", + ["\U0001f3c8"] = ":football:", + ["\U0001f463"] = ":footprints:", + ["\U0001f374"] = ":fork_and_knife:", + ["\U0001f37d\ufe0f"] = ":fork_knife_plate:", + ["\U0001f37d"] = ":fork_knife_plate:", + ["\U0001f960"] = ":fortune_cookie:", + ["\u26f2"] = ":fountain:", + ["\U0001f340"] = ":four_leaf_clover:", + ["\u0034\ufe0f\u20e3"] = ":four:", + ["\u0034\u20e3"] = ":four:", + ["\U0001f98a"] = ":fox:", + ["\U0001f5bc\ufe0f"] = ":frame_photo:", + ["\U0001f5bc"] = ":frame_photo:", + ["\U0001f193"] = ":free:", + ["\U0001f956"] = ":french_bread:", + ["\U0001f364"] = ":fried_shrimp:", + ["\U0001f35f"] = ":fries:", + ["\U0001f438"] = ":frog:", + ["\U0001f626"] = ":frowning:", + ["\u2639\ufe0f"] = ":frowning2:", + ["\u2639"] = ":frowning2:", + ["\u26fd"] = ":fuelpump:", + ["\U0001f31d"] = ":full_moon_with_face:", + ["\U0001f315"] = ":full_moon:", + ["\U0001f3b2"] = ":game_die:", + ["\U0001f9c4"] = ":garlic:", + ["\u2699\ufe0f"] = ":gear:", + ["\u2699"] = ":gear:", + ["\U0001f48e"] = ":gem:", + ["\u264a"] = ":gemini:", + ["\U0001f9de"] = ":genie:", + ["\U0001f47b"] = ":ghost:", + ["\U0001f49d"] = ":gift_heart:", + ["\U0001f381"] = ":gift:", + ["\U0001fada"] = ":ginger_root:", + ["\U0001f992"] = ":giraffe:", + ["\U0001f467\U0001f3fb"] = ":girl_tone1:", + ["\U0001f467\U0001f3fc"] = ":girl_tone2:", + ["\U0001f467\U0001f3fd"] = ":girl_tone3:", + ["\U0001f467\U0001f3fe"] = ":girl_tone4:", + ["\U0001f467\U0001f3ff"] = ":girl_tone5:", + ["\U0001f467"] = ":girl:", + ["\U0001f310"] = ":globe_with_meridians:", + ["\U0001f9e4"] = ":gloves:", + ["\U0001f945"] = ":goal:", + ["\U0001f410"] = ":goat:", + ["\U0001f97d"] = ":goggles:", + ["\u26f3"] = ":golf:", + ["\U0001fabf"] = ":goose:", + ["\U0001f98d"] = ":gorilla:", + ["\U0001f347"] = ":grapes:", + ["\U0001f34f"] = ":green_apple:", + ["\U0001f4d7"] = ":green_book:", + ["\U0001f7e2"] = ":green_circle:", + ["\U0001f49a"] = ":green_heart:", + ["\U0001f7e9"] = ":green_square:", + ["\u2755"] = ":grey_exclamation:", + ["\U0001fa76"] = ":grey_heart:", + ["\u2754"] = ":grey_question:", + ["\U0001f62c"] = ":grimacing:", + ["\U0001f601"] = ":grin:", + ["\U0001f600"] = ":grinning:", + ["\U0001f482\U0001f3fb"] = ":guard_tone1:", + ["\U0001f482\U0001f3fc"] = ":guard_tone2:", + ["\U0001f482\U0001f3fd"] = ":guard_tone3:", + ["\U0001f482\U0001f3fe"] = ":guard_tone4:", + ["\U0001f482\U0001f3ff"] = ":guard_tone5:", + ["\U0001f482"] = ":guard:", + ["\U0001f9ae"] = ":guide_dog:", + ["\U0001f3b8"] = ":guitar:", + ["\U0001f52b"] = ":gun:", + ["\U0001faae"] = ":hair_pick:", + ["\U0001f354"] = ":hamburger:", + ["\u2692\ufe0f"] = ":hammer_pick:", + ["\u2692"] = ":hammer_pick:", + ["\U0001f528"] = ":hammer:", + ["\U0001faac"] = ":hamsa:", + ["\U0001f439"] = ":hamster:", + ["\U0001f590\U0001f3fb"] = ":hand_splayed_tone1:", + ["\U0001f590\U0001f3fc"] = ":hand_splayed_tone2:", + ["\U0001f590\U0001f3fd"] = ":hand_splayed_tone3:", + ["\U0001f590\U0001f3fe"] = ":hand_splayed_tone4:", + ["\U0001f590\U0001f3ff"] = ":hand_splayed_tone5:", + ["\U0001f590\ufe0f"] = ":hand_splayed:", + ["\U0001f590"] = ":hand_splayed:", + ["\U0001faf0\U0001f3fb"] = ":hand_with_index_finger_and_thumb_crossed_tone1:", + ["\U0001faf0\U0001f3fc"] = ":hand_with_index_finger_and_thumb_crossed_tone2:", + ["\U0001faf0\U0001f3fd"] = ":hand_with_index_finger_and_thumb_crossed_tone3:", + ["\U0001faf0\U0001f3fe"] = ":hand_with_index_finger_and_thumb_crossed_tone4:", + ["\U0001faf0\U0001f3ff"] = ":hand_with_index_finger_and_thumb_crossed_tone5:", + ["\U0001faf0"] = ":hand_with_index_finger_and_thumb_crossed:", + ["\U0001f45c"] = ":handbag:", + ["\U0001f91d"] = ":handshake_tone5_tone4:", + ["\u0023\ufe0f\u20e3"] = ":hash:", + ["\u0023\u20e3"] = ":hash:", + ["\U0001f425"] = ":hatched_chick:", + ["\U0001f423"] = ":hatching_chick:", + ["\U0001f915"] = ":head_bandage:", + ["\U0001f642\u200d\u2194\ufe0f"] = ":head_shaking_horizontally:", + ["\U0001f642\u200d\u2195\ufe0f"] = ":head_shaking_vertically:", + ["\U0001f3a7"] = ":headphones:", + ["\U0001faa6"] = ":headstone:", + ["\U0001f9d1\U0001f3fb\u200d\u2695\ufe0f"] = ":health_worker_tone1:", + ["\U0001f9d1\U0001f3fc\u200d\u2695\ufe0f"] = ":health_worker_tone2:", + ["\U0001f9d1\U0001f3fd\u200d\u2695\ufe0f"] = ":health_worker_tone3:", + ["\U0001f9d1\U0001f3fe\u200d\u2695\ufe0f"] = ":health_worker_tone4:", + ["\U0001f9d1\U0001f3ff\u200d\u2695\ufe0f"] = ":health_worker_tone5:", + ["\U0001f9d1\u200d\u2695\ufe0f"] = ":health_worker:", + ["\U0001f649"] = ":hear_no_evil:", + ["\U0001f49f"] = ":heart_decoration:", + ["\u2763\ufe0f"] = ":heart_exclamation:", + ["\u2763"] = ":heart_exclamation:", + ["\U0001f63b"] = ":heart_eyes_cat:", + ["\U0001f60d"] = ":heart_eyes:", + ["\U0001faf6\U0001f3fb"] = ":heart_hands_tone1:", + ["\U0001faf6\U0001f3fc"] = ":heart_hands_tone2:", + ["\U0001faf6\U0001f3fd"] = ":heart_hands_tone3:", + ["\U0001faf6\U0001f3fe"] = ":heart_hands_tone4:", + ["\U0001faf6\U0001f3ff"] = ":heart_hands_tone5:", + ["\U0001faf6"] = ":heart_hands:", + ["\u2764\ufe0f\u200d\U0001f525"] = ":heart_on_fire:", + ["\u2764\ufe0f"] = ":heart:", + ["\u2764"] = ":heart:", + ["\U0001f493"] = ":heartbeat:", + ["\U0001f497"] = ":heartpulse:", + ["\u2665\ufe0f"] = ":hearts:", + ["\u2665"] = ":hearts:", + ["\u2714\ufe0f"] = ":heavy_check_mark:", + ["\u2714"] = ":heavy_check_mark:", + ["\u2797"] = ":heavy_division_sign:", + ["\U0001f4b2"] = ":heavy_dollar_sign:", + ["\U0001f7f0"] = ":heavy_equals_sign:", + ["\u2796"] = ":heavy_minus_sign:", + ["\u2716\ufe0f"] = ":heavy_multiplication_x:", + ["\u2716"] = ":heavy_multiplication_x:", + ["\u2795"] = ":heavy_plus_sign:", + ["\U0001f994"] = ":hedgehog:", + ["\U0001f681"] = ":helicopter:", + ["\u26d1\ufe0f"] = ":helmet_with_cross:", + ["\u26d1"] = ":helmet_with_cross:", + ["\U0001f33f"] = ":herb:", + ["\U0001f33a"] = ":hibiscus:", + ["\U0001f506"] = ":high_brightness:", + ["\U0001f460"] = ":high_heel:", + ["\U0001f97e"] = ":hiking_boot:", + ["\U0001f6d5"] = ":hindu_temple:", + ["\U0001f99b"] = ":hippopotamus:", + ["\U0001f3d2"] = ":hockey:", + ["\U0001f573\ufe0f"] = ":hole:", + ["\U0001f573"] = ":hole:", + ["\U0001f3d8\ufe0f"] = ":homes:", + ["\U0001f3d8"] = ":homes:", + ["\U0001f36f"] = ":honey_pot:", + ["\U0001fa9d"] = ":hook:", + ["\U0001f3c7\U0001f3fb"] = ":horse_racing_tone1:", + ["\U0001f3c7\U0001f3fc"] = ":horse_racing_tone2:", + ["\U0001f3c7\U0001f3fd"] = ":horse_racing_tone3:", + ["\U0001f3c7\U0001f3fe"] = ":horse_racing_tone4:", + ["\U0001f3c7\U0001f3ff"] = ":horse_racing_tone5:", + ["\U0001f3c7"] = ":horse_racing:", + ["\U0001f434"] = ":horse:", + ["\U0001f3e5"] = ":hospital:", + ["\U0001f975"] = ":hot_face:", + ["\U0001f336\ufe0f"] = ":hot_pepper:", + ["\U0001f336"] = ":hot_pepper:", + ["\U0001f32d"] = ":hotdog:", + ["\U0001f3e8"] = ":hotel:", + ["\u2668\ufe0f"] = ":hotsprings:", + ["\u2668"] = ":hotsprings:", + ["\u23f3"] = ":hourglass_flowing_sand:", + ["\u231b"] = ":hourglass:", + ["\U0001f3da\ufe0f"] = ":house_abandoned:", + ["\U0001f3da"] = ":house_abandoned:", + ["\U0001f3e1"] = ":house_with_garden:", + ["\U0001f3e0"] = ":house:", + ["\U0001f917"] = ":hugging:", + ["\U0001f62f"] = ":hushed:", + ["\U0001f6d6"] = ":hut:", + ["\U0001fabb"] = ":hyacinth:", + ["\U0001f368"] = ":ice_cream:", + ["\U0001f9ca"] = ":ice_cube:", + ["\u26f8\ufe0f"] = ":ice_skate:", + ["\u26f8"] = ":ice_skate:", + ["\U0001f366"] = ":icecream:", + ["\U0001f194"] = ":id:", + ["\U0001faaa"] = ":identification_card:", + ["\U0001f250"] = ":ideograph_advantage:", + ["\U0001f47f"] = ":imp:", + ["\U0001f4e5"] = ":inbox_tray:", + ["\U0001f4e8"] = ":incoming_envelope:", + ["\U0001faf5\U0001f3fb"] = ":index_pointing_at_the_viewer_tone1:", + ["\U0001faf5\U0001f3fc"] = ":index_pointing_at_the_viewer_tone2:", + ["\U0001faf5\U0001f3fd"] = ":index_pointing_at_the_viewer_tone3:", + ["\U0001faf5\U0001f3fe"] = ":index_pointing_at_the_viewer_tone4:", + ["\U0001faf5\U0001f3ff"] = ":index_pointing_at_the_viewer_tone5:", + ["\U0001faf5"] = ":index_pointing_at_the_viewer:", + ["\u267e\ufe0f"] = ":infinity:", + ["\u267e"] = ":infinity:", + ["\u2139\ufe0f"] = ":information_source:", + ["\u2139"] = ":information_source:", + ["\U0001f607"] = ":innocent:", + ["\u2049\ufe0f"] = ":interrobang:", + ["\u2049"] = ":interrobang:", + ["\U0001f3dd\ufe0f"] = ":island:", + ["\U0001f3dd"] = ":island:", + ["\U0001f3ee"] = ":izakaya_lantern:", + ["\U0001f383"] = ":jack_o_lantern:", + ["\U0001f5fe"] = ":japan:", + ["\U0001f3ef"] = ":japanese_castle:", + ["\U0001f47a"] = ":japanese_goblin:", + ["\U0001f479"] = ":japanese_ogre:", + ["\U0001fad9"] = ":jar:", + ["\U0001f456"] = ":jeans:", + ["\U0001fabc"] = ":jellyfish:", + ["\U0001f9e9"] = ":jigsaw:", + ["\U0001f639"] = ":joy_cat:", + ["\U0001f602"] = ":joy:", + ["\U0001f579\ufe0f"] = ":joystick:", + ["\U0001f579"] = ":joystick:", + ["\U0001f9d1\U0001f3fb\u200d\u2696\ufe0f"] = ":judge_tone1:", + ["\U0001f9d1\U0001f3fc\u200d\u2696\ufe0f"] = ":judge_tone2:", + ["\U0001f9d1\U0001f3fd\u200d\u2696\ufe0f"] = ":judge_tone3:", + ["\U0001f9d1\U0001f3fe\u200d\u2696\ufe0f"] = ":judge_tone4:", + ["\U0001f9d1\U0001f3ff\u200d\u2696\ufe0f"] = ":judge_tone5:", + ["\U0001f9d1\u200d\u2696\ufe0f"] = ":judge:", + ["\U0001f54b"] = ":kaaba:", + ["\U0001f998"] = ":kangaroo:", + ["\U0001f511"] = ":key:", + ["\U0001f5dd\ufe0f"] = ":key2:", + ["\U0001f5dd"] = ":key2:", + ["\u2328\ufe0f"] = ":keyboard:", + ["\u2328"] = ":keyboard:", + ["\U0001f51f"] = ":keycap_ten:", + ["\U0001faaf"] = ":khanda:", + ["\U0001f458"] = ":kimono:", + ["\U0001f468\u200d\u2764\ufe0f\u200d\U0001f48b\u200d\U0001f468"] = ":kiss_mm:", + ["\U0001f48f"] = ":kiss_tone5:", + ["\U0001f469\u200d\u2764\ufe0f\u200d\U0001f48b\u200d\U0001f468"] = ":kiss_woman_man_tone5_tone4:", + ["\U0001f469\u200d\u2764\ufe0f\u200d\U0001f48b\u200d\U0001f469"] = ":kiss_ww:", + ["\U0001f48b"] = ":kiss:", + ["\U0001f63d"] = ":kissing_cat:", + ["\U0001f61a"] = ":kissing_closed_eyes:", + ["\U0001f618"] = ":kissing_heart:", + ["\U0001f619"] = ":kissing_smiling_eyes:", + ["\U0001f617"] = ":kissing:", + ["\U0001fa81"] = ":kite:", + ["\U0001f95d"] = ":kiwi:", + ["\U0001f52a"] = ":knife:", + ["\U0001faa2"] = ":knot:", + ["\U0001f428"] = ":koala:", + ["\U0001f201"] = ":koko:", + ["\U0001f97c"] = ":lab_coat:", + ["\U0001f3f7\ufe0f"] = ":label:", + ["\U0001f3f7"] = ":label:", + ["\U0001f94d"] = ":lacrosse:", + ["\U0001fa9c"] = ":ladder:", + ["\U0001f41e"] = ":lady_beetle:", + ["\U0001f537"] = ":large_blue_diamond:", + ["\U0001f536"] = ":large_orange_diamond:", + ["\U0001f31c"] = ":last_quarter_moon_with_face:", + ["\U0001f317"] = ":last_quarter_moon:", + ["\U0001f606"] = ":laughing:", + ["\U0001f96c"] = ":leafy_green:", + ["\U0001f343"] = ":leaves:", + ["\U0001f4d2"] = ":ledger:", + ["\U0001f91b\U0001f3fb"] = ":left_facing_fist_tone1:", + ["\U0001f91b\U0001f3fc"] = ":left_facing_fist_tone2:", + ["\U0001f91b\U0001f3fd"] = ":left_facing_fist_tone3:", + ["\U0001f91b\U0001f3fe"] = ":left_facing_fist_tone4:", + ["\U0001f91b\U0001f3ff"] = ":left_facing_fist_tone5:", + ["\U0001f91b"] = ":left_facing_fist:", + ["\U0001f6c5"] = ":left_luggage:", + ["\u2194\ufe0f"] = ":left_right_arrow:", + ["\u2194"] = ":left_right_arrow:", + ["\u21a9\ufe0f"] = ":leftwards_arrow_with_hook:", + ["\u21a9"] = ":leftwards_arrow_with_hook:", + ["\U0001faf2\U0001f3fb"] = ":leftwards_hand_tone1:", + ["\U0001faf2\U0001f3fc"] = ":leftwards_hand_tone2:", + ["\U0001faf2\U0001f3fd"] = ":leftwards_hand_tone3:", + ["\U0001faf2\U0001f3fe"] = ":leftwards_hand_tone4:", + ["\U0001faf2\U0001f3ff"] = ":leftwards_hand_tone5:", + ["\U0001faf2"] = ":leftwards_hand:", + ["\U0001faf7\U0001f3fb"] = ":leftwards_pushing_hand_tone1:", + ["\U0001faf7\U0001f3fc"] = ":leftwards_pushing_hand_tone2:", + ["\U0001faf7\U0001f3fd"] = ":leftwards_pushing_hand_tone3:", + ["\U0001faf7\U0001f3fe"] = ":leftwards_pushing_hand_tone4:", + ["\U0001faf7\U0001f3ff"] = ":leftwards_pushing_hand_tone5:", + ["\U0001faf7"] = ":leftwards_pushing_hand:", + ["\U0001f9b5\U0001f3fb"] = ":leg_tone1:", + ["\U0001f9b5\U0001f3fc"] = ":leg_tone2:", + ["\U0001f9b5\U0001f3fd"] = ":leg_tone3:", + ["\U0001f9b5\U0001f3fe"] = ":leg_tone4:", + ["\U0001f9b5\U0001f3ff"] = ":leg_tone5:", + ["\U0001f9b5"] = ":leg:", + ["\U0001f34b"] = ":lemon:", + ["\u264c"] = ":leo:", + ["\U0001f406"] = ":leopard:", + ["\U0001f39a\ufe0f"] = ":level_slider:", + ["\U0001f39a"] = ":level_slider:", + ["\U0001f574\U0001f3fb"] = ":levitate_tone1:", + ["\U0001f574\U0001f3fc"] = ":levitate_tone2:", + ["\U0001f574\U0001f3fd"] = ":levitate_tone3:", + ["\U0001f574\U0001f3fe"] = ":levitate_tone4:", + ["\U0001f574\U0001f3ff"] = ":levitate_tone5:", + ["\U0001f574\ufe0f"] = ":levitate:", + ["\U0001f574"] = ":levitate:", + ["\u264e"] = ":libra:", + ["\U0001fa75"] = ":light_blue_heart:", + ["\U0001f688"] = ":light_rail:", + ["\U0001f34b\u200d\U0001f7e9"] = ":lime:", + ["\U0001f517"] = ":link:", + ["\U0001f981"] = ":lion_face:", + ["\U0001f444"] = ":lips:", + ["\U0001f484"] = ":lipstick:", + ["\U0001f98e"] = ":lizard:", + ["\U0001f999"] = ":llama:", + ["\U0001f99e"] = ":lobster:", + ["\U0001f50f"] = ":lock_with_ink_pen:", + ["\U0001f512"] = ":lock:", + ["\U0001f36d"] = ":lollipop:", + ["\U0001fa98"] = ":long_drum:", + ["\u27bf"] = ":loop:", + ["\U0001fab7"] = ":lotus:", + ["\U0001f50a"] = ":loud_sound:", + ["\U0001f4e2"] = ":loudspeaker:", + ["\U0001f3e9"] = ":love_hotel:", + ["\U0001f48c"] = ":love_letter:", + ["\U0001f91f\U0001f3fb"] = ":love_you_gesture_tone1:", + ["\U0001f91f\U0001f3fc"] = ":love_you_gesture_tone2:", + ["\U0001f91f\U0001f3fd"] = ":love_you_gesture_tone3:", + ["\U0001f91f\U0001f3fe"] = ":love_you_gesture_tone4:", + ["\U0001f91f\U0001f3ff"] = ":love_you_gesture_tone5:", + ["\U0001f91f"] = ":love_you_gesture:", + ["\U0001faab"] = ":low_battery:", + ["\U0001f505"] = ":low_brightness:", + ["\U0001f9f3"] = ":luggage:", + ["\U0001fac1"] = ":lungs:", + ["\U0001f925"] = ":lying_face:", + ["\u24c2\ufe0f"] = ":m:", + ["\u24c2"] = ":m:", + ["\U0001f50e"] = ":mag_right:", + ["\U0001f50d"] = ":mag:", + ["\U0001f9d9\U0001f3fb"] = ":mage_tone1:", + ["\U0001f9d9\U0001f3fc"] = ":mage_tone2:", + ["\U0001f9d9\U0001f3fd"] = ":mage_tone3:", + ["\U0001f9d9\U0001f3fe"] = ":mage_tone4:", + ["\U0001f9d9\U0001f3ff"] = ":mage_tone5:", + ["\U0001f9d9"] = ":mage:", + ["\U0001fa84"] = ":magic_wand:", + ["\U0001f9f2"] = ":magnet:", + ["\U0001f004"] = ":mahjong:", + ["\U0001f4ea"] = ":mailbox_closed:", + ["\U0001f4ec"] = ":mailbox_with_mail:", + ["\U0001f4ed"] = ":mailbox_with_no_mail:", + ["\U0001f4eb"] = ":mailbox:", + ["\u2642\ufe0f"] = ":male_sign:", + ["\u2642"] = ":male_sign:", + ["\U0001f9a3"] = ":mammoth:", + ["\U0001f468\U0001f3fb\u200d\U0001f3a8"] = ":man_artist_tone1:", + ["\U0001f468\U0001f3fc\u200d\U0001f3a8"] = ":man_artist_tone2:", + ["\U0001f468\U0001f3fd\u200d\U0001f3a8"] = ":man_artist_tone3:", + ["\U0001f468\U0001f3fe\u200d\U0001f3a8"] = ":man_artist_tone4:", + ["\U0001f468\U0001f3ff\u200d\U0001f3a8"] = ":man_artist_tone5:", + ["\U0001f468\u200d\U0001f3a8"] = ":man_artist:", + ["\U0001f468\U0001f3fb\u200d\U0001f680"] = ":man_astronaut_tone1:", + ["\U0001f468\U0001f3fc\u200d\U0001f680"] = ":man_astronaut_tone2:", + ["\U0001f468\U0001f3fd\u200d\U0001f680"] = ":man_astronaut_tone3:", + ["\U0001f468\U0001f3fe\u200d\U0001f680"] = ":man_astronaut_tone4:", + ["\U0001f468\U0001f3ff\u200d\U0001f680"] = ":man_astronaut_tone5:", + ["\U0001f468\u200d\U0001f680"] = ":man_astronaut:", + ["\U0001f468\U0001f3fb\u200d\U0001f9b2"] = ":man_bald_tone1:", + ["\U0001f468\U0001f3fc\u200d\U0001f9b2"] = ":man_bald_tone2:", + ["\U0001f468\U0001f3fd\u200d\U0001f9b2"] = ":man_bald_tone3:", + ["\U0001f468\U0001f3fe\u200d\U0001f9b2"] = ":man_bald_tone4:", + ["\U0001f468\U0001f3ff\u200d\U0001f9b2"] = ":man_bald_tone5:", + ["\U0001f468\u200d\U0001f9b2"] = ":man_bald:", + ["\U0001f9d4\u200d\u2642\ufe0f"] = ":man_beard:", + ["\U0001f6b4\U0001f3fb\u200d\u2642\ufe0f"] = ":man_biking_tone1:", + ["\U0001f6b4\U0001f3fc\u200d\u2642\ufe0f"] = ":man_biking_tone2:", + ["\U0001f6b4\U0001f3fd\u200d\u2642\ufe0f"] = ":man_biking_tone3:", + ["\U0001f6b4\U0001f3fe\u200d\u2642\ufe0f"] = ":man_biking_tone4:", + ["\U0001f6b4\U0001f3ff\u200d\u2642\ufe0f"] = ":man_biking_tone5:", + ["\U0001f6b4\u200d\u2642\ufe0f"] = ":man_biking:", + ["\u26f9\U0001f3fb\u200d\u2642\ufe0f"] = ":man_bouncing_ball_tone1:", + ["\u26f9\U0001f3fc\u200d\u2642\ufe0f"] = ":man_bouncing_ball_tone2:", + ["\u26f9\U0001f3fd\u200d\u2642\ufe0f"] = ":man_bouncing_ball_tone3:", + ["\u26f9\U0001f3fe\u200d\u2642\ufe0f"] = ":man_bouncing_ball_tone4:", + ["\u26f9\U0001f3ff\u200d\u2642\ufe0f"] = ":man_bouncing_ball_tone5:", + ["\u26f9\ufe0f\u200d\u2642\ufe0f"] = ":man_bouncing_ball:", + ["\U0001f647\U0001f3fb\u200d\u2642\ufe0f"] = ":man_bowing_tone1:", + ["\U0001f647\U0001f3fc\u200d\u2642\ufe0f"] = ":man_bowing_tone2:", + ["\U0001f647\U0001f3fd\u200d\u2642\ufe0f"] = ":man_bowing_tone3:", + ["\U0001f647\U0001f3fe\u200d\u2642\ufe0f"] = ":man_bowing_tone4:", + ["\U0001f647\U0001f3ff\u200d\u2642\ufe0f"] = ":man_bowing_tone5:", + ["\U0001f647\u200d\u2642\ufe0f"] = ":man_bowing:", + ["\U0001f938\U0001f3fb\u200d\u2642\ufe0f"] = ":man_cartwheeling_tone1:", + ["\U0001f938\U0001f3fc\u200d\u2642\ufe0f"] = ":man_cartwheeling_tone2:", + ["\U0001f938\U0001f3fd\u200d\u2642\ufe0f"] = ":man_cartwheeling_tone3:", + ["\U0001f938\U0001f3fe\u200d\u2642\ufe0f"] = ":man_cartwheeling_tone4:", + ["\U0001f938\U0001f3ff\u200d\u2642\ufe0f"] = ":man_cartwheeling_tone5:", + ["\U0001f938\u200d\u2642\ufe0f"] = ":man_cartwheeling:", + ["\U0001f9d7\U0001f3fb\u200d\u2642\ufe0f"] = ":man_climbing_tone1:", + ["\U0001f9d7\U0001f3fc\u200d\u2642\ufe0f"] = ":man_climbing_tone2:", + ["\U0001f9d7\U0001f3fd\u200d\u2642\ufe0f"] = ":man_climbing_tone3:", + ["\U0001f9d7\U0001f3fe\u200d\u2642\ufe0f"] = ":man_climbing_tone4:", + ["\U0001f9d7\U0001f3ff\u200d\u2642\ufe0f"] = ":man_climbing_tone5:", + ["\U0001f9d7\u200d\u2642\ufe0f"] = ":man_climbing:", + ["\U0001f477\U0001f3fb\u200d\u2642\ufe0f"] = ":man_construction_worker_tone1:", + ["\U0001f477\U0001f3fc\u200d\u2642\ufe0f"] = ":man_construction_worker_tone2:", + ["\U0001f477\U0001f3fd\u200d\u2642\ufe0f"] = ":man_construction_worker_tone3:", + ["\U0001f477\U0001f3fe\u200d\u2642\ufe0f"] = ":man_construction_worker_tone4:", + ["\U0001f477\U0001f3ff\u200d\u2642\ufe0f"] = ":man_construction_worker_tone5:", + ["\U0001f477\u200d\u2642\ufe0f"] = ":man_construction_worker:", + ["\U0001f468\U0001f3fb\u200d\U0001f373"] = ":man_cook_tone1:", + ["\U0001f468\U0001f3fc\u200d\U0001f373"] = ":man_cook_tone2:", + ["\U0001f468\U0001f3fd\u200d\U0001f373"] = ":man_cook_tone3:", + ["\U0001f468\U0001f3fe\u200d\U0001f373"] = ":man_cook_tone4:", + ["\U0001f468\U0001f3ff\u200d\U0001f373"] = ":man_cook_tone5:", + ["\U0001f468\u200d\U0001f373"] = ":man_cook:", + ["\U0001f468\U0001f3fb\u200d\U0001f9b1"] = ":man_curly_haired_tone1:", + ["\U0001f468\U0001f3fc\u200d\U0001f9b1"] = ":man_curly_haired_tone2:", + ["\U0001f468\U0001f3fd\u200d\U0001f9b1"] = ":man_curly_haired_tone3:", + ["\U0001f468\U0001f3fe\u200d\U0001f9b1"] = ":man_curly_haired_tone4:", + ["\U0001f468\U0001f3ff\u200d\U0001f9b1"] = ":man_curly_haired_tone5:", + ["\U0001f468\u200d\U0001f9b1"] = ":man_curly_haired:", + ["\U0001f57a\U0001f3fb"] = ":man_dancing_tone1:", + ["\U0001f57a\U0001f3fc"] = ":man_dancing_tone2:", + ["\U0001f57a\U0001f3fd"] = ":man_dancing_tone3:", + ["\U0001f57a\U0001f3fe"] = ":man_dancing_tone4:", + ["\U0001f57a\U0001f3ff"] = ":man_dancing_tone5:", + ["\U0001f57a"] = ":man_dancing:", + ["\U0001f575\U0001f3fb\u200d\u2642\ufe0f"] = ":man_detective_tone1:", + ["\U0001f575\U0001f3fc\u200d\u2642\ufe0f"] = ":man_detective_tone2:", + ["\U0001f575\U0001f3fd\u200d\u2642\ufe0f"] = ":man_detective_tone3:", + ["\U0001f575\U0001f3fe\u200d\u2642\ufe0f"] = ":man_detective_tone4:", + ["\U0001f575\U0001f3ff\u200d\u2642\ufe0f"] = ":man_detective_tone5:", + ["\U0001f575\ufe0f\u200d\u2642\ufe0f"] = ":man_detective:", + ["\U0001f9dd\U0001f3fb\u200d\u2642\ufe0f"] = ":man_elf_tone1:", + ["\U0001f9dd\U0001f3fc\u200d\u2642\ufe0f"] = ":man_elf_tone2:", + ["\U0001f9dd\U0001f3fd\u200d\u2642\ufe0f"] = ":man_elf_tone3:", + ["\U0001f9dd\U0001f3fe\u200d\u2642\ufe0f"] = ":man_elf_tone4:", + ["\U0001f9dd\U0001f3ff\u200d\u2642\ufe0f"] = ":man_elf_tone5:", + ["\U0001f9dd\u200d\u2642\ufe0f"] = ":man_elf:", + ["\U0001f926\U0001f3fb\u200d\u2642\ufe0f"] = ":man_facepalming_tone1:", + ["\U0001f926\U0001f3fc\u200d\u2642\ufe0f"] = ":man_facepalming_tone2:", + ["\U0001f926\U0001f3fd\u200d\u2642\ufe0f"] = ":man_facepalming_tone3:", + ["\U0001f926\U0001f3fe\u200d\u2642\ufe0f"] = ":man_facepalming_tone4:", + ["\U0001f926\U0001f3ff\u200d\u2642\ufe0f"] = ":man_facepalming_tone5:", + ["\U0001f926\u200d\u2642\ufe0f"] = ":man_facepalming:", + ["\U0001f468\U0001f3fb\u200d\U0001f3ed"] = ":man_factory_worker_tone1:", + ["\U0001f468\U0001f3fc\u200d\U0001f3ed"] = ":man_factory_worker_tone2:", + ["\U0001f468\U0001f3fd\u200d\U0001f3ed"] = ":man_factory_worker_tone3:", + ["\U0001f468\U0001f3fe\u200d\U0001f3ed"] = ":man_factory_worker_tone4:", + ["\U0001f468\U0001f3ff\u200d\U0001f3ed"] = ":man_factory_worker_tone5:", + ["\U0001f468\u200d\U0001f3ed"] = ":man_factory_worker:", + ["\U0001f9da\U0001f3fb\u200d\u2642\ufe0f"] = ":man_fairy_tone1:", + ["\U0001f9da\U0001f3fc\u200d\u2642\ufe0f"] = ":man_fairy_tone2:", + ["\U0001f9da\U0001f3fd\u200d\u2642\ufe0f"] = ":man_fairy_tone3:", + ["\U0001f9da\U0001f3fe\u200d\u2642\ufe0f"] = ":man_fairy_tone4:", + ["\U0001f9da\U0001f3ff\u200d\u2642\ufe0f"] = ":man_fairy_tone5:", + ["\U0001f9da\u200d\u2642\ufe0f"] = ":man_fairy:", + ["\U0001f468\U0001f3fb\u200d\U0001f33e"] = ":man_farmer_tone1:", + ["\U0001f468\U0001f3fc\u200d\U0001f33e"] = ":man_farmer_tone2:", + ["\U0001f468\U0001f3fd\u200d\U0001f33e"] = ":man_farmer_tone3:", + ["\U0001f468\U0001f3fe\u200d\U0001f33e"] = ":man_farmer_tone4:", + ["\U0001f468\U0001f3ff\u200d\U0001f33e"] = ":man_farmer_tone5:", + ["\U0001f468\u200d\U0001f33e"] = ":man_farmer:", + ["\U0001f468\U0001f3fb\u200d\U0001f37c"] = ":man_feeding_baby_tone1:", + ["\U0001f468\U0001f3fc\u200d\U0001f37c"] = ":man_feeding_baby_tone2:", + ["\U0001f468\U0001f3fd\u200d\U0001f37c"] = ":man_feeding_baby_tone3:", + ["\U0001f468\U0001f3fe\u200d\U0001f37c"] = ":man_feeding_baby_tone4:", + ["\U0001f468\U0001f3ff\u200d\U0001f37c"] = ":man_feeding_baby_tone5:", + ["\U0001f468\u200d\U0001f37c"] = ":man_feeding_baby:", + ["\U0001f468\U0001f3fb\u200d\U0001f692"] = ":man_firefighter_tone1:", + ["\U0001f468\U0001f3fc\u200d\U0001f692"] = ":man_firefighter_tone2:", + ["\U0001f468\U0001f3fd\u200d\U0001f692"] = ":man_firefighter_tone3:", + ["\U0001f468\U0001f3fe\u200d\U0001f692"] = ":man_firefighter_tone4:", + ["\U0001f468\U0001f3ff\u200d\U0001f692"] = ":man_firefighter_tone5:", + ["\U0001f468\u200d\U0001f692"] = ":man_firefighter:", + ["\U0001f64d\U0001f3fb\u200d\u2642\ufe0f"] = ":man_frowning_tone1:", + ["\U0001f64d\U0001f3fc\u200d\u2642\ufe0f"] = ":man_frowning_tone2:", + ["\U0001f64d\U0001f3fd\u200d\u2642\ufe0f"] = ":man_frowning_tone3:", + ["\U0001f64d\U0001f3fe\u200d\u2642\ufe0f"] = ":man_frowning_tone4:", + ["\U0001f64d\U0001f3ff\u200d\u2642\ufe0f"] = ":man_frowning_tone5:", + ["\U0001f64d\u200d\u2642\ufe0f"] = ":man_frowning:", + ["\U0001f9de\u200d\u2642\ufe0f"] = ":man_genie:", + ["\U0001f645\U0001f3fb\u200d\u2642\ufe0f"] = ":man_gesturing_no_tone1:", + ["\U0001f645\U0001f3fc\u200d\u2642\ufe0f"] = ":man_gesturing_no_tone2:", + ["\U0001f645\U0001f3fd\u200d\u2642\ufe0f"] = ":man_gesturing_no_tone3:", + ["\U0001f645\U0001f3fe\u200d\u2642\ufe0f"] = ":man_gesturing_no_tone4:", + ["\U0001f645\U0001f3ff\u200d\u2642\ufe0f"] = ":man_gesturing_no_tone5:", + ["\U0001f645\u200d\u2642\ufe0f"] = ":man_gesturing_no:", + ["\U0001f646\U0001f3fb\u200d\u2642\ufe0f"] = ":man_gesturing_ok_tone1:", + ["\U0001f646\U0001f3fc\u200d\u2642\ufe0f"] = ":man_gesturing_ok_tone2:", + ["\U0001f646\U0001f3fd\u200d\u2642\ufe0f"] = ":man_gesturing_ok_tone3:", + ["\U0001f646\U0001f3fe\u200d\u2642\ufe0f"] = ":man_gesturing_ok_tone4:", + ["\U0001f646\U0001f3ff\u200d\u2642\ufe0f"] = ":man_gesturing_ok_tone5:", + ["\U0001f646\u200d\u2642\ufe0f"] = ":man_gesturing_ok:", + ["\U0001f486\U0001f3fb\u200d\u2642\ufe0f"] = ":man_getting_face_massage_tone1:", + ["\U0001f486\U0001f3fc\u200d\u2642\ufe0f"] = ":man_getting_face_massage_tone2:", + ["\U0001f486\U0001f3fd\u200d\u2642\ufe0f"] = ":man_getting_face_massage_tone3:", + ["\U0001f486\U0001f3fe\u200d\u2642\ufe0f"] = ":man_getting_face_massage_tone4:", + ["\U0001f486\U0001f3ff\u200d\u2642\ufe0f"] = ":man_getting_face_massage_tone5:", + ["\U0001f486\u200d\u2642\ufe0f"] = ":man_getting_face_massage:", + ["\U0001f487\U0001f3fb\u200d\u2642\ufe0f"] = ":man_getting_haircut_tone1:", + ["\U0001f487\U0001f3fc\u200d\u2642\ufe0f"] = ":man_getting_haircut_tone2:", + ["\U0001f487\U0001f3fd\u200d\u2642\ufe0f"] = ":man_getting_haircut_tone3:", + ["\U0001f487\U0001f3fe\u200d\u2642\ufe0f"] = ":man_getting_haircut_tone4:", + ["\U0001f487\U0001f3ff\u200d\u2642\ufe0f"] = ":man_getting_haircut_tone5:", + ["\U0001f487\u200d\u2642\ufe0f"] = ":man_getting_haircut:", + ["\U0001f3cc\U0001f3fb\u200d\u2642\ufe0f"] = ":man_golfing_tone1:", + ["\U0001f3cc\U0001f3fc\u200d\u2642\ufe0f"] = ":man_golfing_tone2:", + ["\U0001f3cc\U0001f3fd\u200d\u2642\ufe0f"] = ":man_golfing_tone3:", + ["\U0001f3cc\U0001f3fe\u200d\u2642\ufe0f"] = ":man_golfing_tone4:", + ["\U0001f3cc\U0001f3ff\u200d\u2642\ufe0f"] = ":man_golfing_tone5:", + ["\U0001f3cc\ufe0f\u200d\u2642\ufe0f"] = ":man_golfing:", + ["\U0001f482\U0001f3fb\u200d\u2642\ufe0f"] = ":man_guard_tone1:", + ["\U0001f482\U0001f3fc\u200d\u2642\ufe0f"] = ":man_guard_tone2:", + ["\U0001f482\U0001f3fd\u200d\u2642\ufe0f"] = ":man_guard_tone3:", + ["\U0001f482\U0001f3fe\u200d\u2642\ufe0f"] = ":man_guard_tone4:", + ["\U0001f482\U0001f3ff\u200d\u2642\ufe0f"] = ":man_guard_tone5:", + ["\U0001f482\u200d\u2642\ufe0f"] = ":man_guard:", + ["\U0001f468\U0001f3fb\u200d\u2695\ufe0f"] = ":man_health_worker_tone1:", + ["\U0001f468\U0001f3fc\u200d\u2695\ufe0f"] = ":man_health_worker_tone2:", + ["\U0001f468\U0001f3fd\u200d\u2695\ufe0f"] = ":man_health_worker_tone3:", + ["\U0001f468\U0001f3fe\u200d\u2695\ufe0f"] = ":man_health_worker_tone4:", + ["\U0001f468\U0001f3ff\u200d\u2695\ufe0f"] = ":man_health_worker_tone5:", + ["\U0001f468\u200d\u2695\ufe0f"] = ":man_health_worker:", + ["\U0001f9d8\U0001f3fb\u200d\u2642\ufe0f"] = ":man_in_lotus_position_tone1:", + ["\U0001f9d8\U0001f3fc\u200d\u2642\ufe0f"] = ":man_in_lotus_position_tone2:", + ["\U0001f9d8\U0001f3fd\u200d\u2642\ufe0f"] = ":man_in_lotus_position_tone3:", + ["\U0001f9d8\U0001f3fe\u200d\u2642\ufe0f"] = ":man_in_lotus_position_tone4:", + ["\U0001f9d8\U0001f3ff\u200d\u2642\ufe0f"] = ":man_in_lotus_position_tone5:", + ["\U0001f9d8\u200d\u2642\ufe0f"] = ":man_in_lotus_position:", + ["\U0001f468\U0001f3fb\u200d\U0001f9bd\u200d\u27a1\ufe0f"] = ":man_in_manual_wheelchair_facing_right_tone1:", + ["\U0001f468\U0001f3fc\u200d\U0001f9bd\u200d\u27a1\ufe0f"] = ":man_in_manual_wheelchair_facing_right_tone2:", + ["\U0001f468\U0001f3fd\u200d\U0001f9bd\u200d\u27a1\ufe0f"] = ":man_in_manual_wheelchair_facing_right_tone3:", + ["\U0001f468\U0001f3fe\u200d\U0001f9bd\u200d\u27a1\ufe0f"] = ":man_in_manual_wheelchair_facing_right_tone4:", + ["\U0001f468\U0001f3ff\u200d\U0001f9bd\u200d\u27a1\ufe0f"] = ":man_in_manual_wheelchair_facing_right_tone5:", + ["\U0001f468\u200d\U0001f9bd\u200d\u27a1\ufe0f"] = ":man_in_manual_wheelchair_facing_right:", + ["\U0001f468\U0001f3fb\u200d\U0001f9bd"] = ":man_in_manual_wheelchair_tone1:", + ["\U0001f468\U0001f3fc\u200d\U0001f9bd"] = ":man_in_manual_wheelchair_tone2:", + ["\U0001f468\U0001f3fd\u200d\U0001f9bd"] = ":man_in_manual_wheelchair_tone3:", + ["\U0001f468\U0001f3fe\u200d\U0001f9bd"] = ":man_in_manual_wheelchair_tone4:", + ["\U0001f468\U0001f3ff\u200d\U0001f9bd"] = ":man_in_manual_wheelchair_tone5:", + ["\U0001f468\u200d\U0001f9bd"] = ":man_in_manual_wheelchair:", + ["\U0001f468\U0001f3fb\u200d\U0001f9bc\u200d\u27a1\ufe0f"] = ":man_in_motorized_wheelchair_facing_right_tone1:", + ["\U0001f468\U0001f3fc\u200d\U0001f9bc\u200d\u27a1\ufe0f"] = ":man_in_motorized_wheelchair_facing_right_tone2:", + ["\U0001f468\U0001f3fd\u200d\U0001f9bc\u200d\u27a1\ufe0f"] = ":man_in_motorized_wheelchair_facing_right_tone3:", + ["\U0001f468\U0001f3fe\u200d\U0001f9bc\u200d\u27a1\ufe0f"] = ":man_in_motorized_wheelchair_facing_right_tone4:", + ["\U0001f468\U0001f3ff\u200d\U0001f9bc\u200d\u27a1\ufe0f"] = ":man_in_motorized_wheelchair_facing_right_tone5:", + ["\U0001f468\u200d\U0001f9bc\u200d\u27a1\ufe0f"] = ":man_in_motorized_wheelchair_facing_right:", + ["\U0001f468\U0001f3fb\u200d\U0001f9bc"] = ":man_in_motorized_wheelchair_tone1:", + ["\U0001f468\U0001f3fc\u200d\U0001f9bc"] = ":man_in_motorized_wheelchair_tone2:", + ["\U0001f468\U0001f3fd\u200d\U0001f9bc"] = ":man_in_motorized_wheelchair_tone3:", + ["\U0001f468\U0001f3fe\u200d\U0001f9bc"] = ":man_in_motorized_wheelchair_tone4:", + ["\U0001f468\U0001f3ff\u200d\U0001f9bc"] = ":man_in_motorized_wheelchair_tone5:", + ["\U0001f468\u200d\U0001f9bc"] = ":man_in_motorized_wheelchair:", + ["\U0001f9d6\U0001f3fb\u200d\u2642\ufe0f"] = ":man_in_steamy_room_tone1:", + ["\U0001f9d6\U0001f3fc\u200d\u2642\ufe0f"] = ":man_in_steamy_room_tone2:", + ["\U0001f9d6\U0001f3fd\u200d\u2642\ufe0f"] = ":man_in_steamy_room_tone3:", + ["\U0001f9d6\U0001f3fe\u200d\u2642\ufe0f"] = ":man_in_steamy_room_tone4:", + ["\U0001f9d6\U0001f3ff\u200d\u2642\ufe0f"] = ":man_in_steamy_room_tone5:", + ["\U0001f9d6\u200d\u2642\ufe0f"] = ":man_in_steamy_room:", + ["\U0001f935\U0001f3fb\u200d\u2642\ufe0f"] = ":man_in_tuxedo_tone1:", + ["\U0001f935\U0001f3fc\u200d\u2642\ufe0f"] = ":man_in_tuxedo_tone2:", + ["\U0001f935\U0001f3fd\u200d\u2642\ufe0f"] = ":man_in_tuxedo_tone3:", + ["\U0001f935\U0001f3fe\u200d\u2642\ufe0f"] = ":man_in_tuxedo_tone4:", + ["\U0001f935\U0001f3ff\u200d\u2642\ufe0f"] = ":man_in_tuxedo_tone5:", + ["\U0001f935\u200d\u2642\ufe0f"] = ":man_in_tuxedo:", + ["\U0001f468\U0001f3fb\u200d\u2696\ufe0f"] = ":man_judge_tone1:", + ["\U0001f468\U0001f3fc\u200d\u2696\ufe0f"] = ":man_judge_tone2:", + ["\U0001f468\U0001f3fd\u200d\u2696\ufe0f"] = ":man_judge_tone3:", + ["\U0001f468\U0001f3fe\u200d\u2696\ufe0f"] = ":man_judge_tone4:", + ["\U0001f468\U0001f3ff\u200d\u2696\ufe0f"] = ":man_judge_tone5:", + ["\U0001f468\u200d\u2696\ufe0f"] = ":man_judge:", + ["\U0001f939\U0001f3fb\u200d\u2642\ufe0f"] = ":man_juggling_tone1:", + ["\U0001f939\U0001f3fc\u200d\u2642\ufe0f"] = ":man_juggling_tone2:", + ["\U0001f939\U0001f3fd\u200d\u2642\ufe0f"] = ":man_juggling_tone3:", + ["\U0001f939\U0001f3fe\u200d\u2642\ufe0f"] = ":man_juggling_tone4:", + ["\U0001f939\U0001f3ff\u200d\u2642\ufe0f"] = ":man_juggling_tone5:", + ["\U0001f939\u200d\u2642\ufe0f"] = ":man_juggling:", + ["\U0001f9ce\U0001f3fb\u200d\u2642\ufe0f\u200d\u27a1\ufe0f"] = ":man_kneeling_facing_right_tone1:", + ["\U0001f9ce\U0001f3fc\u200d\u2642\ufe0f\u200d\u27a1\ufe0f"] = ":man_kneeling_facing_right_tone2:", + ["\U0001f9ce\U0001f3fd\u200d\u2642\ufe0f\u200d\u27a1\ufe0f"] = ":man_kneeling_facing_right_tone3:", + ["\U0001f9ce\U0001f3fe\u200d\u2642\ufe0f\u200d\u27a1\ufe0f"] = ":man_kneeling_facing_right_tone4:", + ["\U0001f9ce\U0001f3ff\u200d\u2642\ufe0f\u200d\u27a1\ufe0f"] = ":man_kneeling_facing_right_tone5:", + ["\U0001f9ce\u200d\u2642\ufe0f\u200d\u27a1\ufe0f"] = ":man_kneeling_facing_right:", + ["\U0001f9ce\U0001f3fb\u200d\u2642\ufe0f"] = ":man_kneeling_tone1:", + ["\U0001f9ce\U0001f3fc\u200d\u2642\ufe0f"] = ":man_kneeling_tone2:", + ["\U0001f9ce\U0001f3fd\u200d\u2642\ufe0f"] = ":man_kneeling_tone3:", + ["\U0001f9ce\U0001f3fe\u200d\u2642\ufe0f"] = ":man_kneeling_tone4:", + ["\U0001f9ce\U0001f3ff\u200d\u2642\ufe0f"] = ":man_kneeling_tone5:", + ["\U0001f9ce\u200d\u2642\ufe0f"] = ":man_kneeling:", + ["\U0001f3cb\U0001f3fb\u200d\u2642\ufe0f"] = ":man_lifting_weights_tone1:", + ["\U0001f3cb\U0001f3fc\u200d\u2642\ufe0f"] = ":man_lifting_weights_tone2:", + ["\U0001f3cb\U0001f3fd\u200d\u2642\ufe0f"] = ":man_lifting_weights_tone3:", + ["\U0001f3cb\U0001f3fe\u200d\u2642\ufe0f"] = ":man_lifting_weights_tone4:", + ["\U0001f3cb\U0001f3ff\u200d\u2642\ufe0f"] = ":man_lifting_weights_tone5:", + ["\U0001f3cb\ufe0f\u200d\u2642\ufe0f"] = ":man_lifting_weights:", + ["\U0001f9d9\U0001f3fb\u200d\u2642\ufe0f"] = ":man_mage_tone1:", + ["\U0001f9d9\U0001f3fc\u200d\u2642\ufe0f"] = ":man_mage_tone2:", + ["\U0001f9d9\U0001f3fd\u200d\u2642\ufe0f"] = ":man_mage_tone3:", + ["\U0001f9d9\U0001f3fe\u200d\u2642\ufe0f"] = ":man_mage_tone4:", + ["\U0001f9d9\U0001f3ff\u200d\u2642\ufe0f"] = ":man_mage_tone5:", + ["\U0001f9d9\u200d\u2642\ufe0f"] = ":man_mage:", + ["\U0001f468\U0001f3fb\u200d\U0001f527"] = ":man_mechanic_tone1:", + ["\U0001f468\U0001f3fc\u200d\U0001f527"] = ":man_mechanic_tone2:", + ["\U0001f468\U0001f3fd\u200d\U0001f527"] = ":man_mechanic_tone3:", + ["\U0001f468\U0001f3fe\u200d\U0001f527"] = ":man_mechanic_tone4:", + ["\U0001f468\U0001f3ff\u200d\U0001f527"] = ":man_mechanic_tone5:", + ["\U0001f468\u200d\U0001f527"] = ":man_mechanic:", + ["\U0001f6b5\U0001f3fb\u200d\u2642\ufe0f"] = ":man_mountain_biking_tone1:", + ["\U0001f6b5\U0001f3fc\u200d\u2642\ufe0f"] = ":man_mountain_biking_tone2:", + ["\U0001f6b5\U0001f3fd\u200d\u2642\ufe0f"] = ":man_mountain_biking_tone3:", + ["\U0001f6b5\U0001f3fe\u200d\u2642\ufe0f"] = ":man_mountain_biking_tone4:", + ["\U0001f6b5\U0001f3ff\u200d\u2642\ufe0f"] = ":man_mountain_biking_tone5:", + ["\U0001f6b5\u200d\u2642\ufe0f"] = ":man_mountain_biking:", + ["\U0001f468\U0001f3fb\u200d\U0001f4bc"] = ":man_office_worker_tone1:", + ["\U0001f468\U0001f3fc\u200d\U0001f4bc"] = ":man_office_worker_tone2:", + ["\U0001f468\U0001f3fd\u200d\U0001f4bc"] = ":man_office_worker_tone3:", + ["\U0001f468\U0001f3fe\u200d\U0001f4bc"] = ":man_office_worker_tone4:", + ["\U0001f468\U0001f3ff\u200d\U0001f4bc"] = ":man_office_worker_tone5:", + ["\U0001f468\u200d\U0001f4bc"] = ":man_office_worker:", + ["\U0001f468\U0001f3fb\u200d\u2708\ufe0f"] = ":man_pilot_tone1:", + ["\U0001f468\U0001f3fc\u200d\u2708\ufe0f"] = ":man_pilot_tone2:", + ["\U0001f468\U0001f3fd\u200d\u2708\ufe0f"] = ":man_pilot_tone3:", + ["\U0001f468\U0001f3fe\u200d\u2708\ufe0f"] = ":man_pilot_tone4:", + ["\U0001f468\U0001f3ff\u200d\u2708\ufe0f"] = ":man_pilot_tone5:", + ["\U0001f468\u200d\u2708\ufe0f"] = ":man_pilot:", + ["\U0001f93e\U0001f3fb\u200d\u2642\ufe0f"] = ":man_playing_handball_tone1:", + ["\U0001f93e\U0001f3fc\u200d\u2642\ufe0f"] = ":man_playing_handball_tone2:", + ["\U0001f93e\U0001f3fd\u200d\u2642\ufe0f"] = ":man_playing_handball_tone3:", + ["\U0001f93e\U0001f3fe\u200d\u2642\ufe0f"] = ":man_playing_handball_tone4:", + ["\U0001f93e\U0001f3ff\u200d\u2642\ufe0f"] = ":man_playing_handball_tone5:", + ["\U0001f93e\u200d\u2642\ufe0f"] = ":man_playing_handball:", + ["\U0001f93d\U0001f3fb\u200d\u2642\ufe0f"] = ":man_playing_water_polo_tone1:", + ["\U0001f93d\U0001f3fc\u200d\u2642\ufe0f"] = ":man_playing_water_polo_tone2:", + ["\U0001f93d\U0001f3fd\u200d\u2642\ufe0f"] = ":man_playing_water_polo_tone3:", + ["\U0001f93d\U0001f3fe\u200d\u2642\ufe0f"] = ":man_playing_water_polo_tone4:", + ["\U0001f93d\U0001f3ff\u200d\u2642\ufe0f"] = ":man_playing_water_polo_tone5:", + ["\U0001f93d\u200d\u2642\ufe0f"] = ":man_playing_water_polo:", + ["\U0001f46e\U0001f3fb\u200d\u2642\ufe0f"] = ":man_police_officer_tone1:", + ["\U0001f46e\U0001f3fc\u200d\u2642\ufe0f"] = ":man_police_officer_tone2:", + ["\U0001f46e\U0001f3fd\u200d\u2642\ufe0f"] = ":man_police_officer_tone3:", + ["\U0001f46e\U0001f3fe\u200d\u2642\ufe0f"] = ":man_police_officer_tone4:", + ["\U0001f46e\U0001f3ff\u200d\u2642\ufe0f"] = ":man_police_officer_tone5:", + ["\U0001f46e\u200d\u2642\ufe0f"] = ":man_police_officer:", + ["\U0001f64e\U0001f3fb\u200d\u2642\ufe0f"] = ":man_pouting_tone1:", + ["\U0001f64e\U0001f3fc\u200d\u2642\ufe0f"] = ":man_pouting_tone2:", + ["\U0001f64e\U0001f3fd\u200d\u2642\ufe0f"] = ":man_pouting_tone3:", + ["\U0001f64e\U0001f3fe\u200d\u2642\ufe0f"] = ":man_pouting_tone4:", + ["\U0001f64e\U0001f3ff\u200d\u2642\ufe0f"] = ":man_pouting_tone5:", + ["\U0001f64e\u200d\u2642\ufe0f"] = ":man_pouting:", + ["\U0001f64b\U0001f3fb\u200d\u2642\ufe0f"] = ":man_raising_hand_tone1:", + ["\U0001f64b\U0001f3fc\u200d\u2642\ufe0f"] = ":man_raising_hand_tone2:", + ["\U0001f64b\U0001f3fd\u200d\u2642\ufe0f"] = ":man_raising_hand_tone3:", + ["\U0001f64b\U0001f3fe\u200d\u2642\ufe0f"] = ":man_raising_hand_tone4:", + ["\U0001f64b\U0001f3ff\u200d\u2642\ufe0f"] = ":man_raising_hand_tone5:", + ["\U0001f64b\u200d\u2642\ufe0f"] = ":man_raising_hand:", + ["\U0001f468\U0001f3fb\u200d\U0001f9b0"] = ":man_red_haired_tone1:", + ["\U0001f468\U0001f3fc\u200d\U0001f9b0"] = ":man_red_haired_tone2:", + ["\U0001f468\U0001f3fd\u200d\U0001f9b0"] = ":man_red_haired_tone3:", + ["\U0001f468\U0001f3fe\u200d\U0001f9b0"] = ":man_red_haired_tone4:", + ["\U0001f468\U0001f3ff\u200d\U0001f9b0"] = ":man_red_haired_tone5:", + ["\U0001f468\u200d\U0001f9b0"] = ":man_red_haired:", + ["\U0001f6a3\U0001f3fb\u200d\u2642\ufe0f"] = ":man_rowing_boat_tone1:", + ["\U0001f6a3\U0001f3fc\u200d\u2642\ufe0f"] = ":man_rowing_boat_tone2:", + ["\U0001f6a3\U0001f3fd\u200d\u2642\ufe0f"] = ":man_rowing_boat_tone3:", + ["\U0001f6a3\U0001f3fe\u200d\u2642\ufe0f"] = ":man_rowing_boat_tone4:", + ["\U0001f6a3\U0001f3ff\u200d\u2642\ufe0f"] = ":man_rowing_boat_tone5:", + ["\U0001f6a3\u200d\u2642\ufe0f"] = ":man_rowing_boat:", + ["\U0001f3c3\U0001f3fb\u200d\u2642\ufe0f\u200d\u27a1\ufe0f"] = ":man_running_facing_right_tone1:", + ["\U0001f3c3\U0001f3fc\u200d\u2642\ufe0f\u200d\u27a1\ufe0f"] = ":man_running_facing_right_tone2:", + ["\U0001f3c3\U0001f3fd\u200d\u2642\ufe0f\u200d\u27a1\ufe0f"] = ":man_running_facing_right_tone3:", + ["\U0001f3c3\U0001f3fe\u200d\u2642\ufe0f\u200d\u27a1\ufe0f"] = ":man_running_facing_right_tone4:", + ["\U0001f3c3\U0001f3ff\u200d\u2642\ufe0f\u200d\u27a1\ufe0f"] = ":man_running_facing_right_tone5:", + ["\U0001f3c3\u200d\u2642\ufe0f\u200d\u27a1\ufe0f"] = ":man_running_facing_right:", + ["\U0001f3c3\U0001f3fb\u200d\u2642\ufe0f"] = ":man_running_tone1:", + ["\U0001f3c3\U0001f3fc\u200d\u2642\ufe0f"] = ":man_running_tone2:", + ["\U0001f3c3\U0001f3fd\u200d\u2642\ufe0f"] = ":man_running_tone3:", + ["\U0001f3c3\U0001f3fe\u200d\u2642\ufe0f"] = ":man_running_tone4:", + ["\U0001f3c3\U0001f3ff\u200d\u2642\ufe0f"] = ":man_running_tone5:", + ["\U0001f3c3\u200d\u2642\ufe0f"] = ":man_running:", + ["\U0001f468\U0001f3fb\u200d\U0001f52c"] = ":man_scientist_tone1:", + ["\U0001f468\U0001f3fc\u200d\U0001f52c"] = ":man_scientist_tone2:", + ["\U0001f468\U0001f3fd\u200d\U0001f52c"] = ":man_scientist_tone3:", + ["\U0001f468\U0001f3fe\u200d\U0001f52c"] = ":man_scientist_tone4:", + ["\U0001f468\U0001f3ff\u200d\U0001f52c"] = ":man_scientist_tone5:", + ["\U0001f468\u200d\U0001f52c"] = ":man_scientist:", + ["\U0001f937\U0001f3fb\u200d\u2642\ufe0f"] = ":man_shrugging_tone1:", + ["\U0001f937\U0001f3fc\u200d\u2642\ufe0f"] = ":man_shrugging_tone2:", + ["\U0001f937\U0001f3fd\u200d\u2642\ufe0f"] = ":man_shrugging_tone3:", + ["\U0001f937\U0001f3fe\u200d\u2642\ufe0f"] = ":man_shrugging_tone4:", + ["\U0001f937\U0001f3ff\u200d\u2642\ufe0f"] = ":man_shrugging_tone5:", + ["\U0001f937\u200d\u2642\ufe0f"] = ":man_shrugging:", + ["\U0001f468\U0001f3fb\u200d\U0001f3a4"] = ":man_singer_tone1:", + ["\U0001f468\U0001f3fc\u200d\U0001f3a4"] = ":man_singer_tone2:", + ["\U0001f468\U0001f3fd\u200d\U0001f3a4"] = ":man_singer_tone3:", + ["\U0001f468\U0001f3fe\u200d\U0001f3a4"] = ":man_singer_tone4:", + ["\U0001f468\U0001f3ff\u200d\U0001f3a4"] = ":man_singer_tone5:", + ["\U0001f468\u200d\U0001f3a4"] = ":man_singer:", + ["\U0001f9cd\U0001f3fb\u200d\u2642\ufe0f"] = ":man_standing_tone1:", + ["\U0001f9cd\U0001f3fc\u200d\u2642\ufe0f"] = ":man_standing_tone2:", + ["\U0001f9cd\U0001f3fd\u200d\u2642\ufe0f"] = ":man_standing_tone3:", + ["\U0001f9cd\U0001f3fe\u200d\u2642\ufe0f"] = ":man_standing_tone4:", + ["\U0001f9cd\U0001f3ff\u200d\u2642\ufe0f"] = ":man_standing_tone5:", + ["\U0001f9cd\u200d\u2642\ufe0f"] = ":man_standing:", + ["\U0001f468\U0001f3fb\u200d\U0001f393"] = ":man_student_tone1:", + ["\U0001f468\U0001f3fc\u200d\U0001f393"] = ":man_student_tone2:", + ["\U0001f468\U0001f3fd\u200d\U0001f393"] = ":man_student_tone3:", + ["\U0001f468\U0001f3fe\u200d\U0001f393"] = ":man_student_tone4:", + ["\U0001f468\U0001f3ff\u200d\U0001f393"] = ":man_student_tone5:", + ["\U0001f468\u200d\U0001f393"] = ":man_student:", + ["\U0001f9b8\U0001f3fb\u200d\u2642\ufe0f"] = ":man_superhero_tone1:", + ["\U0001f9b8\U0001f3fc\u200d\u2642\ufe0f"] = ":man_superhero_tone2:", + ["\U0001f9b8\U0001f3fd\u200d\u2642\ufe0f"] = ":man_superhero_tone3:", + ["\U0001f9b8\U0001f3fe\u200d\u2642\ufe0f"] = ":man_superhero_tone4:", + ["\U0001f9b8\U0001f3ff\u200d\u2642\ufe0f"] = ":man_superhero_tone5:", + ["\U0001f9b8\u200d\u2642\ufe0f"] = ":man_superhero:", + ["\U0001f9b9\U0001f3fb\u200d\u2642\ufe0f"] = ":man_supervillain_tone1:", + ["\U0001f9b9\U0001f3fc\u200d\u2642\ufe0f"] = ":man_supervillain_tone2:", + ["\U0001f9b9\U0001f3fd\u200d\u2642\ufe0f"] = ":man_supervillain_tone3:", + ["\U0001f9b9\U0001f3fe\u200d\u2642\ufe0f"] = ":man_supervillain_tone4:", + ["\U0001f9b9\U0001f3ff\u200d\u2642\ufe0f"] = ":man_supervillain_tone5:", + ["\U0001f9b9\u200d\u2642\ufe0f"] = ":man_supervillain:", + ["\U0001f3c4\U0001f3fb\u200d\u2642\ufe0f"] = ":man_surfing_tone1:", + ["\U0001f3c4\U0001f3fc\u200d\u2642\ufe0f"] = ":man_surfing_tone2:", + ["\U0001f3c4\U0001f3fd\u200d\u2642\ufe0f"] = ":man_surfing_tone3:", + ["\U0001f3c4\U0001f3fe\u200d\u2642\ufe0f"] = ":man_surfing_tone4:", + ["\U0001f3c4\U0001f3ff\u200d\u2642\ufe0f"] = ":man_surfing_tone5:", + ["\U0001f3c4\u200d\u2642\ufe0f"] = ":man_surfing:", + ["\U0001f3ca\U0001f3fb\u200d\u2642\ufe0f"] = ":man_swimming_tone1:", + ["\U0001f3ca\U0001f3fc\u200d\u2642\ufe0f"] = ":man_swimming_tone2:", + ["\U0001f3ca\U0001f3fd\u200d\u2642\ufe0f"] = ":man_swimming_tone3:", + ["\U0001f3ca\U0001f3fe\u200d\u2642\ufe0f"] = ":man_swimming_tone4:", + ["\U0001f3ca\U0001f3ff\u200d\u2642\ufe0f"] = ":man_swimming_tone5:", + ["\U0001f3ca\u200d\u2642\ufe0f"] = ":man_swimming:", + ["\U0001f468\U0001f3fb\u200d\U0001f3eb"] = ":man_teacher_tone1:", + ["\U0001f468\U0001f3fc\u200d\U0001f3eb"] = ":man_teacher_tone2:", + ["\U0001f468\U0001f3fd\u200d\U0001f3eb"] = ":man_teacher_tone3:", + ["\U0001f468\U0001f3fe\u200d\U0001f3eb"] = ":man_teacher_tone4:", + ["\U0001f468\U0001f3ff\u200d\U0001f3eb"] = ":man_teacher_tone5:", + ["\U0001f468\u200d\U0001f3eb"] = ":man_teacher:", + ["\U0001f468\U0001f3fb\u200d\U0001f4bb"] = ":man_technologist_tone1:", + ["\U0001f468\U0001f3fc\u200d\U0001f4bb"] = ":man_technologist_tone2:", + ["\U0001f468\U0001f3fd\u200d\U0001f4bb"] = ":man_technologist_tone3:", + ["\U0001f468\U0001f3fe\u200d\U0001f4bb"] = ":man_technologist_tone4:", + ["\U0001f468\U0001f3ff\u200d\U0001f4bb"] = ":man_technologist_tone5:", + ["\U0001f468\u200d\U0001f4bb"] = ":man_technologist:", + ["\U0001f481\U0001f3fb\u200d\u2642\ufe0f"] = ":man_tipping_hand_tone1:", + ["\U0001f481\U0001f3fc\u200d\u2642\ufe0f"] = ":man_tipping_hand_tone2:", + ["\U0001f481\U0001f3fd\u200d\u2642\ufe0f"] = ":man_tipping_hand_tone3:", + ["\U0001f481\U0001f3fe\u200d\u2642\ufe0f"] = ":man_tipping_hand_tone4:", + ["\U0001f481\U0001f3ff\u200d\u2642\ufe0f"] = ":man_tipping_hand_tone5:", + ["\U0001f481\u200d\u2642\ufe0f"] = ":man_tipping_hand:", + ["\U0001f9d4\U0001f3fb\u200d\u2642\ufe0f"] = ":man_tone1_beard:", + ["\U0001f468\U0001f3fb"] = ":man_tone1:", + ["\U0001f9d4\U0001f3fc\u200d\u2642\ufe0f"] = ":man_tone2_beard:", + ["\U0001f468\U0001f3fc"] = ":man_tone2:", + ["\U0001f9d4\U0001f3fd\u200d\u2642\ufe0f"] = ":man_tone3_beard:", + ["\U0001f468\U0001f3fd"] = ":man_tone3:", + ["\U0001f9d4\U0001f3fe\u200d\u2642\ufe0f"] = ":man_tone4_beard:", + ["\U0001f468\U0001f3fe"] = ":man_tone4:", + ["\U0001f9d4\U0001f3ff\u200d\u2642\ufe0f"] = ":man_tone5_beard:", + ["\U0001f468\U0001f3ff"] = ":man_tone5:", + ["\U0001f9db\U0001f3fb\u200d\u2642\ufe0f"] = ":man_vampire_tone1:", + ["\U0001f9db\U0001f3fc\u200d\u2642\ufe0f"] = ":man_vampire_tone2:", + ["\U0001f9db\U0001f3fd\u200d\u2642\ufe0f"] = ":man_vampire_tone3:", + ["\U0001f9db\U0001f3fe\u200d\u2642\ufe0f"] = ":man_vampire_tone4:", + ["\U0001f9db\U0001f3ff\u200d\u2642\ufe0f"] = ":man_vampire_tone5:", + ["\U0001f9db\u200d\u2642\ufe0f"] = ":man_vampire:", + ["\U0001f6b6\U0001f3fb\u200d\u2642\ufe0f\u200d\u27a1\ufe0f"] = ":man_walking_facing_right_tone1:", + ["\U0001f6b6\U0001f3fc\u200d\u2642\ufe0f\u200d\u27a1\ufe0f"] = ":man_walking_facing_right_tone2:", + ["\U0001f6b6\U0001f3fd\u200d\u2642\ufe0f\u200d\u27a1\ufe0f"] = ":man_walking_facing_right_tone3:", + ["\U0001f6b6\U0001f3fe\u200d\u2642\ufe0f\u200d\u27a1\ufe0f"] = ":man_walking_facing_right_tone4:", + ["\U0001f6b6\U0001f3ff\u200d\u2642\ufe0f\u200d\u27a1\ufe0f"] = ":man_walking_facing_right_tone5:", + ["\U0001f6b6\u200d\u2642\ufe0f\u200d\u27a1\ufe0f"] = ":man_walking_facing_right:", + ["\U0001f6b6\U0001f3fb\u200d\u2642\ufe0f"] = ":man_walking_tone1:", + ["\U0001f6b6\U0001f3fc\u200d\u2642\ufe0f"] = ":man_walking_tone2:", + ["\U0001f6b6\U0001f3fd\u200d\u2642\ufe0f"] = ":man_walking_tone3:", + ["\U0001f6b6\U0001f3fe\u200d\u2642\ufe0f"] = ":man_walking_tone4:", + ["\U0001f6b6\U0001f3ff\u200d\u2642\ufe0f"] = ":man_walking_tone5:", + ["\U0001f6b6\u200d\u2642\ufe0f"] = ":man_walking:", + ["\U0001f473\U0001f3fb\u200d\u2642\ufe0f"] = ":man_wearing_turban_tone1:", + ["\U0001f473\U0001f3fc\u200d\u2642\ufe0f"] = ":man_wearing_turban_tone2:", + ["\U0001f473\U0001f3fd\u200d\u2642\ufe0f"] = ":man_wearing_turban_tone3:", + ["\U0001f473\U0001f3fe\u200d\u2642\ufe0f"] = ":man_wearing_turban_tone4:", + ["\U0001f473\U0001f3ff\u200d\u2642\ufe0f"] = ":man_wearing_turban_tone5:", + ["\U0001f473\u200d\u2642\ufe0f"] = ":man_wearing_turban:", + ["\U0001f468\U0001f3fb\u200d\U0001f9b3"] = ":man_white_haired_tone1:", + ["\U0001f468\U0001f3fc\u200d\U0001f9b3"] = ":man_white_haired_tone2:", + ["\U0001f468\U0001f3fd\u200d\U0001f9b3"] = ":man_white_haired_tone3:", + ["\U0001f468\U0001f3fe\u200d\U0001f9b3"] = ":man_white_haired_tone4:", + ["\U0001f468\U0001f3ff\u200d\U0001f9b3"] = ":man_white_haired_tone5:", + ["\U0001f468\u200d\U0001f9b3"] = ":man_white_haired:", + ["\U0001f472\U0001f3fb"] = ":man_with_chinese_cap_tone1:", + ["\U0001f472\U0001f3fc"] = ":man_with_chinese_cap_tone2:", + ["\U0001f472\U0001f3fd"] = ":man_with_chinese_cap_tone3:", + ["\U0001f472\U0001f3fe"] = ":man_with_chinese_cap_tone4:", + ["\U0001f472\U0001f3ff"] = ":man_with_chinese_cap_tone5:", + ["\U0001f472"] = ":man_with_chinese_cap:", + ["\U0001f468\U0001f3fb\u200d\U0001f9af"] = ":man_with_probing_cane_tone1:", + ["\U0001f468\U0001f3fc\u200d\U0001f9af"] = ":man_with_probing_cane_tone2:", + ["\U0001f468\U0001f3fd\u200d\U0001f9af"] = ":man_with_probing_cane_tone3:", + ["\U0001f468\U0001f3fe\u200d\U0001f9af"] = ":man_with_probing_cane_tone4:", + ["\U0001f468\U0001f3ff\u200d\U0001f9af"] = ":man_with_probing_cane_tone5:", + ["\U0001f468\u200d\U0001f9af"] = ":man_with_probing_cane:", + ["\U0001f470\U0001f3fb\u200d\u2642\ufe0f"] = ":man_with_veil_tone1:", + ["\U0001f470\U0001f3fc\u200d\u2642\ufe0f"] = ":man_with_veil_tone2:", + ["\U0001f470\U0001f3fd\u200d\u2642\ufe0f"] = ":man_with_veil_tone3:", + ["\U0001f470\U0001f3fe\u200d\u2642\ufe0f"] = ":man_with_veil_tone4:", + ["\U0001f470\U0001f3ff\u200d\u2642\ufe0f"] = ":man_with_veil_tone5:", + ["\U0001f470\u200d\u2642\ufe0f"] = ":man_with_veil:", + ["\U0001f468\U0001f3fb\u200d\U0001f9af\u200d\u27a1\ufe0f"] = ":man_with_white_cane_facing_right_tone1:", + ["\U0001f468\U0001f3fc\u200d\U0001f9af\u200d\u27a1\ufe0f"] = ":man_with_white_cane_facing_right_tone2:", + ["\U0001f468\U0001f3fd\u200d\U0001f9af\u200d\u27a1\ufe0f"] = ":man_with_white_cane_facing_right_tone3:", + ["\U0001f468\U0001f3fe\u200d\U0001f9af\u200d\u27a1\ufe0f"] = ":man_with_white_cane_facing_right_tone4:", + ["\U0001f468\U0001f3ff\u200d\U0001f9af\u200d\u27a1\ufe0f"] = ":man_with_white_cane_facing_right_tone5:", + ["\U0001f468\u200d\U0001f9af\u200d\u27a1\ufe0f"] = ":man_with_white_cane_facing_right:", + ["\U0001f9df\u200d\u2642\ufe0f"] = ":man_zombie:", + ["\U0001f468"] = ":man:", + ["\U0001f96d"] = ":mango:", + ["\U0001f45e"] = ":mans_shoe:", + ["\U0001f9bd"] = ":manual_wheelchair:", + ["\U0001f5fa\ufe0f"] = ":map:", + ["\U0001f5fa"] = ":map:", + ["\U0001f341"] = ":maple_leaf:", + ["\U0001fa87"] = ":maracas:", + ["\U0001f94b"] = ":martial_arts_uniform:", + ["\U0001f637"] = ":mask:", + ["\U0001f9c9"] = ":mate:", + ["\U0001f356"] = ":meat_on_bone:", + ["\U0001f9d1\U0001f3fb\u200d\U0001f527"] = ":mechanic_tone1:", + ["\U0001f9d1\U0001f3fc\u200d\U0001f527"] = ":mechanic_tone2:", + ["\U0001f9d1\U0001f3fd\u200d\U0001f527"] = ":mechanic_tone3:", + ["\U0001f9d1\U0001f3fe\u200d\U0001f527"] = ":mechanic_tone4:", + ["\U0001f9d1\U0001f3ff\u200d\U0001f527"] = ":mechanic_tone5:", + ["\U0001f9d1\u200d\U0001f527"] = ":mechanic:", + ["\U0001f9be"] = ":mechanical_arm:", + ["\U0001f9bf"] = ":mechanical_leg:", + ["\U0001f3c5"] = ":medal:", + ["\u2695\ufe0f"] = ":medical_symbol:", + ["\u2695"] = ":medical_symbol:", + ["\U0001f4e3"] = ":mega:", + ["\U0001f348"] = ":melon:", + ["\U0001fae0"] = ":melting_face:", + ["\U0001f46f\u200d\u2642\ufe0f"] = ":men_with_bunny_ears_partying:", + ["\U0001f93c\u200d\u2642\ufe0f"] = ":men_wrestling:", + ["\u2764\ufe0f\u200d\U0001fa79"] = ":mending_heart:", + ["\U0001f54e"] = ":menorah:", + ["\U0001f6b9"] = ":mens:", + ["\U0001f9dc\U0001f3fb\u200d\u2640\ufe0f"] = ":mermaid_tone1:", + ["\U0001f9dc\U0001f3fc\u200d\u2640\ufe0f"] = ":mermaid_tone2:", + ["\U0001f9dc\U0001f3fd\u200d\u2640\ufe0f"] = ":mermaid_tone3:", + ["\U0001f9dc\U0001f3fe\u200d\u2640\ufe0f"] = ":mermaid_tone4:", + ["\U0001f9dc\U0001f3ff\u200d\u2640\ufe0f"] = ":mermaid_tone5:", + ["\U0001f9dc\u200d\u2640\ufe0f"] = ":mermaid:", + ["\U0001f9dc\U0001f3fb\u200d\u2642\ufe0f"] = ":merman_tone1:", + ["\U0001f9dc\U0001f3fc\u200d\u2642\ufe0f"] = ":merman_tone2:", + ["\U0001f9dc\U0001f3fd\u200d\u2642\ufe0f"] = ":merman_tone3:", + ["\U0001f9dc\U0001f3fe\u200d\u2642\ufe0f"] = ":merman_tone4:", + ["\U0001f9dc\U0001f3ff\u200d\u2642\ufe0f"] = ":merman_tone5:", + ["\U0001f9dc\u200d\u2642\ufe0f"] = ":merman:", + ["\U0001f9dc\U0001f3fb"] = ":merperson_tone1:", + ["\U0001f9dc\U0001f3fc"] = ":merperson_tone2:", + ["\U0001f9dc\U0001f3fd"] = ":merperson_tone3:", + ["\U0001f9dc\U0001f3fe"] = ":merperson_tone4:", + ["\U0001f9dc\U0001f3ff"] = ":merperson_tone5:", + ["\U0001f9dc"] = ":merperson:", + ["\U0001f918\U0001f3fb"] = ":metal_tone1:", + ["\U0001f918\U0001f3fc"] = ":metal_tone2:", + ["\U0001f918\U0001f3fd"] = ":metal_tone3:", + ["\U0001f918\U0001f3fe"] = ":metal_tone4:", + ["\U0001f918\U0001f3ff"] = ":metal_tone5:", + ["\U0001f918"] = ":metal:", + ["\U0001f687"] = ":metro:", + ["\U0001f9a0"] = ":microbe:", + ["\U0001f3a4"] = ":microphone:", + ["\U0001f399\ufe0f"] = ":microphone2:", + ["\U0001f399"] = ":microphone2:", + ["\U0001f52c"] = ":microscope:", + ["\U0001f595\U0001f3fb"] = ":middle_finger_tone1:", + ["\U0001f595\U0001f3fc"] = ":middle_finger_tone2:", + ["\U0001f595\U0001f3fd"] = ":middle_finger_tone3:", + ["\U0001f595\U0001f3fe"] = ":middle_finger_tone4:", + ["\U0001f595\U0001f3ff"] = ":middle_finger_tone5:", + ["\U0001f595"] = ":middle_finger:", + ["\U0001fa96"] = ":military_helmet:", + ["\U0001f396\ufe0f"] = ":military_medal:", + ["\U0001f396"] = ":military_medal:", + ["\U0001f95b"] = ":milk:", + ["\U0001f30c"] = ":milky_way:", + ["\U0001f690"] = ":minibus:", + ["\U0001f4bd"] = ":minidisc:", + ["\U0001faa9"] = ":mirror_ball:", + ["\U0001fa9e"] = ":mirror:", + ["\U0001f4f4"] = ":mobile_phone_off:", + ["\U0001f4f1"] = ":mobile_phone:", + ["\U0001f911"] = ":money_mouth:", + ["\U0001f4b8"] = ":money_with_wings:", + ["\U0001f4b0"] = ":moneybag:", + ["\U0001f435"] = ":monkey_face:", + ["\U0001f412"] = ":monkey:", + ["\U0001f69d"] = ":monorail:", + ["\U0001f96e"] = ":moon_cake:", + ["\U0001face"] = ":moose:", + ["\U0001f393"] = ":mortar_board:", + ["\U0001f54c"] = ":mosque:", + ["\U0001f99f"] = ":mosquito:", + ["\U0001f6f5"] = ":motor_scooter:", + ["\U0001f6e5\ufe0f"] = ":motorboat:", + ["\U0001f6e5"] = ":motorboat:", + ["\U0001f3cd\ufe0f"] = ":motorcycle:", + ["\U0001f3cd"] = ":motorcycle:", + ["\U0001f9bc"] = ":motorized_wheelchair:", + ["\U0001f6e3\ufe0f"] = ":motorway:", + ["\U0001f6e3"] = ":motorway:", + ["\U0001f5fb"] = ":mount_fuji:", + ["\U0001f6a0"] = ":mountain_cableway:", + ["\U0001f69e"] = ":mountain_railway:", + ["\U0001f3d4\ufe0f"] = ":mountain_snow:", + ["\U0001f3d4"] = ":mountain_snow:", + ["\u26f0\ufe0f"] = ":mountain:", + ["\u26f0"] = ":mountain:", + ["\U0001f5b1\ufe0f"] = ":mouse_three_button:", + ["\U0001f5b1"] = ":mouse_three_button:", + ["\U0001faa4"] = ":mouse_trap:", + ["\U0001f42d"] = ":mouse:", + ["\U0001f401"] = ":mouse2:", + ["\U0001f3a5"] = ":movie_camera:", + ["\U0001f5ff"] = ":moyai:", + ["\U0001f936\U0001f3fb"] = ":mrs_claus_tone1:", + ["\U0001f936\U0001f3fc"] = ":mrs_claus_tone2:", + ["\U0001f936\U0001f3fd"] = ":mrs_claus_tone3:", + ["\U0001f936\U0001f3fe"] = ":mrs_claus_tone4:", + ["\U0001f936\U0001f3ff"] = ":mrs_claus_tone5:", + ["\U0001f936"] = ":mrs_claus:", + ["\U0001f4aa\U0001f3fb"] = ":muscle_tone1:", + ["\U0001f4aa\U0001f3fc"] = ":muscle_tone2:", + ["\U0001f4aa\U0001f3fd"] = ":muscle_tone3:", + ["\U0001f4aa\U0001f3fe"] = ":muscle_tone4:", + ["\U0001f4aa\U0001f3ff"] = ":muscle_tone5:", + ["\U0001f4aa"] = ":muscle:", + ["\U0001f344"] = ":mushroom:", + ["\U0001f3b9"] = ":musical_keyboard:", + ["\U0001f3b5"] = ":musical_note:", + ["\U0001f3bc"] = ":musical_score:", + ["\U0001f507"] = ":mute:", + ["\U0001f9d1\U0001f3fb\u200d\U0001f384"] = ":mx_claus_tone1:", + ["\U0001f9d1\U0001f3fc\u200d\U0001f384"] = ":mx_claus_tone2:", + ["\U0001f9d1\U0001f3fd\u200d\U0001f384"] = ":mx_claus_tone3:", + ["\U0001f9d1\U0001f3fe\u200d\U0001f384"] = ":mx_claus_tone4:", + ["\U0001f9d1\U0001f3ff\u200d\U0001f384"] = ":mx_claus_tone5:", + ["\U0001f9d1\u200d\U0001f384"] = ":mx_claus:", + ["\U0001f485\U0001f3fb"] = ":nail_care_tone1:", + ["\U0001f485\U0001f3fc"] = ":nail_care_tone2:", + ["\U0001f485\U0001f3fd"] = ":nail_care_tone3:", + ["\U0001f485\U0001f3fe"] = ":nail_care_tone4:", + ["\U0001f485\U0001f3ff"] = ":nail_care_tone5:", + ["\U0001f485"] = ":nail_care:", + ["\U0001f4db"] = ":name_badge:", + ["\U0001f922"] = ":nauseated_face:", + ["\U0001f9ff"] = ":nazar_amulet:", + ["\U0001f454"] = ":necktie:", + ["\u274e"] = ":negative_squared_cross_mark:", + ["\U0001f913"] = ":nerd:", + ["\U0001faba"] = ":nest_with_eggs:", + ["\U0001fa86"] = ":nesting_dolls:", + ["\U0001f610"] = ":neutral_face:", + ["\U0001f31a"] = ":new_moon_with_face:", + ["\U0001f311"] = ":new_moon:", + ["\U0001f195"] = ":new:", + ["\U0001f4f0"] = ":newspaper:", + ["\U0001f5de\ufe0f"] = ":newspaper2:", + ["\U0001f5de"] = ":newspaper2:", + ["\U0001f196"] = ":ng:", + ["\U0001f303"] = ":night_with_stars:", + ["\u0039\ufe0f\u20e3"] = ":nine:", + ["\u0039\u20e3"] = ":nine:", + ["\U0001f977\U0001f3fb"] = ":ninja_tone1:", + ["\U0001f977\U0001f3fc"] = ":ninja_tone2:", + ["\U0001f977\U0001f3fd"] = ":ninja_tone3:", + ["\U0001f977\U0001f3fe"] = ":ninja_tone4:", + ["\U0001f977\U0001f3ff"] = ":ninja_tone5:", + ["\U0001f977"] = ":ninja:", + ["\U0001f515"] = ":no_bell:", + ["\U0001f6b3"] = ":no_bicycles:", + ["\U0001f6ab"] = ":no_entry_sign:", + ["\u26d4"] = ":no_entry:", + ["\U0001f4f5"] = ":no_mobile_phones:", + ["\U0001f636"] = ":no_mouth:", + ["\U0001f6b7"] = ":no_pedestrians:", + ["\U0001f6ad"] = ":no_smoking:", + ["\U0001f6b1"] = ":non_potable_water:", + ["\U0001f443\U0001f3fb"] = ":nose_tone1:", + ["\U0001f443\U0001f3fc"] = ":nose_tone2:", + ["\U0001f443\U0001f3fd"] = ":nose_tone3:", + ["\U0001f443\U0001f3fe"] = ":nose_tone4:", + ["\U0001f443\U0001f3ff"] = ":nose_tone5:", + ["\U0001f443"] = ":nose:", + ["\U0001f4d4"] = ":notebook_with_decorative_cover:", + ["\U0001f4d3"] = ":notebook:", + ["\U0001f5d2\ufe0f"] = ":notepad_spiral:", + ["\U0001f5d2"] = ":notepad_spiral:", + ["\U0001f3b6"] = ":notes:", + ["\U0001f529"] = ":nut_and_bolt:", + ["\u2b55"] = ":o:", + ["\U0001f17e\ufe0f"] = ":o2:", + ["\U0001f17e"] = ":o2:", + ["\U0001f30a"] = ":ocean:", + ["\U0001f6d1"] = ":octagonal_sign:", + ["\U0001f419"] = ":octopus:", + ["\U0001f362"] = ":oden:", + ["\U0001f9d1\U0001f3fb\u200d\U0001f4bc"] = ":office_worker_tone1:", + ["\U0001f9d1\U0001f3fc\u200d\U0001f4bc"] = ":office_worker_tone2:", + ["\U0001f9d1\U0001f3fd\u200d\U0001f4bc"] = ":office_worker_tone3:", + ["\U0001f9d1\U0001f3fe\u200d\U0001f4bc"] = ":office_worker_tone4:", + ["\U0001f9d1\U0001f3ff\u200d\U0001f4bc"] = ":office_worker_tone5:", + ["\U0001f9d1\u200d\U0001f4bc"] = ":office_worker:", + ["\U0001f3e2"] = ":office:", + ["\U0001f6e2\ufe0f"] = ":oil:", + ["\U0001f6e2"] = ":oil:", + ["\U0001f44c\U0001f3fb"] = ":ok_hand_tone1:", + ["\U0001f44c\U0001f3fc"] = ":ok_hand_tone2:", + ["\U0001f44c\U0001f3fd"] = ":ok_hand_tone3:", + ["\U0001f44c\U0001f3fe"] = ":ok_hand_tone4:", + ["\U0001f44c\U0001f3ff"] = ":ok_hand_tone5:", + ["\U0001f44c"] = ":ok_hand:", + ["\U0001f197"] = ":ok:", + ["\U0001f9d3\U0001f3fb"] = ":older_adult_tone1:", + ["\U0001f9d3\U0001f3fc"] = ":older_adult_tone2:", + ["\U0001f9d3\U0001f3fd"] = ":older_adult_tone3:", + ["\U0001f9d3\U0001f3fe"] = ":older_adult_tone4:", + ["\U0001f9d3\U0001f3ff"] = ":older_adult_tone5:", + ["\U0001f9d3"] = ":older_adult:", + ["\U0001f474\U0001f3fb"] = ":older_man_tone1:", + ["\U0001f474\U0001f3fc"] = ":older_man_tone2:", + ["\U0001f474\U0001f3fd"] = ":older_man_tone3:", + ["\U0001f474\U0001f3fe"] = ":older_man_tone4:", + ["\U0001f474\U0001f3ff"] = ":older_man_tone5:", + ["\U0001f474"] = ":older_man:", + ["\U0001f475\U0001f3fb"] = ":older_woman_tone1:", + ["\U0001f475\U0001f3fc"] = ":older_woman_tone2:", + ["\U0001f475\U0001f3fd"] = ":older_woman_tone3:", + ["\U0001f475\U0001f3fe"] = ":older_woman_tone4:", + ["\U0001f475\U0001f3ff"] = ":older_woman_tone5:", + ["\U0001f475"] = ":older_woman:", + ["\U0001fad2"] = ":olive:", + ["\U0001f549\ufe0f"] = ":om_symbol:", + ["\U0001f549"] = ":om_symbol:", + ["\U0001f51b"] = ":on:", + ["\U0001f698"] = ":oncoming_automobile:", + ["\U0001f68d"] = ":oncoming_bus:", + ["\U0001f694"] = ":oncoming_police_car:", + ["\U0001f696"] = ":oncoming_taxi:", + ["\U0001fa71"] = ":one_piece_swimsuit:", + ["\u0031\ufe0f\u20e3"] = ":one:", + ["\u0031\u20e3"] = ":one:", + ["\U0001f9c5"] = ":onion:", + ["\U0001f4c2"] = ":open_file_folder:", + ["\U0001f450\U0001f3fb"] = ":open_hands_tone1:", + ["\U0001f450\U0001f3fc"] = ":open_hands_tone2:", + ["\U0001f450\U0001f3fd"] = ":open_hands_tone3:", + ["\U0001f450\U0001f3fe"] = ":open_hands_tone4:", + ["\U0001f450\U0001f3ff"] = ":open_hands_tone5:", + ["\U0001f450"] = ":open_hands:", + ["\U0001f62e"] = ":open_mouth:", + ["\u26ce"] = ":ophiuchus:", + ["\U0001f4d9"] = ":orange_book:", + ["\U0001f7e0"] = ":orange_circle:", + ["\U0001f9e1"] = ":orange_heart:", + ["\U0001f7e7"] = ":orange_square:", + ["\U0001f9a7"] = ":orangutan:", + ["\u2626\ufe0f"] = ":orthodox_cross:", + ["\u2626"] = ":orthodox_cross:", + ["\U0001f9a6"] = ":otter:", + ["\U0001f4e4"] = ":outbox_tray:", + ["\U0001f989"] = ":owl:", + ["\U0001f402"] = ":ox:", + ["\U0001f9aa"] = ":oyster:", + ["\U0001f4e6"] = ":package:", + ["\U0001f4c4"] = ":page_facing_up:", + ["\U0001f4c3"] = ":page_with_curl:", + ["\U0001f4df"] = ":pager:", + ["\U0001f58c\ufe0f"] = ":paintbrush:", + ["\U0001f58c"] = ":paintbrush:", + ["\U0001faf3\U0001f3fb"] = ":palm_down_hand_tone1:", + ["\U0001faf3\U0001f3fc"] = ":palm_down_hand_tone2:", + ["\U0001faf3\U0001f3fd"] = ":palm_down_hand_tone3:", + ["\U0001faf3\U0001f3fe"] = ":palm_down_hand_tone4:", + ["\U0001faf3\U0001f3ff"] = ":palm_down_hand_tone5:", + ["\U0001faf3"] = ":palm_down_hand:", + ["\U0001f334"] = ":palm_tree:", + ["\U0001faf4\U0001f3fb"] = ":palm_up_hand_tone1:", + ["\U0001faf4\U0001f3fc"] = ":palm_up_hand_tone2:", + ["\U0001faf4\U0001f3fd"] = ":palm_up_hand_tone3:", + ["\U0001faf4\U0001f3fe"] = ":palm_up_hand_tone4:", + ["\U0001faf4\U0001f3ff"] = ":palm_up_hand_tone5:", + ["\U0001faf4"] = ":palm_up_hand:", + ["\U0001f932\U0001f3fb"] = ":palms_up_together_tone1:", + ["\U0001f932\U0001f3fc"] = ":palms_up_together_tone2:", + ["\U0001f932\U0001f3fd"] = ":palms_up_together_tone3:", + ["\U0001f932\U0001f3fe"] = ":palms_up_together_tone4:", + ["\U0001f932\U0001f3ff"] = ":palms_up_together_tone5:", + ["\U0001f932"] = ":palms_up_together:", + ["\U0001f95e"] = ":pancakes:", + ["\U0001f43c"] = ":panda_face:", + ["\U0001f4ce"] = ":paperclip:", + ["\U0001f587\ufe0f"] = ":paperclips:", + ["\U0001f587"] = ":paperclips:", + ["\U0001fa82"] = ":parachute:", + ["\U0001f3de\ufe0f"] = ":park:", + ["\U0001f3de"] = ":park:", + ["\U0001f17f\ufe0f"] = ":parking:", + ["\U0001f17f"] = ":parking:", + ["\U0001f99c"] = ":parrot:", + ["\u303d\ufe0f"] = ":part_alternation_mark:", + ["\u303d"] = ":part_alternation_mark:", + ["\u26c5"] = ":partly_sunny:", + ["\U0001f973"] = ":partying_face:", + ["\U0001f6c2"] = ":passport_control:", + ["\u23f8\ufe0f"] = ":pause_button:", + ["\u23f8"] = ":pause_button:", + ["\U0001fadb"] = ":pea_pod:", + ["\u262e\ufe0f"] = ":peace:", + ["\u262e"] = ":peace:", + ["\U0001f351"] = ":peach:", + ["\U0001f99a"] = ":peacock:", + ["\U0001f95c"] = ":peanuts:", + ["\U0001f350"] = ":pear:", + ["\U0001f58a\ufe0f"] = ":pen_ballpoint:", + ["\U0001f58a"] = ":pen_ballpoint:", + ["\U0001f58b\ufe0f"] = ":pen_fountain:", + ["\U0001f58b"] = ":pen_fountain:", + ["\U0001f4dd"] = ":pencil:", + ["\u270f\ufe0f"] = ":pencil2:", + ["\u270f"] = ":pencil2:", + ["\U0001f427"] = ":penguin:", + ["\U0001f614"] = ":pensive:", + ["\U0001f9d1\u200d\U0001f91d\u200d\U0001f9d1"] = ":people_holding_hands_tone5_tone4:", + ["\U0001fac2"] = ":people_hugging:", + ["\U0001f46f"] = ":people_with_bunny_ears_partying:", + ["\U0001f93c"] = ":people_wrestling:", + ["\U0001f3ad"] = ":performing_arts:", + ["\U0001f623"] = ":persevere:", + ["\U0001f9d1\u200d\U0001f9b2"] = ":person_bald:", + ["\U0001f6b4\U0001f3fb"] = ":person_biking_tone1:", + ["\U0001f6b4\U0001f3fc"] = ":person_biking_tone2:", + ["\U0001f6b4\U0001f3fd"] = ":person_biking_tone3:", + ["\U0001f6b4\U0001f3fe"] = ":person_biking_tone4:", + ["\U0001f6b4\U0001f3ff"] = ":person_biking_tone5:", + ["\U0001f6b4"] = ":person_biking:", + ["\u26f9\U0001f3fb"] = ":person_bouncing_ball_tone1:", + ["\u26f9\U0001f3fc"] = ":person_bouncing_ball_tone2:", + ["\u26f9\U0001f3fd"] = ":person_bouncing_ball_tone3:", + ["\u26f9\U0001f3fe"] = ":person_bouncing_ball_tone4:", + ["\u26f9\U0001f3ff"] = ":person_bouncing_ball_tone5:", + ["\u26f9\ufe0f"] = ":person_bouncing_ball:", + ["\u26f9"] = ":person_bouncing_ball:", + ["\U0001f647\U0001f3fb"] = ":person_bowing_tone1:", + ["\U0001f647\U0001f3fc"] = ":person_bowing_tone2:", + ["\U0001f647\U0001f3fd"] = ":person_bowing_tone3:", + ["\U0001f647\U0001f3fe"] = ":person_bowing_tone4:", + ["\U0001f647\U0001f3ff"] = ":person_bowing_tone5:", + ["\U0001f647"] = ":person_bowing:", + ["\U0001f9d7\U0001f3fb"] = ":person_climbing_tone1:", + ["\U0001f9d7\U0001f3fc"] = ":person_climbing_tone2:", + ["\U0001f9d7\U0001f3fd"] = ":person_climbing_tone3:", + ["\U0001f9d7\U0001f3fe"] = ":person_climbing_tone4:", + ["\U0001f9d7\U0001f3ff"] = ":person_climbing_tone5:", + ["\U0001f9d7"] = ":person_climbing:", + ["\U0001f9d1\u200d\U0001f9b1"] = ":person_curly_hair:", + ["\U0001f938\U0001f3fb"] = ":person_doing_cartwheel_tone1:", + ["\U0001f938\U0001f3fc"] = ":person_doing_cartwheel_tone2:", + ["\U0001f938\U0001f3fd"] = ":person_doing_cartwheel_tone3:", + ["\U0001f938\U0001f3fe"] = ":person_doing_cartwheel_tone4:", + ["\U0001f938\U0001f3ff"] = ":person_doing_cartwheel_tone5:", + ["\U0001f938"] = ":person_doing_cartwheel:", + ["\U0001f926\U0001f3fb"] = ":person_facepalming_tone1:", + ["\U0001f926\U0001f3fc"] = ":person_facepalming_tone2:", + ["\U0001f926\U0001f3fd"] = ":person_facepalming_tone3:", + ["\U0001f926\U0001f3fe"] = ":person_facepalming_tone4:", + ["\U0001f926\U0001f3ff"] = ":person_facepalming_tone5:", + ["\U0001f926"] = ":person_facepalming:", + ["\U0001f9d1\U0001f3fb\u200d\U0001f37c"] = ":person_feeding_baby_tone1:", + ["\U0001f9d1\U0001f3fc\u200d\U0001f37c"] = ":person_feeding_baby_tone2:", + ["\U0001f9d1\U0001f3fd\u200d\U0001f37c"] = ":person_feeding_baby_tone3:", + ["\U0001f9d1\U0001f3fe\u200d\U0001f37c"] = ":person_feeding_baby_tone4:", + ["\U0001f9d1\U0001f3ff\u200d\U0001f37c"] = ":person_feeding_baby_tone5:", + ["\U0001f9d1\u200d\U0001f37c"] = ":person_feeding_baby:", + ["\U0001f93a"] = ":person_fencing:", + ["\U0001f64d\U0001f3fb"] = ":person_frowning_tone1:", + ["\U0001f64d\U0001f3fc"] = ":person_frowning_tone2:", + ["\U0001f64d\U0001f3fd"] = ":person_frowning_tone3:", + ["\U0001f64d\U0001f3fe"] = ":person_frowning_tone4:", + ["\U0001f64d\U0001f3ff"] = ":person_frowning_tone5:", + ["\U0001f64d"] = ":person_frowning:", + ["\U0001f645\U0001f3fb"] = ":person_gesturing_no_tone1:", + ["\U0001f645\U0001f3fc"] = ":person_gesturing_no_tone2:", + ["\U0001f645\U0001f3fd"] = ":person_gesturing_no_tone3:", + ["\U0001f645\U0001f3fe"] = ":person_gesturing_no_tone4:", + ["\U0001f645\U0001f3ff"] = ":person_gesturing_no_tone5:", + ["\U0001f645"] = ":person_gesturing_no:", + ["\U0001f646\U0001f3fb"] = ":person_gesturing_ok_tone1:", + ["\U0001f646\U0001f3fc"] = ":person_gesturing_ok_tone2:", + ["\U0001f646\U0001f3fd"] = ":person_gesturing_ok_tone3:", + ["\U0001f646\U0001f3fe"] = ":person_gesturing_ok_tone4:", + ["\U0001f646\U0001f3ff"] = ":person_gesturing_ok_tone5:", + ["\U0001f646"] = ":person_gesturing_ok:", + ["\U0001f487\U0001f3fb"] = ":person_getting_haircut_tone1:", + ["\U0001f487\U0001f3fc"] = ":person_getting_haircut_tone2:", + ["\U0001f487\U0001f3fd"] = ":person_getting_haircut_tone3:", + ["\U0001f487\U0001f3fe"] = ":person_getting_haircut_tone4:", + ["\U0001f487\U0001f3ff"] = ":person_getting_haircut_tone5:", + ["\U0001f487"] = ":person_getting_haircut:", + ["\U0001f486\U0001f3fb"] = ":person_getting_massage_tone1:", + ["\U0001f486\U0001f3fc"] = ":person_getting_massage_tone2:", + ["\U0001f486\U0001f3fd"] = ":person_getting_massage_tone3:", + ["\U0001f486\U0001f3fe"] = ":person_getting_massage_tone4:", + ["\U0001f486\U0001f3ff"] = ":person_getting_massage_tone5:", + ["\U0001f486"] = ":person_getting_massage:", + ["\U0001f3cc\U0001f3fb"] = ":person_golfing_tone1:", + ["\U0001f3cc\U0001f3fc"] = ":person_golfing_tone2:", + ["\U0001f3cc\U0001f3fd"] = ":person_golfing_tone3:", + ["\U0001f3cc\U0001f3fe"] = ":person_golfing_tone4:", + ["\U0001f3cc\U0001f3ff"] = ":person_golfing_tone5:", + ["\U0001f3cc\ufe0f"] = ":person_golfing:", + ["\U0001f3cc"] = ":person_golfing:", + ["\U0001f6cc\U0001f3fb"] = ":person_in_bed_tone1:", + ["\U0001f6cc\U0001f3fc"] = ":person_in_bed_tone2:", + ["\U0001f6cc\U0001f3fd"] = ":person_in_bed_tone3:", + ["\U0001f6cc\U0001f3fe"] = ":person_in_bed_tone4:", + ["\U0001f6cc\U0001f3ff"] = ":person_in_bed_tone5:", + ["\U0001f9d8\U0001f3fb"] = ":person_in_lotus_position_tone1:", + ["\U0001f9d8\U0001f3fc"] = ":person_in_lotus_position_tone2:", + ["\U0001f9d8\U0001f3fd"] = ":person_in_lotus_position_tone3:", + ["\U0001f9d8\U0001f3fe"] = ":person_in_lotus_position_tone4:", + ["\U0001f9d8\U0001f3ff"] = ":person_in_lotus_position_tone5:", + ["\U0001f9d8"] = ":person_in_lotus_position:", + ["\U0001f9d1\U0001f3fb\u200d\U0001f9bd\u200d\u27a1\ufe0f"] = ":person_in_manual_wheelchair_facing_right_tone1:", + ["\U0001f9d1\U0001f3fc\u200d\U0001f9bd\u200d\u27a1\ufe0f"] = ":person_in_manual_wheelchair_facing_right_tone2:", + ["\U0001f9d1\U0001f3fd\u200d\U0001f9bd\u200d\u27a1\ufe0f"] = ":person_in_manual_wheelchair_facing_right_tone3:", + ["\U0001f9d1\U0001f3fe\u200d\U0001f9bd\u200d\u27a1\ufe0f"] = ":person_in_manual_wheelchair_facing_right_tone4:", + ["\U0001f9d1\U0001f3ff\u200d\U0001f9bd\u200d\u27a1\ufe0f"] = ":person_in_manual_wheelchair_facing_right_tone5:", + ["\U0001f9d1\u200d\U0001f9bd\u200d\u27a1\ufe0f"] = ":person_in_manual_wheelchair_facing_right:", + ["\U0001f9d1\U0001f3fb\u200d\U0001f9bd"] = ":person_in_manual_wheelchair_tone1:", + ["\U0001f9d1\U0001f3fc\u200d\U0001f9bd"] = ":person_in_manual_wheelchair_tone2:", + ["\U0001f9d1\U0001f3fd\u200d\U0001f9bd"] = ":person_in_manual_wheelchair_tone3:", + ["\U0001f9d1\U0001f3fe\u200d\U0001f9bd"] = ":person_in_manual_wheelchair_tone4:", + ["\U0001f9d1\U0001f3ff\u200d\U0001f9bd"] = ":person_in_manual_wheelchair_tone5:", + ["\U0001f9d1\u200d\U0001f9bd"] = ":person_in_manual_wheelchair:", + ["\U0001f9d1\U0001f3fb\u200d\U0001f9bc\u200d\u27a1\ufe0f"] = ":person_in_motorized_wheelchair_facing_right_tone1:", + ["\U0001f9d1\U0001f3fc\u200d\U0001f9bc\u200d\u27a1\ufe0f"] = ":person_in_motorized_wheelchair_facing_right_tone2:", + ["\U0001f9d1\U0001f3fd\u200d\U0001f9bc\u200d\u27a1\ufe0f"] = ":person_in_motorized_wheelchair_facing_right_tone3:", + ["\U0001f9d1\U0001f3fe\u200d\U0001f9bc\u200d\u27a1\ufe0f"] = ":person_in_motorized_wheelchair_facing_right_tone4:", + ["\U0001f9d1\U0001f3ff\u200d\U0001f9bc\u200d\u27a1\ufe0f"] = ":person_in_motorized_wheelchair_facing_right_tone5:", + ["\U0001f9d1\u200d\U0001f9bc\u200d\u27a1\ufe0f"] = ":person_in_motorized_wheelchair_facing_right:", + ["\U0001f9d1\U0001f3fb\u200d\U0001f9bc"] = ":person_in_motorized_wheelchair_tone1:", + ["\U0001f9d1\U0001f3fc\u200d\U0001f9bc"] = ":person_in_motorized_wheelchair_tone2:", + ["\U0001f9d1\U0001f3fd\u200d\U0001f9bc"] = ":person_in_motorized_wheelchair_tone3:", + ["\U0001f9d1\U0001f3fe\u200d\U0001f9bc"] = ":person_in_motorized_wheelchair_tone4:", + ["\U0001f9d1\U0001f3ff\u200d\U0001f9bc"] = ":person_in_motorized_wheelchair_tone5:", + ["\U0001f9d1\u200d\U0001f9bc"] = ":person_in_motorized_wheelchair:", + ["\U0001f9d6\U0001f3fb"] = ":person_in_steamy_room_tone1:", + ["\U0001f9d6\U0001f3fc"] = ":person_in_steamy_room_tone2:", + ["\U0001f9d6\U0001f3fd"] = ":person_in_steamy_room_tone3:", + ["\U0001f9d6\U0001f3fe"] = ":person_in_steamy_room_tone4:", + ["\U0001f9d6\U0001f3ff"] = ":person_in_steamy_room_tone5:", + ["\U0001f9d6"] = ":person_in_steamy_room:", + ["\U0001f935\U0001f3fb"] = ":person_in_tuxedo_tone1:", + ["\U0001f935\U0001f3fc"] = ":person_in_tuxedo_tone2:", + ["\U0001f935\U0001f3fd"] = ":person_in_tuxedo_tone3:", + ["\U0001f935\U0001f3fe"] = ":person_in_tuxedo_tone4:", + ["\U0001f935\U0001f3ff"] = ":person_in_tuxedo_tone5:", + ["\U0001f935"] = ":person_in_tuxedo:", + ["\U0001f939\U0001f3fb"] = ":person_juggling_tone1:", + ["\U0001f939\U0001f3fc"] = ":person_juggling_tone2:", + ["\U0001f939\U0001f3fd"] = ":person_juggling_tone3:", + ["\U0001f939\U0001f3fe"] = ":person_juggling_tone4:", + ["\U0001f939\U0001f3ff"] = ":person_juggling_tone5:", + ["\U0001f939"] = ":person_juggling:", + ["\U0001f9ce\U0001f3fb\u200d\u27a1\ufe0f"] = ":person_kneeling_facing_right_tone1:", + ["\U0001f9ce\U0001f3fc\u200d\u27a1\ufe0f"] = ":person_kneeling_facing_right_tone2:", + ["\U0001f9ce\U0001f3fd\u200d\u27a1\ufe0f"] = ":person_kneeling_facing_right_tone3:", + ["\U0001f9ce\U0001f3fe\u200d\u27a1\ufe0f"] = ":person_kneeling_facing_right_tone4:", + ["\U0001f9ce\U0001f3ff\u200d\u27a1\ufe0f"] = ":person_kneeling_facing_right_tone5:", + ["\U0001f9ce\u200d\u27a1\ufe0f"] = ":person_kneeling_facing_right:", + ["\U0001f9ce\U0001f3fb"] = ":person_kneeling_tone1:", + ["\U0001f9ce\U0001f3fc"] = ":person_kneeling_tone2:", + ["\U0001f9ce\U0001f3fd"] = ":person_kneeling_tone3:", + ["\U0001f9ce\U0001f3fe"] = ":person_kneeling_tone4:", + ["\U0001f9ce\U0001f3ff"] = ":person_kneeling_tone5:", + ["\U0001f9ce"] = ":person_kneeling:", + ["\U0001f3cb\U0001f3fb"] = ":person_lifting_weights_tone1:", + ["\U0001f3cb\U0001f3fc"] = ":person_lifting_weights_tone2:", + ["\U0001f3cb\U0001f3fd"] = ":person_lifting_weights_tone3:", + ["\U0001f3cb\U0001f3fe"] = ":person_lifting_weights_tone4:", + ["\U0001f3cb\U0001f3ff"] = ":person_lifting_weights_tone5:", + ["\U0001f3cb\ufe0f"] = ":person_lifting_weights:", + ["\U0001f3cb"] = ":person_lifting_weights:", + ["\U0001f6b5\U0001f3fb"] = ":person_mountain_biking_tone1:", + ["\U0001f6b5\U0001f3fc"] = ":person_mountain_biking_tone2:", + ["\U0001f6b5\U0001f3fd"] = ":person_mountain_biking_tone3:", + ["\U0001f6b5\U0001f3fe"] = ":person_mountain_biking_tone4:", + ["\U0001f6b5\U0001f3ff"] = ":person_mountain_biking_tone5:", + ["\U0001f6b5"] = ":person_mountain_biking:", + ["\U0001f93e\U0001f3fb"] = ":person_playing_handball_tone1:", + ["\U0001f93e\U0001f3fc"] = ":person_playing_handball_tone2:", + ["\U0001f93e\U0001f3fd"] = ":person_playing_handball_tone3:", + ["\U0001f93e\U0001f3fe"] = ":person_playing_handball_tone4:", + ["\U0001f93e\U0001f3ff"] = ":person_playing_handball_tone5:", + ["\U0001f93e"] = ":person_playing_handball:", + ["\U0001f93d\U0001f3fb"] = ":person_playing_water_polo_tone1:", + ["\U0001f93d\U0001f3fc"] = ":person_playing_water_polo_tone2:", + ["\U0001f93d\U0001f3fd"] = ":person_playing_water_polo_tone3:", + ["\U0001f93d\U0001f3fe"] = ":person_playing_water_polo_tone4:", + ["\U0001f93d\U0001f3ff"] = ":person_playing_water_polo_tone5:", + ["\U0001f93d"] = ":person_playing_water_polo:", + ["\U0001f64e\U0001f3fb"] = ":person_pouting_tone1:", + ["\U0001f64e\U0001f3fc"] = ":person_pouting_tone2:", + ["\U0001f64e\U0001f3fd"] = ":person_pouting_tone3:", + ["\U0001f64e\U0001f3fe"] = ":person_pouting_tone4:", + ["\U0001f64e\U0001f3ff"] = ":person_pouting_tone5:", + ["\U0001f64e"] = ":person_pouting:", + ["\U0001f64b\U0001f3fb"] = ":person_raising_hand_tone1:", + ["\U0001f64b\U0001f3fc"] = ":person_raising_hand_tone2:", + ["\U0001f64b\U0001f3fd"] = ":person_raising_hand_tone3:", + ["\U0001f64b\U0001f3fe"] = ":person_raising_hand_tone4:", + ["\U0001f64b\U0001f3ff"] = ":person_raising_hand_tone5:", + ["\U0001f64b"] = ":person_raising_hand:", + ["\U0001f9d1\u200d\U0001f9b0"] = ":person_red_hair:", + ["\U0001f6a3\U0001f3fb"] = ":person_rowing_boat_tone1:", + ["\U0001f6a3\U0001f3fc"] = ":person_rowing_boat_tone2:", + ["\U0001f6a3\U0001f3fd"] = ":person_rowing_boat_tone3:", + ["\U0001f6a3\U0001f3fe"] = ":person_rowing_boat_tone4:", + ["\U0001f6a3\U0001f3ff"] = ":person_rowing_boat_tone5:", + ["\U0001f6a3"] = ":person_rowing_boat:", + ["\U0001f3c3\U0001f3fb\u200d\u27a1\ufe0f"] = ":person_running_facing_right_tone1:", + ["\U0001f3c3\U0001f3fc\u200d\u27a1\ufe0f"] = ":person_running_facing_right_tone2:", + ["\U0001f3c3\U0001f3fd\u200d\u27a1\ufe0f"] = ":person_running_facing_right_tone3:", + ["\U0001f3c3\U0001f3fe\u200d\u27a1\ufe0f"] = ":person_running_facing_right_tone4:", + ["\U0001f3c3\U0001f3ff\u200d\u27a1\ufe0f"] = ":person_running_facing_right_tone5:", + ["\U0001f3c3\u200d\u27a1\ufe0f"] = ":person_running_facing_right:", + ["\U0001f3c3\U0001f3fb"] = ":person_running_tone1:", + ["\U0001f3c3\U0001f3fc"] = ":person_running_tone2:", + ["\U0001f3c3\U0001f3fd"] = ":person_running_tone3:", + ["\U0001f3c3\U0001f3fe"] = ":person_running_tone4:", + ["\U0001f3c3\U0001f3ff"] = ":person_running_tone5:", + ["\U0001f3c3"] = ":person_running:", + ["\U0001f937\U0001f3fb"] = ":person_shrugging_tone1:", + ["\U0001f937\U0001f3fc"] = ":person_shrugging_tone2:", + ["\U0001f937\U0001f3fd"] = ":person_shrugging_tone3:", + ["\U0001f937\U0001f3fe"] = ":person_shrugging_tone4:", + ["\U0001f937\U0001f3ff"] = ":person_shrugging_tone5:", + ["\U0001f937"] = ":person_shrugging:", + ["\U0001f9cd\U0001f3fb"] = ":person_standing_tone1:", + ["\U0001f9cd\U0001f3fc"] = ":person_standing_tone2:", + ["\U0001f9cd\U0001f3fd"] = ":person_standing_tone3:", + ["\U0001f9cd\U0001f3fe"] = ":person_standing_tone4:", + ["\U0001f9cd\U0001f3ff"] = ":person_standing_tone5:", + ["\U0001f9cd"] = ":person_standing:", + ["\U0001f3c4\U0001f3fb"] = ":person_surfing_tone1:", + ["\U0001f3c4\U0001f3fc"] = ":person_surfing_tone2:", + ["\U0001f3c4\U0001f3fd"] = ":person_surfing_tone3:", + ["\U0001f3c4\U0001f3fe"] = ":person_surfing_tone4:", + ["\U0001f3c4\U0001f3ff"] = ":person_surfing_tone5:", + ["\U0001f3c4"] = ":person_surfing:", + ["\U0001f3ca\U0001f3fb"] = ":person_swimming_tone1:", + ["\U0001f3ca\U0001f3fc"] = ":person_swimming_tone2:", + ["\U0001f3ca\U0001f3fd"] = ":person_swimming_tone3:", + ["\U0001f3ca\U0001f3fe"] = ":person_swimming_tone4:", + ["\U0001f3ca\U0001f3ff"] = ":person_swimming_tone5:", + ["\U0001f3ca"] = ":person_swimming:", + ["\U0001f481\U0001f3fb"] = ":person_tipping_hand_tone1:", + ["\U0001f481\U0001f3fc"] = ":person_tipping_hand_tone2:", + ["\U0001f481\U0001f3fd"] = ":person_tipping_hand_tone3:", + ["\U0001f481\U0001f3fe"] = ":person_tipping_hand_tone4:", + ["\U0001f481\U0001f3ff"] = ":person_tipping_hand_tone5:", + ["\U0001f481"] = ":person_tipping_hand:", + ["\U0001f9d1\U0001f3fb\u200d\U0001f9b2"] = ":person_tone1_bald:", + ["\U0001f9d1\U0001f3fb\u200d\U0001f9b1"] = ":person_tone1_curly_hair:", + ["\U0001f9d1\U0001f3fb\u200d\U0001f9b0"] = ":person_tone1_red_hair:", + ["\U0001f9d1\U0001f3fb\u200d\U0001f9b3"] = ":person_tone1_white_hair:", + ["\U0001f9d1\U0001f3fc\u200d\U0001f9b2"] = ":person_tone2_bald:", + ["\U0001f9d1\U0001f3fc\u200d\U0001f9b1"] = ":person_tone2_curly_hair:", + ["\U0001f9d1\U0001f3fc\u200d\U0001f9b0"] = ":person_tone2_red_hair:", + ["\U0001f9d1\U0001f3fc\u200d\U0001f9b3"] = ":person_tone2_white_hair:", + ["\U0001f9d1\U0001f3fd\u200d\U0001f9b2"] = ":person_tone3_bald:", + ["\U0001f9d1\U0001f3fd\u200d\U0001f9b1"] = ":person_tone3_curly_hair:", + ["\U0001f9d1\U0001f3fd\u200d\U0001f9b0"] = ":person_tone3_red_hair:", + ["\U0001f9d1\U0001f3fd\u200d\U0001f9b3"] = ":person_tone3_white_hair:", + ["\U0001f9d1\U0001f3fe\u200d\U0001f9b2"] = ":person_tone4_bald:", + ["\U0001f9d1\U0001f3fe\u200d\U0001f9b1"] = ":person_tone4_curly_hair:", + ["\U0001f9d1\U0001f3fe\u200d\U0001f9b0"] = ":person_tone4_red_hair:", + ["\U0001f9d1\U0001f3fe\u200d\U0001f9b3"] = ":person_tone4_white_hair:", + ["\U0001f9d1\U0001f3ff\u200d\U0001f9b2"] = ":person_tone5_bald:", + ["\U0001f9d1\U0001f3ff\u200d\U0001f9b1"] = ":person_tone5_curly_hair:", + ["\U0001f9d1\U0001f3ff\u200d\U0001f9b0"] = ":person_tone5_red_hair:", + ["\U0001f9d1\U0001f3ff\u200d\U0001f9b3"] = ":person_tone5_white_hair:", + ["\U0001f6b6\U0001f3fb\u200d\u27a1\ufe0f"] = ":person_walking_facing_right_tone1:", + ["\U0001f6b6\U0001f3fc\u200d\u27a1\ufe0f"] = ":person_walking_facing_right_tone2:", + ["\U0001f6b6\U0001f3fd\u200d\u27a1\ufe0f"] = ":person_walking_facing_right_tone3:", + ["\U0001f6b6\U0001f3fe\u200d\u27a1\ufe0f"] = ":person_walking_facing_right_tone4:", + ["\U0001f6b6\U0001f3ff\u200d\u27a1\ufe0f"] = ":person_walking_facing_right_tone5:", + ["\U0001f6b6\u200d\u27a1\ufe0f"] = ":person_walking_facing_right:", + ["\U0001f6b6\U0001f3fb"] = ":person_walking_tone1:", + ["\U0001f6b6\U0001f3fc"] = ":person_walking_tone2:", + ["\U0001f6b6\U0001f3fd"] = ":person_walking_tone3:", + ["\U0001f6b6\U0001f3fe"] = ":person_walking_tone4:", + ["\U0001f6b6\U0001f3ff"] = ":person_walking_tone5:", + ["\U0001f6b6"] = ":person_walking:", + ["\U0001f473\U0001f3fb"] = ":person_wearing_turban_tone1:", + ["\U0001f473\U0001f3fc"] = ":person_wearing_turban_tone2:", + ["\U0001f473\U0001f3fd"] = ":person_wearing_turban_tone3:", + ["\U0001f473\U0001f3fe"] = ":person_wearing_turban_tone4:", + ["\U0001f473\U0001f3ff"] = ":person_wearing_turban_tone5:", + ["\U0001f473"] = ":person_wearing_turban:", + ["\U0001f9d1\u200d\U0001f9b3"] = ":person_white_hair:", + ["\U0001fac5\U0001f3fb"] = ":person_with_crown_tone1:", + ["\U0001fac5\U0001f3fc"] = ":person_with_crown_tone2:", + ["\U0001fac5\U0001f3fd"] = ":person_with_crown_tone3:", + ["\U0001fac5\U0001f3fe"] = ":person_with_crown_tone4:", + ["\U0001fac5\U0001f3ff"] = ":person_with_crown_tone5:", + ["\U0001fac5"] = ":person_with_crown:", + ["\U0001f9d1\U0001f3fb\u200d\U0001f9af"] = ":person_with_probing_cane_tone1:", + ["\U0001f9d1\U0001f3fc\u200d\U0001f9af"] = ":person_with_probing_cane_tone2:", + ["\U0001f9d1\U0001f3fd\u200d\U0001f9af"] = ":person_with_probing_cane_tone3:", + ["\U0001f9d1\U0001f3fe\u200d\U0001f9af"] = ":person_with_probing_cane_tone4:", + ["\U0001f9d1\U0001f3ff\u200d\U0001f9af"] = ":person_with_probing_cane_tone5:", + ["\U0001f9d1\u200d\U0001f9af"] = ":person_with_probing_cane:", + ["\U0001f470\U0001f3fb"] = ":person_with_veil_tone1:", + ["\U0001f470\U0001f3fc"] = ":person_with_veil_tone2:", + ["\U0001f470\U0001f3fd"] = ":person_with_veil_tone3:", + ["\U0001f470\U0001f3fe"] = ":person_with_veil_tone4:", + ["\U0001f470\U0001f3ff"] = ":person_with_veil_tone5:", + ["\U0001f470"] = ":person_with_veil:", + ["\U0001f9d1\U0001f3fb\u200d\U0001f9af\u200d\u27a1\ufe0f"] = ":person_with_white_cane_facing_right_tone1:", + ["\U0001f9d1\U0001f3fc\u200d\U0001f9af\u200d\u27a1\ufe0f"] = ":person_with_white_cane_facing_right_tone2:", + ["\U0001f9d1\U0001f3fd\u200d\U0001f9af\u200d\u27a1\ufe0f"] = ":person_with_white_cane_facing_right_tone3:", + ["\U0001f9d1\U0001f3fe\u200d\U0001f9af\u200d\u27a1\ufe0f"] = ":person_with_white_cane_facing_right_tone4:", + ["\U0001f9d1\U0001f3ff\u200d\U0001f9af\u200d\u27a1\ufe0f"] = ":person_with_white_cane_facing_right_tone5:", + ["\U0001f9d1\u200d\U0001f9af\u200d\u27a1\ufe0f"] = ":person_with_white_cane_facing_right:", + ["\U0001f9eb"] = ":petri_dish:", + ["\U0001f426\u200d\U0001f525"] = ":phoenix:", + ["\u26cf\ufe0f"] = ":pick:", + ["\u26cf"] = ":pick:", + ["\U0001f6fb"] = ":pickup_truck:", + ["\U0001f967"] = ":pie:", + ["\U0001f43d"] = ":pig_nose:", + ["\U0001f437"] = ":pig:", + ["\U0001f416"] = ":pig2:", + ["\U0001f48a"] = ":pill:", + ["\U0001f9d1\U0001f3fb\u200d\u2708\ufe0f"] = ":pilot_tone1:", + ["\U0001f9d1\U0001f3fc\u200d\u2708\ufe0f"] = ":pilot_tone2:", + ["\U0001f9d1\U0001f3fd\u200d\u2708\ufe0f"] = ":pilot_tone3:", + ["\U0001f9d1\U0001f3fe\u200d\u2708\ufe0f"] = ":pilot_tone4:", + ["\U0001f9d1\U0001f3ff\u200d\u2708\ufe0f"] = ":pilot_tone5:", + ["\U0001f9d1\u200d\u2708\ufe0f"] = ":pilot:", + ["\U0001fa85"] = ":piñata:", + ["\U0001f90c\U0001f3fb"] = ":pinched_fingers_tone1:", + ["\U0001f90c\U0001f3fc"] = ":pinched_fingers_tone2:", + ["\U0001f90c\U0001f3fd"] = ":pinched_fingers_tone3:", + ["\U0001f90c\U0001f3fe"] = ":pinched_fingers_tone4:", + ["\U0001f90c\U0001f3ff"] = ":pinched_fingers_tone5:", + ["\U0001f90c"] = ":pinched_fingers:", + ["\U0001f90f\U0001f3fb"] = ":pinching_hand_tone1:", + ["\U0001f90f\U0001f3fc"] = ":pinching_hand_tone2:", + ["\U0001f90f\U0001f3fd"] = ":pinching_hand_tone3:", + ["\U0001f90f\U0001f3fe"] = ":pinching_hand_tone4:", + ["\U0001f90f\U0001f3ff"] = ":pinching_hand_tone5:", + ["\U0001f90f"] = ":pinching_hand:", + ["\U0001f34d"] = ":pineapple:", + ["\U0001f3d3"] = ":ping_pong:", + ["\U0001fa77"] = ":pink_heart:", + ["\U0001f3f4\u200d\u2620\ufe0f"] = ":pirate_flag:", + ["\u2653"] = ":pisces:", + ["\U0001f355"] = ":pizza:", + ["\U0001faa7"] = ":placard:", + ["\U0001f6d0"] = ":place_of_worship:", + ["\u23ef\ufe0f"] = ":play_pause:", + ["\u23ef"] = ":play_pause:", + ["\U0001f6dd"] = ":playground_slide:", + ["\U0001f97a"] = ":pleading_face:", + ["\U0001faa0"] = ":plunger:", + ["\U0001f447\U0001f3fb"] = ":point_down_tone1:", + ["\U0001f447\U0001f3fc"] = ":point_down_tone2:", + ["\U0001f447\U0001f3fd"] = ":point_down_tone3:", + ["\U0001f447\U0001f3fe"] = ":point_down_tone4:", + ["\U0001f447\U0001f3ff"] = ":point_down_tone5:", + ["\U0001f447"] = ":point_down:", + ["\U0001f448\U0001f3fb"] = ":point_left_tone1:", + ["\U0001f448\U0001f3fc"] = ":point_left_tone2:", + ["\U0001f448\U0001f3fd"] = ":point_left_tone3:", + ["\U0001f448\U0001f3fe"] = ":point_left_tone4:", + ["\U0001f448\U0001f3ff"] = ":point_left_tone5:", + ["\U0001f448"] = ":point_left:", + ["\U0001f449\U0001f3fb"] = ":point_right_tone1:", + ["\U0001f449\U0001f3fc"] = ":point_right_tone2:", + ["\U0001f449\U0001f3fd"] = ":point_right_tone3:", + ["\U0001f449\U0001f3fe"] = ":point_right_tone4:", + ["\U0001f449\U0001f3ff"] = ":point_right_tone5:", + ["\U0001f449"] = ":point_right:", + ["\U0001f446\U0001f3fb"] = ":point_up_2_tone1:", + ["\U0001f446\U0001f3fc"] = ":point_up_2_tone2:", + ["\U0001f446\U0001f3fd"] = ":point_up_2_tone3:", + ["\U0001f446\U0001f3fe"] = ":point_up_2_tone4:", + ["\U0001f446\U0001f3ff"] = ":point_up_2_tone5:", + ["\U0001f446"] = ":point_up_2:", + ["\u261d\U0001f3fb"] = ":point_up_tone1:", + ["\u261d\U0001f3fc"] = ":point_up_tone2:", + ["\u261d\U0001f3fd"] = ":point_up_tone3:", + ["\u261d\U0001f3fe"] = ":point_up_tone4:", + ["\u261d\U0001f3ff"] = ":point_up_tone5:", + ["\u261d\ufe0f"] = ":point_up:", + ["\u261d"] = ":point_up:", + ["\U0001f43b\u200d\u2744\ufe0f"] = ":polar_bear:", + ["\U0001f693"] = ":police_car:", + ["\U0001f46e\U0001f3fb"] = ":police_officer_tone1:", + ["\U0001f46e\U0001f3fc"] = ":police_officer_tone2:", + ["\U0001f46e\U0001f3fd"] = ":police_officer_tone3:", + ["\U0001f46e\U0001f3fe"] = ":police_officer_tone4:", + ["\U0001f46e\U0001f3ff"] = ":police_officer_tone5:", + ["\U0001f46e"] = ":police_officer:", + ["\U0001f429"] = ":poodle:", + ["\U0001f4a9"] = ":poop:", + ["\U0001f37f"] = ":popcorn:", + ["\U0001f3e3"] = ":post_office:", + ["\U0001f4ef"] = ":postal_horn:", + ["\U0001f4ee"] = ":postbox:", + ["\U0001f6b0"] = ":potable_water:", + ["\U0001f954"] = ":potato:", + ["\U0001fab4"] = ":potted_plant:", + ["\U0001f45d"] = ":pouch:", + ["\U0001f357"] = ":poultry_leg:", + ["\U0001f4b7"] = ":pound:", + ["\U0001fad7"] = ":pouring_liquid:", + ["\U0001f63e"] = ":pouting_cat:", + ["\U0001f64f\U0001f3fb"] = ":pray_tone1:", + ["\U0001f64f\U0001f3fc"] = ":pray_tone2:", + ["\U0001f64f\U0001f3fd"] = ":pray_tone3:", + ["\U0001f64f\U0001f3fe"] = ":pray_tone4:", + ["\U0001f64f\U0001f3ff"] = ":pray_tone5:", + ["\U0001f64f"] = ":pray:", + ["\U0001f4ff"] = ":prayer_beads:", + ["\U0001fac3\U0001f3fb"] = ":pregnant_man_tone1:", + ["\U0001fac3\U0001f3fc"] = ":pregnant_man_tone2:", + ["\U0001fac3\U0001f3fd"] = ":pregnant_man_tone3:", + ["\U0001fac3\U0001f3fe"] = ":pregnant_man_tone4:", + ["\U0001fac3\U0001f3ff"] = ":pregnant_man_tone5:", + ["\U0001fac3"] = ":pregnant_man:", + ["\U0001fac4\U0001f3fb"] = ":pregnant_person_tone1:", + ["\U0001fac4\U0001f3fc"] = ":pregnant_person_tone2:", + ["\U0001fac4\U0001f3fd"] = ":pregnant_person_tone3:", + ["\U0001fac4\U0001f3fe"] = ":pregnant_person_tone4:", + ["\U0001fac4\U0001f3ff"] = ":pregnant_person_tone5:", + ["\U0001fac4"] = ":pregnant_person:", + ["\U0001f930\U0001f3fb"] = ":pregnant_woman_tone1:", + ["\U0001f930\U0001f3fc"] = ":pregnant_woman_tone2:", + ["\U0001f930\U0001f3fd"] = ":pregnant_woman_tone3:", + ["\U0001f930\U0001f3fe"] = ":pregnant_woman_tone4:", + ["\U0001f930\U0001f3ff"] = ":pregnant_woman_tone5:", + ["\U0001f930"] = ":pregnant_woman:", + ["\U0001f968"] = ":pretzel:", + ["\U0001f934\U0001f3fb"] = ":prince_tone1:", + ["\U0001f934\U0001f3fc"] = ":prince_tone2:", + ["\U0001f934\U0001f3fd"] = ":prince_tone3:", + ["\U0001f934\U0001f3fe"] = ":prince_tone4:", + ["\U0001f934\U0001f3ff"] = ":prince_tone5:", + ["\U0001f934"] = ":prince:", + ["\U0001f478\U0001f3fb"] = ":princess_tone1:", + ["\U0001f478\U0001f3fc"] = ":princess_tone2:", + ["\U0001f478\U0001f3fd"] = ":princess_tone3:", + ["\U0001f478\U0001f3fe"] = ":princess_tone4:", + ["\U0001f478\U0001f3ff"] = ":princess_tone5:", + ["\U0001f478"] = ":princess:", + ["\U0001f5a8\ufe0f"] = ":printer:", + ["\U0001f5a8"] = ":printer:", + ["\U0001f9af"] = ":probing_cane:", + ["\U0001f4fd\ufe0f"] = ":projector:", + ["\U0001f4fd"] = ":projector:", + ["\U0001f44a\U0001f3fb"] = ":punch_tone1:", + ["\U0001f44a\U0001f3fc"] = ":punch_tone2:", + ["\U0001f44a\U0001f3fd"] = ":punch_tone3:", + ["\U0001f44a\U0001f3fe"] = ":punch_tone4:", + ["\U0001f44a\U0001f3ff"] = ":punch_tone5:", + ["\U0001f44a"] = ":punch:", + ["\U0001f7e3"] = ":purple_circle:", + ["\U0001f49c"] = ":purple_heart:", + ["\U0001f7ea"] = ":purple_square:", + ["\U0001f45b"] = ":purse:", + ["\U0001f4cc"] = ":pushpin:", + ["\U0001f6ae"] = ":put_litter_in_its_place:", + ["\u2753"] = ":question:", + ["\U0001f430"] = ":rabbit:", + ["\U0001f407"] = ":rabbit2:", + ["\U0001f99d"] = ":raccoon:", + ["\U0001f3ce\ufe0f"] = ":race_car:", + ["\U0001f3ce"] = ":race_car:", + ["\U0001f40e"] = ":racehorse:", + ["\U0001f518"] = ":radio_button:", + ["\U0001f4fb"] = ":radio:", + ["\u2622\ufe0f"] = ":radioactive:", + ["\u2622"] = ":radioactive:", + ["\U0001f621"] = ":rage:", + ["\U0001f683"] = ":railway_car:", + ["\U0001f6e4\ufe0f"] = ":railway_track:", + ["\U0001f6e4"] = ":railway_track:", + ["\U0001f3f3\ufe0f\u200d\U0001f308"] = ":rainbow_flag:", + ["\U0001f308"] = ":rainbow:", + ["\U0001f91a\U0001f3fb"] = ":raised_back_of_hand_tone1:", + ["\U0001f91a\U0001f3fc"] = ":raised_back_of_hand_tone2:", + ["\U0001f91a\U0001f3fd"] = ":raised_back_of_hand_tone3:", + ["\U0001f91a\U0001f3fe"] = ":raised_back_of_hand_tone4:", + ["\U0001f91a\U0001f3ff"] = ":raised_back_of_hand_tone5:", + ["\U0001f91a"] = ":raised_back_of_hand:", + ["\u270b\U0001f3fb"] = ":raised_hand_tone1:", + ["\u270b\U0001f3fc"] = ":raised_hand_tone2:", + ["\u270b\U0001f3fd"] = ":raised_hand_tone3:", + ["\u270b\U0001f3fe"] = ":raised_hand_tone4:", + ["\u270b\U0001f3ff"] = ":raised_hand_tone5:", + ["\u270b"] = ":raised_hand:", + ["\U0001f64c\U0001f3fb"] = ":raised_hands_tone1:", + ["\U0001f64c\U0001f3fc"] = ":raised_hands_tone2:", + ["\U0001f64c\U0001f3fd"] = ":raised_hands_tone3:", + ["\U0001f64c\U0001f3fe"] = ":raised_hands_tone4:", + ["\U0001f64c\U0001f3ff"] = ":raised_hands_tone5:", + ["\U0001f64c"] = ":raised_hands:", + ["\U0001f40f"] = ":ram:", + ["\U0001f35c"] = ":ramen:", + ["\U0001f400"] = ":rat:", + ["\U0001fa92"] = ":razor:", + ["\U0001f9fe"] = ":receipt:", + ["\u23fa\ufe0f"] = ":record_button:", + ["\u23fa"] = ":record_button:", + ["\u267b\ufe0f"] = ":recycle:", + ["\u267b"] = ":recycle:", + ["\U0001f697"] = ":red_car:", + ["\U0001f534"] = ":red_circle:", + ["\U0001f9e7"] = ":red_envelope:", + ["\U0001f7e5"] = ":red_square:", + ["\U0001f1e6"] = ":regional_indicator_a:", + ["\U0001f1e7"] = ":regional_indicator_b:", + ["\U0001f1e8"] = ":regional_indicator_c:", + ["\U0001f1e9"] = ":regional_indicator_d:", + ["\U0001f1ea"] = ":regional_indicator_e:", + ["\U0001f1eb"] = ":regional_indicator_f:", + ["\U0001f1ec"] = ":regional_indicator_g:", + ["\U0001f1ed"] = ":regional_indicator_h:", + ["\U0001f1ee"] = ":regional_indicator_i:", + ["\U0001f1ef"] = ":regional_indicator_j:", + ["\U0001f1f0"] = ":regional_indicator_k:", + ["\U0001f1f1"] = ":regional_indicator_l:", + ["\U0001f1f2"] = ":regional_indicator_m:", + ["\U0001f1f3"] = ":regional_indicator_n:", + ["\U0001f1f4"] = ":regional_indicator_o:", + ["\U0001f1f5"] = ":regional_indicator_p:", + ["\U0001f1f6"] = ":regional_indicator_q:", + ["\U0001f1f7"] = ":regional_indicator_r:", + ["\U0001f1f8"] = ":regional_indicator_s:", + ["\U0001f1f9"] = ":regional_indicator_t:", + ["\U0001f1fa"] = ":regional_indicator_u:", + ["\U0001f1fb"] = ":regional_indicator_v:", + ["\U0001f1fc"] = ":regional_indicator_w:", + ["\U0001f1fd"] = ":regional_indicator_x:", + ["\U0001f1fe"] = ":regional_indicator_y:", + ["\U0001f1ff"] = ":regional_indicator_z:", + ["\u00ae\ufe0f"] = ":registered:", + ["\u00ae"] = ":registered:", + ["\u263a\ufe0f"] = ":relaxed:", + ["\u263a"] = ":relaxed:", + ["\U0001f60c"] = ":relieved:", + ["\U0001f397\ufe0f"] = ":reminder_ribbon:", + ["\U0001f397"] = ":reminder_ribbon:", + ["\U0001f502"] = ":repeat_one:", + ["\U0001f501"] = ":repeat:", + ["\U0001f6bb"] = ":restroom:", + ["\U0001f49e"] = ":revolving_hearts:", + ["\u23ea"] = ":rewind:", + ["\U0001f98f"] = ":rhino:", + ["\U0001f380"] = ":ribbon:", + ["\U0001f359"] = ":rice_ball:", + ["\U0001f358"] = ":rice_cracker:", + ["\U0001f391"] = ":rice_scene:", + ["\U0001f35a"] = ":rice:", + ["\U0001f91c\U0001f3fb"] = ":right_facing_fist_tone1:", + ["\U0001f91c\U0001f3fc"] = ":right_facing_fist_tone2:", + ["\U0001f91c\U0001f3fd"] = ":right_facing_fist_tone3:", + ["\U0001f91c\U0001f3fe"] = ":right_facing_fist_tone4:", + ["\U0001f91c\U0001f3ff"] = ":right_facing_fist_tone5:", + ["\U0001f91c"] = ":right_facing_fist:", + ["\U0001faf1\U0001f3fb"] = ":rightwards_hand_tone1:", + ["\U0001faf1\U0001f3fc"] = ":rightwards_hand_tone2:", + ["\U0001faf1\U0001f3fd"] = ":rightwards_hand_tone3:", + ["\U0001faf1\U0001f3fe"] = ":rightwards_hand_tone4:", + ["\U0001faf1\U0001f3ff"] = ":rightwards_hand_tone5:", + ["\U0001faf1"] = ":rightwards_hand:", + ["\U0001faf8\U0001f3fb"] = ":rightwards_pushing_hand_tone1:", + ["\U0001faf8\U0001f3fc"] = ":rightwards_pushing_hand_tone2:", + ["\U0001faf8\U0001f3fd"] = ":rightwards_pushing_hand_tone3:", + ["\U0001faf8\U0001f3fe"] = ":rightwards_pushing_hand_tone4:", + ["\U0001faf8\U0001f3ff"] = ":rightwards_pushing_hand_tone5:", + ["\U0001faf8"] = ":rightwards_pushing_hand:", + ["\U0001f6df"] = ":ring_buoy:", + ["\U0001f48d"] = ":ring:", + ["\U0001fa90"] = ":ringed_planet:", + ["\U0001f916"] = ":robot:", + ["\U0001faa8"] = ":rock:", + ["\U0001f680"] = ":rocket:", + ["\U0001f923"] = ":rofl:", + ["\U0001f9fb"] = ":roll_of_paper:", + ["\U0001f3a2"] = ":roller_coaster:", + ["\U0001f6fc"] = ":roller_skate:", + ["\U0001f644"] = ":rolling_eyes:", + ["\U0001f413"] = ":rooster:", + ["\U0001f339"] = ":rose:", + ["\U0001f3f5\ufe0f"] = ":rosette:", + ["\U0001f3f5"] = ":rosette:", + ["\U0001f6a8"] = ":rotating_light:", + ["\U0001f4cd"] = ":round_pushpin:", + ["\U0001f3c9"] = ":rugby_football:", + ["\U0001f3bd"] = ":running_shirt_with_sash:", + ["\U0001f202\ufe0f"] = ":sa:", + ["\U0001f202"] = ":sa:", + ["\U0001f9f7"] = ":safety_pin:", + ["\U0001f9ba"] = ":safety_vest:", + ["\u2650"] = ":sagittarius:", + ["\u26f5"] = ":sailboat:", + ["\U0001f376"] = ":sake:", + ["\U0001f957"] = ":salad:", + ["\U0001f9c2"] = ":salt:", + ["\U0001fae1"] = ":saluting_face:", + ["\U0001f461"] = ":sandal:", + ["\U0001f96a"] = ":sandwich:", + ["\U0001f385\U0001f3fb"] = ":santa_tone1:", + ["\U0001f385\U0001f3fc"] = ":santa_tone2:", + ["\U0001f385\U0001f3fd"] = ":santa_tone3:", + ["\U0001f385\U0001f3fe"] = ":santa_tone4:", + ["\U0001f385\U0001f3ff"] = ":santa_tone5:", + ["\U0001f385"] = ":santa:", + ["\U0001f97b"] = ":sari:", + ["\U0001f6f0\ufe0f"] = ":satellite_orbital:", + ["\U0001f6f0"] = ":satellite_orbital:", + ["\U0001f4e1"] = ":satellite:", + ["\U0001f995"] = ":sauropod:", + ["\U0001f3b7"] = ":saxophone:", + ["\u2696\ufe0f"] = ":scales:", + ["\u2696"] = ":scales:", + ["\U0001f9e3"] = ":scarf:", + ["\U0001f392"] = ":school_satchel:", + ["\U0001f3eb"] = ":school:", + ["\U0001f9d1\U0001f3fb\u200d\U0001f52c"] = ":scientist_tone1:", + ["\U0001f9d1\U0001f3fc\u200d\U0001f52c"] = ":scientist_tone2:", + ["\U0001f9d1\U0001f3fd\u200d\U0001f52c"] = ":scientist_tone3:", + ["\U0001f9d1\U0001f3fe\u200d\U0001f52c"] = ":scientist_tone4:", + ["\U0001f9d1\U0001f3ff\u200d\U0001f52c"] = ":scientist_tone5:", + ["\U0001f9d1\u200d\U0001f52c"] = ":scientist:", + ["\u2702\ufe0f"] = ":scissors:", + ["\u2702"] = ":scissors:", + ["\U0001f6f4"] = ":scooter:", + ["\U0001f982"] = ":scorpion:", + ["\u264f"] = ":scorpius:", + ["\U0001f3f4\U000e0067\U000e0062\U000e0073\U000e0063\U000e0074\U000e007f"] = ":scotland:", + ["\U0001f640"] = ":scream_cat:", + ["\U0001f631"] = ":scream:", + ["\U0001fa9b"] = ":screwdriver:", + ["\U0001f4dc"] = ":scroll:", + ["\U0001f9ad"] = ":seal:", + ["\U0001f4ba"] = ":seat:", + ["\U0001f948"] = ":second_place:", + ["\u3299\ufe0f"] = ":secret:", + ["\u3299"] = ":secret:", + ["\U0001f648"] = ":see_no_evil:", + ["\U0001f331"] = ":seedling:", + ["\U0001f933\U0001f3fb"] = ":selfie_tone1:", + ["\U0001f933\U0001f3fc"] = ":selfie_tone2:", + ["\U0001f933\U0001f3fd"] = ":selfie_tone3:", + ["\U0001f933\U0001f3fe"] = ":selfie_tone4:", + ["\U0001f933\U0001f3ff"] = ":selfie_tone5:", + ["\U0001f933"] = ":selfie:", + ["\U0001f415\u200d\U0001f9ba"] = ":service_dog:", + ["\u0037\ufe0f\u20e3"] = ":seven:", + ["\u0037\u20e3"] = ":seven:", + ["\U0001faa1"] = ":sewing_needle:", + ["\U0001fae8"] = ":shaking_face:", + ["\U0001f958"] = ":shallow_pan_of_food:", + ["\u2618\ufe0f"] = ":shamrock:", + ["\u2618"] = ":shamrock:", + ["\U0001f988"] = ":shark:", + ["\U0001f367"] = ":shaved_ice:", + ["\U0001f411"] = ":sheep:", + ["\U0001f41a"] = ":shell:", + ["\U0001f6e1\ufe0f"] = ":shield:", + ["\U0001f6e1"] = ":shield:", + ["\u26e9\ufe0f"] = ":shinto_shrine:", + ["\u26e9"] = ":shinto_shrine:", + ["\U0001f6a2"] = ":ship:", + ["\U0001f455"] = ":shirt:", + ["\U0001f6cd\ufe0f"] = ":shopping_bags:", + ["\U0001f6cd"] = ":shopping_bags:", + ["\U0001f6d2"] = ":shopping_cart:", + ["\U0001fa73"] = ":shorts:", + ["\U0001f6bf"] = ":shower:", + ["\U0001f990"] = ":shrimp:", + ["\U0001f92b"] = ":shushing_face:", + ["\U0001f4f6"] = ":signal_strength:", + ["\U0001f9d1\U0001f3fb\u200d\U0001f3a4"] = ":singer_tone1:", + ["\U0001f9d1\U0001f3fc\u200d\U0001f3a4"] = ":singer_tone2:", + ["\U0001f9d1\U0001f3fd\u200d\U0001f3a4"] = ":singer_tone3:", + ["\U0001f9d1\U0001f3fe\u200d\U0001f3a4"] = ":singer_tone4:", + ["\U0001f9d1\U0001f3ff\u200d\U0001f3a4"] = ":singer_tone5:", + ["\U0001f9d1\u200d\U0001f3a4"] = ":singer:", + ["\U0001f52f"] = ":six_pointed_star:", + ["\u0036\ufe0f\u20e3"] = ":six:", + ["\u0036\u20e3"] = ":six:", + ["\U0001f6f9"] = ":skateboard:", + ["\U0001f3bf"] = ":ski:", + ["\u26f7\ufe0f"] = ":skier:", + ["\u26f7"] = ":skier:", + ["\u2620\ufe0f"] = ":skull_crossbones:", + ["\u2620"] = ":skull_crossbones:", + ["\U0001f480"] = ":skull:", + ["\U0001f9a8"] = ":skunk:", + ["\U0001f6f7"] = ":sled:", + ["\U0001f6cc"] = ":sleeping_accommodation:", + ["\U0001f634"] = ":sleeping:", + ["\U0001f62a"] = ":sleepy:", + ["\U0001f641"] = ":slight_frown:", + ["\U0001f642"] = ":slight_smile:", + ["\U0001f3b0"] = ":slot_machine:", + ["\U0001f9a5"] = ":sloth:", + ["\U0001f539"] = ":small_blue_diamond:", + ["\U0001f538"] = ":small_orange_diamond:", + ["\U0001f53b"] = ":small_red_triangle_down:", + ["\U0001f53a"] = ":small_red_triangle:", + ["\U0001f638"] = ":smile_cat:", + ["\U0001f604"] = ":smile:", + ["\U0001f63a"] = ":smiley_cat:", + ["\U0001f603"] = ":smiley:", + ["\U0001f970"] = ":smiling_face_with_3_hearts:", + ["\U0001f972"] = ":smiling_face_with_tear:", + ["\U0001f608"] = ":smiling_imp:", + ["\U0001f63c"] = ":smirk_cat:", + ["\U0001f60f"] = ":smirk:", + ["\U0001f6ac"] = ":smoking:", + ["\U0001f40c"] = ":snail:", + ["\U0001f40d"] = ":snake:", + ["\U0001f927"] = ":sneezing_face:", + ["\U0001f3c2\U0001f3fb"] = ":snowboarder_tone1:", + ["\U0001f3c2\U0001f3fc"] = ":snowboarder_tone2:", + ["\U0001f3c2\U0001f3fd"] = ":snowboarder_tone3:", + ["\U0001f3c2\U0001f3fe"] = ":snowboarder_tone4:", + ["\U0001f3c2\U0001f3ff"] = ":snowboarder_tone5:", + ["\U0001f3c2"] = ":snowboarder:", + ["\u2744\ufe0f"] = ":snowflake:", + ["\u2744"] = ":snowflake:", + ["\u26c4"] = ":snowman:", + ["\u2603\ufe0f"] = ":snowman2:", + ["\u2603"] = ":snowman2:", + ["\U0001f9fc"] = ":soap:", + ["\U0001f62d"] = ":sob:", + ["\u26bd"] = ":soccer:", + ["\U0001f9e6"] = ":socks:", + ["\U0001f94e"] = ":softball:", + ["\U0001f51c"] = ":soon:", + ["\U0001f198"] = ":sos:", + ["\U0001f509"] = ":sound:", + ["\U0001f47e"] = ":space_invader:", + ["\u2660\ufe0f"] = ":spades:", + ["\u2660"] = ":spades:", + ["\U0001f35d"] = ":spaghetti:", + ["\u2747\ufe0f"] = ":sparkle:", + ["\u2747"] = ":sparkle:", + ["\U0001f387"] = ":sparkler:", + ["\u2728"] = ":sparkles:", + ["\U0001f496"] = ":sparkling_heart:", + ["\U0001f64a"] = ":speak_no_evil:", + ["\U0001f508"] = ":speaker:", + ["\U0001f5e3\ufe0f"] = ":speaking_head:", + ["\U0001f5e3"] = ":speaking_head:", + ["\U0001f4ac"] = ":speech_balloon:", + ["\U0001f5e8\ufe0f"] = ":speech_left:", + ["\U0001f5e8"] = ":speech_left:", + ["\U0001f6a4"] = ":speedboat:", + ["\U0001f578\ufe0f"] = ":spider_web:", + ["\U0001f578"] = ":spider_web:", + ["\U0001f577\ufe0f"] = ":spider:", + ["\U0001f577"] = ":spider:", + ["\U0001f9fd"] = ":sponge:", + ["\U0001f944"] = ":spoon:", + ["\U0001f9f4"] = ":squeeze_bottle:", + ["\U0001f991"] = ":squid:", + ["\U0001f3df\ufe0f"] = ":stadium:", + ["\U0001f3df"] = ":stadium:", + ["\u262a\ufe0f"] = ":star_and_crescent:", + ["\u262a"] = ":star_and_crescent:", + ["\u2721\ufe0f"] = ":star_of_david:", + ["\u2721"] = ":star_of_david:", + ["\U0001f929"] = ":star_struck:", + ["\u2b50"] = ":star:", + ["\U0001f31f"] = ":star2:", + ["\U0001f320"] = ":stars:", + ["\U0001f689"] = ":station:", + ["\U0001f5fd"] = ":statue_of_liberty:", + ["\U0001f682"] = ":steam_locomotive:", + ["\U0001fa7a"] = ":stethoscope:", + ["\U0001f372"] = ":stew:", + ["\u23f9\ufe0f"] = ":stop_button:", + ["\u23f9"] = ":stop_button:", + ["\u23f1\ufe0f"] = ":stopwatch:", + ["\u23f1"] = ":stopwatch:", + ["\U0001f4cf"] = ":straight_ruler:", + ["\U0001f353"] = ":strawberry:", + ["\U0001f61d"] = ":stuck_out_tongue_closed_eyes:", + ["\U0001f61c"] = ":stuck_out_tongue_winking_eye:", + ["\U0001f61b"] = ":stuck_out_tongue:", + ["\U0001f9d1\U0001f3fb\u200d\U0001f393"] = ":student_tone1:", + ["\U0001f9d1\U0001f3fc\u200d\U0001f393"] = ":student_tone2:", + ["\U0001f9d1\U0001f3fd\u200d\U0001f393"] = ":student_tone3:", + ["\U0001f9d1\U0001f3fe\u200d\U0001f393"] = ":student_tone4:", + ["\U0001f9d1\U0001f3ff\u200d\U0001f393"] = ":student_tone5:", + ["\U0001f9d1\u200d\U0001f393"] = ":student:", + ["\U0001f959"] = ":stuffed_flatbread:", + ["\U0001f31e"] = ":sun_with_face:", + ["\U0001f33b"] = ":sunflower:", + ["\U0001f60e"] = ":sunglasses:", + ["\u2600\ufe0f"] = ":sunny:", + ["\u2600"] = ":sunny:", + ["\U0001f304"] = ":sunrise_over_mountains:", + ["\U0001f305"] = ":sunrise:", + ["\U0001f9b8\U0001f3fb"] = ":superhero_tone1:", + ["\U0001f9b8\U0001f3fc"] = ":superhero_tone2:", + ["\U0001f9b8\U0001f3fd"] = ":superhero_tone3:", + ["\U0001f9b8\U0001f3fe"] = ":superhero_tone4:", + ["\U0001f9b8\U0001f3ff"] = ":superhero_tone5:", + ["\U0001f9b8"] = ":superhero:", + ["\U0001f9b9\U0001f3fb"] = ":supervillain_tone1:", + ["\U0001f9b9\U0001f3fc"] = ":supervillain_tone2:", + ["\U0001f9b9\U0001f3fd"] = ":supervillain_tone3:", + ["\U0001f9b9\U0001f3fe"] = ":supervillain_tone4:", + ["\U0001f9b9\U0001f3ff"] = ":supervillain_tone5:", + ["\U0001f9b9"] = ":supervillain:", + ["\U0001f363"] = ":sushi:", + ["\U0001f69f"] = ":suspension_railway:", + ["\U0001f9a2"] = ":swan:", + ["\U0001f4a6"] = ":sweat_drops:", + ["\U0001f605"] = ":sweat_smile:", + ["\U0001f613"] = ":sweat:", + ["\U0001f360"] = ":sweet_potato:", + ["\U0001f523"] = ":symbols:", + ["\U0001f54d"] = ":synagogue:", + ["\U0001f489"] = ":syringe:", + ["\U0001f996"] = ":t_rex:", + ["\U0001f32e"] = ":taco:", + ["\U0001f389"] = ":tada:", + ["\U0001f961"] = ":takeout_box:", + ["\U0001fad4"] = ":tamale:", + ["\U0001f38b"] = ":tanabata_tree:", + ["\U0001f34a"] = ":tangerine:", + ["\u2649"] = ":taurus:", + ["\U0001f695"] = ":taxi:", + ["\U0001f375"] = ":tea:", + ["\U0001f9d1\U0001f3fb\u200d\U0001f3eb"] = ":teacher_tone1:", + ["\U0001f9d1\U0001f3fc\u200d\U0001f3eb"] = ":teacher_tone2:", + ["\U0001f9d1\U0001f3fd\u200d\U0001f3eb"] = ":teacher_tone3:", + ["\U0001f9d1\U0001f3fe\u200d\U0001f3eb"] = ":teacher_tone4:", + ["\U0001f9d1\U0001f3ff\u200d\U0001f3eb"] = ":teacher_tone5:", + ["\U0001f9d1\u200d\U0001f3eb"] = ":teacher:", + ["\U0001fad6"] = ":teapot:", + ["\U0001f9d1\U0001f3fb\u200d\U0001f4bb"] = ":technologist_tone1:", + ["\U0001f9d1\U0001f3fc\u200d\U0001f4bb"] = ":technologist_tone2:", + ["\U0001f9d1\U0001f3fd\u200d\U0001f4bb"] = ":technologist_tone3:", + ["\U0001f9d1\U0001f3fe\u200d\U0001f4bb"] = ":technologist_tone4:", + ["\U0001f9d1\U0001f3ff\u200d\U0001f4bb"] = ":technologist_tone5:", + ["\U0001f9d1\u200d\U0001f4bb"] = ":technologist:", + ["\U0001f9f8"] = ":teddy_bear:", + ["\U0001f4de"] = ":telephone_receiver:", + ["\u260e\ufe0f"] = ":telephone:", + ["\u260e"] = ":telephone:", + ["\U0001f52d"] = ":telescope:", + ["\U0001f3be"] = ":tennis:", + ["\u26fa"] = ":tent:", + ["\U0001f9ea"] = ":test_tube:", + ["\U0001f912"] = ":thermometer_face:", + ["\U0001f321\ufe0f"] = ":thermometer:", + ["\U0001f321"] = ":thermometer:", + ["\U0001f914"] = ":thinking:", + ["\U0001f949"] = ":third_place:", + ["\U0001fa74"] = ":thong_sandal:", + ["\U0001f4ad"] = ":thought_balloon:", + ["\U0001f9f5"] = ":thread:", + ["\u0033\ufe0f\u20e3"] = ":three:", + ["\u0033\u20e3"] = ":three:", + ["\U0001f44e\U0001f3fb"] = ":thumbsdown_tone1:", + ["\U0001f44e\U0001f3fc"] = ":thumbsdown_tone2:", + ["\U0001f44e\U0001f3fd"] = ":thumbsdown_tone3:", + ["\U0001f44e\U0001f3fe"] = ":thumbsdown_tone4:", + ["\U0001f44e\U0001f3ff"] = ":thumbsdown_tone5:", + ["\U0001f44e"] = ":thumbsdown:", + ["\U0001f44d\U0001f3fb"] = ":thumbsup_tone1:", + ["\U0001f44d\U0001f3fc"] = ":thumbsup_tone2:", + ["\U0001f44d\U0001f3fd"] = ":thumbsup_tone3:", + ["\U0001f44d\U0001f3fe"] = ":thumbsup_tone4:", + ["\U0001f44d\U0001f3ff"] = ":thumbsup_tone5:", + ["\U0001f44d"] = ":thumbsup:", + ["\u26c8\ufe0f"] = ":thunder_cloud_rain:", + ["\u26c8"] = ":thunder_cloud_rain:", + ["\U0001f3ab"] = ":ticket:", + ["\U0001f39f\ufe0f"] = ":tickets:", + ["\U0001f39f"] = ":tickets:", + ["\U0001f42f"] = ":tiger:", + ["\U0001f405"] = ":tiger2:", + ["\u23f2\ufe0f"] = ":timer:", + ["\u23f2"] = ":timer:", + ["\U0001f62b"] = ":tired_face:", + ["\u2122\ufe0f"] = ":tm:", + ["\u2122"] = ":tm:", + ["\U0001f6bd"] = ":toilet:", + ["\U0001f5fc"] = ":tokyo_tower:", + ["\U0001f345"] = ":tomato:", + ["\U0001f445"] = ":tongue:", + ["\U0001f9f0"] = ":toolbox:", + ["\U0001f6e0\ufe0f"] = ":tools:", + ["\U0001f6e0"] = ":tools:", + ["\U0001f9b7"] = ":tooth:", + ["\U0001faa5"] = ":toothbrush:", + ["\U0001f51d"] = ":top:", + ["\U0001f3a9"] = ":tophat:", + ["\u23ed\ufe0f"] = ":track_next:", + ["\u23ed"] = ":track_next:", + ["\u23ee\ufe0f"] = ":track_previous:", + ["\u23ee"] = ":track_previous:", + ["\U0001f5b2\ufe0f"] = ":trackball:", + ["\U0001f5b2"] = ":trackball:", + ["\U0001f69c"] = ":tractor:", + ["\U0001f6a5"] = ":traffic_light:", + ["\U0001f68b"] = ":train:", + ["\U0001f686"] = ":train2:", + ["\U0001f68a"] = ":tram:", + ["\U0001f3f3\ufe0f\u200d\u26a7\ufe0f"] = ":transgender_flag:", + ["\u26a7"] = ":transgender_symbol:", + ["\U0001f6a9"] = ":triangular_flag_on_post:", + ["\U0001f4d0"] = ":triangular_ruler:", + ["\U0001f531"] = ":trident:", + ["\U0001f624"] = ":triumph:", + ["\U0001f9cc"] = ":troll:", + ["\U0001f68e"] = ":trolleybus:", + ["\U0001f3c6"] = ":trophy:", + ["\U0001f379"] = ":tropical_drink:", + ["\U0001f420"] = ":tropical_fish:", + ["\U0001f69a"] = ":truck:", + ["\U0001f3ba"] = ":trumpet:", + ["\U0001f337"] = ":tulip:", + ["\U0001f943"] = ":tumbler_glass:", + ["\U0001f983"] = ":turkey:", + ["\U0001f422"] = ":turtle:", + ["\U0001f4fa"] = ":tv:", + ["\U0001f500"] = ":twisted_rightwards_arrows:", + ["\U0001f495"] = ":two_hearts:", + ["\U0001f46c"] = ":two_men_holding_hands:", + ["\u0032\ufe0f\u20e3"] = ":two:", + ["\u0032\u20e3"] = ":two:", + ["\U0001f239"] = ":u5272:", + ["\U0001f234"] = ":u5408:", + ["\U0001f23a"] = ":u55b6:", + ["\U0001f22f"] = ":u6307:", + ["\U0001f237\ufe0f"] = ":u6708:", + ["\U0001f237"] = ":u6708:", + ["\U0001f236"] = ":u6709:", + ["\U0001f235"] = ":u6e80:", + ["\U0001f21a"] = ":u7121:", + ["\U0001f238"] = ":u7533:", + ["\U0001f232"] = ":u7981:", + ["\U0001f233"] = ":u7a7a:", + ["\u2614"] = ":umbrella:", + ["\u2602\ufe0f"] = ":umbrella2:", + ["\u2602"] = ":umbrella2:", + ["\U0001f612"] = ":unamused:", + ["\U0001f51e"] = ":underage:", + ["\U0001f984"] = ":unicorn:", + ["\U0001f1fa\U0001f1f3"] = ":united_nations:", + ["\U0001f513"] = ":unlock:", + ["\U0001f199"] = ":up:", + ["\U0001f643"] = ":upside_down:", + ["\u26b1\ufe0f"] = ":urn:", + ["\u26b1"] = ":urn:", + ["\u270c\U0001f3fb"] = ":v_tone1:", + ["\u270c\U0001f3fc"] = ":v_tone2:", + ["\u270c\U0001f3fd"] = ":v_tone3:", + ["\u270c\U0001f3fe"] = ":v_tone4:", + ["\u270c\U0001f3ff"] = ":v_tone5:", + ["\u270c\ufe0f"] = ":v:", + ["\u270c"] = ":v:", + ["\U0001f9db\U0001f3fb"] = ":vampire_tone1:", + ["\U0001f9db\U0001f3fc"] = ":vampire_tone2:", + ["\U0001f9db\U0001f3fd"] = ":vampire_tone3:", + ["\U0001f9db\U0001f3fe"] = ":vampire_tone4:", + ["\U0001f9db\U0001f3ff"] = ":vampire_tone5:", + ["\U0001f9db"] = ":vampire:", + ["\U0001f6a6"] = ":vertical_traffic_light:", + ["\U0001f4fc"] = ":vhs:", + ["\U0001f4f3"] = ":vibration_mode:", + ["\U0001f4f9"] = ":video_camera:", + ["\U0001f3ae"] = ":video_game:", + ["\U0001f3bb"] = ":violin:", + ["\u264d"] = ":virgo:", + ["\U0001f30b"] = ":volcano:", + ["\U0001f3d0"] = ":volleyball:", + ["\U0001f19a"] = ":vs:", + ["\U0001f596\U0001f3fb"] = ":vulcan_tone1:", + ["\U0001f596\U0001f3fc"] = ":vulcan_tone2:", + ["\U0001f596\U0001f3fd"] = ":vulcan_tone3:", + ["\U0001f596\U0001f3fe"] = ":vulcan_tone4:", + ["\U0001f596\U0001f3ff"] = ":vulcan_tone5:", + ["\U0001f596"] = ":vulcan:", + ["\U0001f9c7"] = ":waffle:", + ["\U0001f3f4\U000e0067\U000e0062\U000e0077\U000e006c\U000e0073\U000e007f"] = ":wales:", + ["\U0001f318"] = ":waning_crescent_moon:", + ["\U0001f316"] = ":waning_gibbous_moon:", + ["\u26a0\ufe0f"] = ":warning:", + ["\u26a0"] = ":warning:", + ["\U0001f5d1\ufe0f"] = ":wastebasket:", + ["\U0001f5d1"] = ":wastebasket:", + ["\u231a"] = ":watch:", + ["\U0001f403"] = ":water_buffalo:", + ["\U0001f349"] = ":watermelon:", + ["\U0001f44b\U0001f3fb"] = ":wave_tone1:", + ["\U0001f44b\U0001f3fc"] = ":wave_tone2:", + ["\U0001f44b\U0001f3fd"] = ":wave_tone3:", + ["\U0001f44b\U0001f3fe"] = ":wave_tone4:", + ["\U0001f44b\U0001f3ff"] = ":wave_tone5:", + ["\U0001f44b"] = ":wave:", + ["\u3030\ufe0f"] = ":wavy_dash:", + ["\u3030"] = ":wavy_dash:", + ["\U0001f312"] = ":waxing_crescent_moon:", + ["\U0001f314"] = ":waxing_gibbous_moon:", + ["\U0001f6be"] = ":wc:", + ["\U0001f629"] = ":weary:", + ["\U0001f492"] = ":wedding:", + ["\U0001f433"] = ":whale:", + ["\U0001f40b"] = ":whale2:", + ["\u2638\ufe0f"] = ":wheel_of_dharma:", + ["\u2638"] = ":wheel_of_dharma:", + ["\U0001f6de"] = ":wheel:", + ["\u267f"] = ":wheelchair:", + ["\u2705"] = ":white_check_mark:", + ["\u26aa"] = ":white_circle:", + ["\U0001f4ae"] = ":white_flower:", + ["\U0001f90d"] = ":white_heart:", + ["\u2b1c"] = ":white_large_square:", + ["\u25fd"] = ":white_medium_small_square:", + ["\u25fb\ufe0f"] = ":white_medium_square:", + ["\u25fb"] = ":white_medium_square:", + ["\u25ab\ufe0f"] = ":white_small_square:", + ["\u25ab"] = ":white_small_square:", + ["\U0001f533"] = ":white_square_button:", + ["\U0001f325\ufe0f"] = ":white_sun_cloud:", + ["\U0001f325"] = ":white_sun_cloud:", + ["\U0001f326\ufe0f"] = ":white_sun_rain_cloud:", + ["\U0001f326"] = ":white_sun_rain_cloud:", + ["\U0001f324\ufe0f"] = ":white_sun_small_cloud:", + ["\U0001f324"] = ":white_sun_small_cloud:", + ["\U0001f940"] = ":wilted_rose:", + ["\U0001f32c\ufe0f"] = ":wind_blowing_face:", + ["\U0001f32c"] = ":wind_blowing_face:", + ["\U0001f390"] = ":wind_chime:", + ["\U0001fa9f"] = ":window:", + ["\U0001f377"] = ":wine_glass:", + ["\U0001fabd"] = ":wing:", + ["\U0001f609"] = ":wink:", + ["\U0001f6dc"] = ":wireless:", + ["\U0001f43a"] = ":wolf:", + ["\U0001f46b"] = ":woman_and_man_holding_hands_tone5_tone4:", + ["\U0001f469\U0001f3fb\u200d\U0001f3a8"] = ":woman_artist_tone1:", + ["\U0001f469\U0001f3fc\u200d\U0001f3a8"] = ":woman_artist_tone2:", + ["\U0001f469\U0001f3fd\u200d\U0001f3a8"] = ":woman_artist_tone3:", + ["\U0001f469\U0001f3fe\u200d\U0001f3a8"] = ":woman_artist_tone4:", + ["\U0001f469\U0001f3ff\u200d\U0001f3a8"] = ":woman_artist_tone5:", + ["\U0001f469\u200d\U0001f3a8"] = ":woman_artist:", + ["\U0001f469\U0001f3fb\u200d\U0001f680"] = ":woman_astronaut_tone1:", + ["\U0001f469\U0001f3fc\u200d\U0001f680"] = ":woman_astronaut_tone2:", + ["\U0001f469\U0001f3fd\u200d\U0001f680"] = ":woman_astronaut_tone3:", + ["\U0001f469\U0001f3fe\u200d\U0001f680"] = ":woman_astronaut_tone4:", + ["\U0001f469\U0001f3ff\u200d\U0001f680"] = ":woman_astronaut_tone5:", + ["\U0001f469\u200d\U0001f680"] = ":woman_astronaut:", + ["\U0001f469\U0001f3fb\u200d\U0001f9b2"] = ":woman_bald_tone1:", + ["\U0001f469\U0001f3fc\u200d\U0001f9b2"] = ":woman_bald_tone2:", + ["\U0001f469\U0001f3fd\u200d\U0001f9b2"] = ":woman_bald_tone3:", + ["\U0001f469\U0001f3fe\u200d\U0001f9b2"] = ":woman_bald_tone4:", + ["\U0001f469\U0001f3ff\u200d\U0001f9b2"] = ":woman_bald_tone5:", + ["\U0001f469\u200d\U0001f9b2"] = ":woman_bald:", + ["\U0001f9d4\u200d\u2640\ufe0f"] = ":woman_beard:", + ["\U0001f6b4\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_biking_tone1:", + ["\U0001f6b4\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_biking_tone2:", + ["\U0001f6b4\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_biking_tone3:", + ["\U0001f6b4\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_biking_tone4:", + ["\U0001f6b4\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_biking_tone5:", + ["\U0001f6b4\u200d\u2640\ufe0f"] = ":woman_biking:", + ["\u26f9\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_bouncing_ball_tone1:", + ["\u26f9\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_bouncing_ball_tone2:", + ["\u26f9\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_bouncing_ball_tone3:", + ["\u26f9\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_bouncing_ball_tone4:", + ["\u26f9\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_bouncing_ball_tone5:", + ["\u26f9\ufe0f\u200d\u2640\ufe0f"] = ":woman_bouncing_ball:", + ["\U0001f647\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_bowing_tone1:", + ["\U0001f647\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_bowing_tone2:", + ["\U0001f647\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_bowing_tone3:", + ["\U0001f647\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_bowing_tone4:", + ["\U0001f647\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_bowing_tone5:", + ["\U0001f647\u200d\u2640\ufe0f"] = ":woman_bowing:", + ["\U0001f938\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_cartwheeling_tone1:", + ["\U0001f938\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_cartwheeling_tone2:", + ["\U0001f938\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_cartwheeling_tone3:", + ["\U0001f938\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_cartwheeling_tone4:", + ["\U0001f938\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_cartwheeling_tone5:", + ["\U0001f938\u200d\u2640\ufe0f"] = ":woman_cartwheeling:", + ["\U0001f9d7\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_climbing_tone1:", + ["\U0001f9d7\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_climbing_tone2:", + ["\U0001f9d7\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_climbing_tone3:", + ["\U0001f9d7\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_climbing_tone4:", + ["\U0001f9d7\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_climbing_tone5:", + ["\U0001f9d7\u200d\u2640\ufe0f"] = ":woman_climbing:", + ["\U0001f477\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_construction_worker_tone1:", + ["\U0001f477\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_construction_worker_tone2:", + ["\U0001f477\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_construction_worker_tone3:", + ["\U0001f477\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_construction_worker_tone4:", + ["\U0001f477\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_construction_worker_tone5:", + ["\U0001f477\u200d\u2640\ufe0f"] = ":woman_construction_worker:", + ["\U0001f469\U0001f3fb\u200d\U0001f373"] = ":woman_cook_tone1:", + ["\U0001f469\U0001f3fc\u200d\U0001f373"] = ":woman_cook_tone2:", + ["\U0001f469\U0001f3fd\u200d\U0001f373"] = ":woman_cook_tone3:", + ["\U0001f469\U0001f3fe\u200d\U0001f373"] = ":woman_cook_tone4:", + ["\U0001f469\U0001f3ff\u200d\U0001f373"] = ":woman_cook_tone5:", + ["\U0001f469\u200d\U0001f373"] = ":woman_cook:", + ["\U0001f469\U0001f3fb\u200d\U0001f9b1"] = ":woman_curly_haired_tone1:", + ["\U0001f469\U0001f3fc\u200d\U0001f9b1"] = ":woman_curly_haired_tone2:", + ["\U0001f469\U0001f3fd\u200d\U0001f9b1"] = ":woman_curly_haired_tone3:", + ["\U0001f469\U0001f3fe\u200d\U0001f9b1"] = ":woman_curly_haired_tone4:", + ["\U0001f469\U0001f3ff\u200d\U0001f9b1"] = ":woman_curly_haired_tone5:", + ["\U0001f469\u200d\U0001f9b1"] = ":woman_curly_haired:", + ["\U0001f575\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_detective_tone1:", + ["\U0001f575\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_detective_tone2:", + ["\U0001f575\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_detective_tone3:", + ["\U0001f575\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_detective_tone4:", + ["\U0001f575\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_detective_tone5:", + ["\U0001f575\ufe0f\u200d\u2640\ufe0f"] = ":woman_detective:", + ["\U0001f9dd\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_elf_tone1:", + ["\U0001f9dd\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_elf_tone2:", + ["\U0001f9dd\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_elf_tone3:", + ["\U0001f9dd\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_elf_tone4:", + ["\U0001f9dd\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_elf_tone5:", + ["\U0001f9dd\u200d\u2640\ufe0f"] = ":woman_elf:", + ["\U0001f926\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_facepalming_tone1:", + ["\U0001f926\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_facepalming_tone2:", + ["\U0001f926\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_facepalming_tone3:", + ["\U0001f926\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_facepalming_tone4:", + ["\U0001f926\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_facepalming_tone5:", + ["\U0001f926\u200d\u2640\ufe0f"] = ":woman_facepalming:", + ["\U0001f469\U0001f3fb\u200d\U0001f3ed"] = ":woman_factory_worker_tone1:", + ["\U0001f469\U0001f3fc\u200d\U0001f3ed"] = ":woman_factory_worker_tone2:", + ["\U0001f469\U0001f3fd\u200d\U0001f3ed"] = ":woman_factory_worker_tone3:", + ["\U0001f469\U0001f3fe\u200d\U0001f3ed"] = ":woman_factory_worker_tone4:", + ["\U0001f469\U0001f3ff\u200d\U0001f3ed"] = ":woman_factory_worker_tone5:", + ["\U0001f469\u200d\U0001f3ed"] = ":woman_factory_worker:", + ["\U0001f9da\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_fairy_tone1:", + ["\U0001f9da\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_fairy_tone2:", + ["\U0001f9da\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_fairy_tone3:", + ["\U0001f9da\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_fairy_tone4:", + ["\U0001f9da\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_fairy_tone5:", + ["\U0001f9da\u200d\u2640\ufe0f"] = ":woman_fairy:", + ["\U0001f469\U0001f3fb\u200d\U0001f33e"] = ":woman_farmer_tone1:", + ["\U0001f469\U0001f3fc\u200d\U0001f33e"] = ":woman_farmer_tone2:", + ["\U0001f469\U0001f3fd\u200d\U0001f33e"] = ":woman_farmer_tone3:", + ["\U0001f469\U0001f3fe\u200d\U0001f33e"] = ":woman_farmer_tone4:", + ["\U0001f469\U0001f3ff\u200d\U0001f33e"] = ":woman_farmer_tone5:", + ["\U0001f469\u200d\U0001f33e"] = ":woman_farmer:", + ["\U0001f469\U0001f3fb\u200d\U0001f37c"] = ":woman_feeding_baby_tone1:", + ["\U0001f469\U0001f3fc\u200d\U0001f37c"] = ":woman_feeding_baby_tone2:", + ["\U0001f469\U0001f3fd\u200d\U0001f37c"] = ":woman_feeding_baby_tone3:", + ["\U0001f469\U0001f3fe\u200d\U0001f37c"] = ":woman_feeding_baby_tone4:", + ["\U0001f469\U0001f3ff\u200d\U0001f37c"] = ":woman_feeding_baby_tone5:", + ["\U0001f469\u200d\U0001f37c"] = ":woman_feeding_baby:", + ["\U0001f469\U0001f3fb\u200d\U0001f692"] = ":woman_firefighter_tone1:", + ["\U0001f469\U0001f3fc\u200d\U0001f692"] = ":woman_firefighter_tone2:", + ["\U0001f469\U0001f3fd\u200d\U0001f692"] = ":woman_firefighter_tone3:", + ["\U0001f469\U0001f3fe\u200d\U0001f692"] = ":woman_firefighter_tone4:", + ["\U0001f469\U0001f3ff\u200d\U0001f692"] = ":woman_firefighter_tone5:", + ["\U0001f469\u200d\U0001f692"] = ":woman_firefighter:", + ["\U0001f64d\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_frowning_tone1:", + ["\U0001f64d\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_frowning_tone2:", + ["\U0001f64d\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_frowning_tone3:", + ["\U0001f64d\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_frowning_tone4:", + ["\U0001f64d\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_frowning_tone5:", + ["\U0001f64d\u200d\u2640\ufe0f"] = ":woman_frowning:", + ["\U0001f9de\u200d\u2640\ufe0f"] = ":woman_genie:", + ["\U0001f645\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_gesturing_no_tone1:", + ["\U0001f645\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_gesturing_no_tone2:", + ["\U0001f645\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_gesturing_no_tone3:", + ["\U0001f645\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_gesturing_no_tone4:", + ["\U0001f645\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_gesturing_no_tone5:", + ["\U0001f645\u200d\u2640\ufe0f"] = ":woman_gesturing_no:", + ["\U0001f646\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_gesturing_ok_tone1:", + ["\U0001f646\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_gesturing_ok_tone2:", + ["\U0001f646\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_gesturing_ok_tone3:", + ["\U0001f646\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_gesturing_ok_tone4:", + ["\U0001f646\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_gesturing_ok_tone5:", + ["\U0001f646\u200d\u2640\ufe0f"] = ":woman_gesturing_ok:", + ["\U0001f486\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_getting_face_massage_tone1:", + ["\U0001f486\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_getting_face_massage_tone2:", + ["\U0001f486\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_getting_face_massage_tone3:", + ["\U0001f486\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_getting_face_massage_tone4:", + ["\U0001f486\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_getting_face_massage_tone5:", + ["\U0001f486\u200d\u2640\ufe0f"] = ":woman_getting_face_massage:", + ["\U0001f487\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_getting_haircut_tone1:", + ["\U0001f487\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_getting_haircut_tone2:", + ["\U0001f487\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_getting_haircut_tone3:", + ["\U0001f487\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_getting_haircut_tone4:", + ["\U0001f487\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_getting_haircut_tone5:", + ["\U0001f487\u200d\u2640\ufe0f"] = ":woman_getting_haircut:", + ["\U0001f3cc\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_golfing_tone1:", + ["\U0001f3cc\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_golfing_tone2:", + ["\U0001f3cc\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_golfing_tone3:", + ["\U0001f3cc\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_golfing_tone4:", + ["\U0001f3cc\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_golfing_tone5:", + ["\U0001f3cc\ufe0f\u200d\u2640\ufe0f"] = ":woman_golfing:", + ["\U0001f482\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_guard_tone1:", + ["\U0001f482\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_guard_tone2:", + ["\U0001f482\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_guard_tone3:", + ["\U0001f482\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_guard_tone4:", + ["\U0001f482\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_guard_tone5:", + ["\U0001f482\u200d\u2640\ufe0f"] = ":woman_guard:", + ["\U0001f469\U0001f3fb\u200d\u2695\ufe0f"] = ":woman_health_worker_tone1:", + ["\U0001f469\U0001f3fc\u200d\u2695\ufe0f"] = ":woman_health_worker_tone2:", + ["\U0001f469\U0001f3fd\u200d\u2695\ufe0f"] = ":woman_health_worker_tone3:", + ["\U0001f469\U0001f3fe\u200d\u2695\ufe0f"] = ":woman_health_worker_tone4:", + ["\U0001f469\U0001f3ff\u200d\u2695\ufe0f"] = ":woman_health_worker_tone5:", + ["\U0001f469\u200d\u2695\ufe0f"] = ":woman_health_worker:", + ["\U0001f9d8\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_in_lotus_position_tone1:", + ["\U0001f9d8\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_in_lotus_position_tone2:", + ["\U0001f9d8\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_in_lotus_position_tone3:", + ["\U0001f9d8\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_in_lotus_position_tone4:", + ["\U0001f9d8\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_in_lotus_position_tone5:", + ["\U0001f9d8\u200d\u2640\ufe0f"] = ":woman_in_lotus_position:", + ["\U0001f469\U0001f3fb\u200d\U0001f9bd\u200d\u27a1\ufe0f"] = ":woman_in_manual_wheelchair_facing_right_tone1:", + ["\U0001f469\U0001f3fc\u200d\U0001f9bd\u200d\u27a1\ufe0f"] = ":woman_in_manual_wheelchair_facing_right_tone2:", + ["\U0001f469\U0001f3fd\u200d\U0001f9bd\u200d\u27a1\ufe0f"] = ":woman_in_manual_wheelchair_facing_right_tone3:", + ["\U0001f469\U0001f3fe\u200d\U0001f9bd\u200d\u27a1\ufe0f"] = ":woman_in_manual_wheelchair_facing_right_tone4:", + ["\U0001f469\U0001f3ff\u200d\U0001f9bd\u200d\u27a1\ufe0f"] = ":woman_in_manual_wheelchair_facing_right_tone5:", + ["\U0001f469\u200d\U0001f9bd\u200d\u27a1\ufe0f"] = ":woman_in_manual_wheelchair_facing_right:", + ["\U0001f469\U0001f3fb\u200d\U0001f9bd"] = ":woman_in_manual_wheelchair_tone1:", + ["\U0001f469\U0001f3fc\u200d\U0001f9bd"] = ":woman_in_manual_wheelchair_tone2:", + ["\U0001f469\U0001f3fd\u200d\U0001f9bd"] = ":woman_in_manual_wheelchair_tone3:", + ["\U0001f469\U0001f3fe\u200d\U0001f9bd"] = ":woman_in_manual_wheelchair_tone4:", + ["\U0001f469\U0001f3ff\u200d\U0001f9bd"] = ":woman_in_manual_wheelchair_tone5:", + ["\U0001f469\u200d\U0001f9bd"] = ":woman_in_manual_wheelchair:", + ["\U0001f469\U0001f3fb\u200d\U0001f9bc\u200d\u27a1\ufe0f"] = ":woman_in_motorized_wheelchair_facing_right_tone1:", + ["\U0001f469\U0001f3fc\u200d\U0001f9bc\u200d\u27a1\ufe0f"] = ":woman_in_motorized_wheelchair_facing_right_tone2:", + ["\U0001f469\U0001f3fd\u200d\U0001f9bc\u200d\u27a1\ufe0f"] = ":woman_in_motorized_wheelchair_facing_right_tone3:", + ["\U0001f469\U0001f3fe\u200d\U0001f9bc\u200d\u27a1\ufe0f"] = ":woman_in_motorized_wheelchair_facing_right_tone4:", + ["\U0001f469\U0001f3ff\u200d\U0001f9bc\u200d\u27a1\ufe0f"] = ":woman_in_motorized_wheelchair_facing_right_tone5:", + ["\U0001f469\u200d\U0001f9bc\u200d\u27a1\ufe0f"] = ":woman_in_motorized_wheelchair_facing_right:", + ["\U0001f469\U0001f3fb\u200d\U0001f9bc"] = ":woman_in_motorized_wheelchair_tone1:", + ["\U0001f469\U0001f3fc\u200d\U0001f9bc"] = ":woman_in_motorized_wheelchair_tone2:", + ["\U0001f469\U0001f3fd\u200d\U0001f9bc"] = ":woman_in_motorized_wheelchair_tone3:", + ["\U0001f469\U0001f3fe\u200d\U0001f9bc"] = ":woman_in_motorized_wheelchair_tone4:", + ["\U0001f469\U0001f3ff\u200d\U0001f9bc"] = ":woman_in_motorized_wheelchair_tone5:", + ["\U0001f469\u200d\U0001f9bc"] = ":woman_in_motorized_wheelchair:", + ["\U0001f9d6\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_in_steamy_room_tone1:", + ["\U0001f9d6\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_in_steamy_room_tone2:", + ["\U0001f9d6\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_in_steamy_room_tone3:", + ["\U0001f9d6\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_in_steamy_room_tone4:", + ["\U0001f9d6\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_in_steamy_room_tone5:", + ["\U0001f9d6\u200d\u2640\ufe0f"] = ":woman_in_steamy_room:", + ["\U0001f935\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_in_tuxedo_tone1:", + ["\U0001f935\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_in_tuxedo_tone2:", + ["\U0001f935\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_in_tuxedo_tone3:", + ["\U0001f935\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_in_tuxedo_tone4:", + ["\U0001f935\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_in_tuxedo_tone5:", + ["\U0001f935\u200d\u2640\ufe0f"] = ":woman_in_tuxedo:", + ["\U0001f469\U0001f3fb\u200d\u2696\ufe0f"] = ":woman_judge_tone1:", + ["\U0001f469\U0001f3fc\u200d\u2696\ufe0f"] = ":woman_judge_tone2:", + ["\U0001f469\U0001f3fd\u200d\u2696\ufe0f"] = ":woman_judge_tone3:", + ["\U0001f469\U0001f3fe\u200d\u2696\ufe0f"] = ":woman_judge_tone4:", + ["\U0001f469\U0001f3ff\u200d\u2696\ufe0f"] = ":woman_judge_tone5:", + ["\U0001f469\u200d\u2696\ufe0f"] = ":woman_judge:", + ["\U0001f939\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_juggling_tone1:", + ["\U0001f939\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_juggling_tone2:", + ["\U0001f939\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_juggling_tone3:", + ["\U0001f939\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_juggling_tone4:", + ["\U0001f939\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_juggling_tone5:", + ["\U0001f939\u200d\u2640\ufe0f"] = ":woman_juggling:", + ["\U0001f9ce\U0001f3fb\u200d\u2640\ufe0f\u200d\u27a1\ufe0f"] = ":woman_kneeling_facing_right_tone1:", + ["\U0001f9ce\U0001f3fc\u200d\u2640\ufe0f\u200d\u27a1\ufe0f"] = ":woman_kneeling_facing_right_tone2:", + ["\U0001f9ce\U0001f3fd\u200d\u2640\ufe0f\u200d\u27a1\ufe0f"] = ":woman_kneeling_facing_right_tone3:", + ["\U0001f9ce\U0001f3fe\u200d\u2640\ufe0f\u200d\u27a1\ufe0f"] = ":woman_kneeling_facing_right_tone4:", + ["\U0001f9ce\U0001f3ff\u200d\u2640\ufe0f\u200d\u27a1\ufe0f"] = ":woman_kneeling_facing_right_tone5:", + ["\U0001f9ce\u200d\u2640\ufe0f\u200d\u27a1\ufe0f"] = ":woman_kneeling_facing_right:", + ["\U0001f9ce\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_kneeling_tone1:", + ["\U0001f9ce\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_kneeling_tone2:", + ["\U0001f9ce\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_kneeling_tone3:", + ["\U0001f9ce\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_kneeling_tone4:", + ["\U0001f9ce\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_kneeling_tone5:", + ["\U0001f9ce\u200d\u2640\ufe0f"] = ":woman_kneeling:", + ["\U0001f3cb\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_lifting_weights_tone1:", + ["\U0001f3cb\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_lifting_weights_tone2:", + ["\U0001f3cb\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_lifting_weights_tone3:", + ["\U0001f3cb\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_lifting_weights_tone4:", + ["\U0001f3cb\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_lifting_weights_tone5:", + ["\U0001f3cb\ufe0f\u200d\u2640\ufe0f"] = ":woman_lifting_weights:", + ["\U0001f9d9\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_mage_tone1:", + ["\U0001f9d9\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_mage_tone2:", + ["\U0001f9d9\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_mage_tone3:", + ["\U0001f9d9\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_mage_tone4:", + ["\U0001f9d9\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_mage_tone5:", + ["\U0001f9d9\u200d\u2640\ufe0f"] = ":woman_mage:", + ["\U0001f469\U0001f3fb\u200d\U0001f527"] = ":woman_mechanic_tone1:", + ["\U0001f469\U0001f3fc\u200d\U0001f527"] = ":woman_mechanic_tone2:", + ["\U0001f469\U0001f3fd\u200d\U0001f527"] = ":woman_mechanic_tone3:", + ["\U0001f469\U0001f3fe\u200d\U0001f527"] = ":woman_mechanic_tone4:", + ["\U0001f469\U0001f3ff\u200d\U0001f527"] = ":woman_mechanic_tone5:", + ["\U0001f469\u200d\U0001f527"] = ":woman_mechanic:", + ["\U0001f6b5\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_mountain_biking_tone1:", + ["\U0001f6b5\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_mountain_biking_tone2:", + ["\U0001f6b5\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_mountain_biking_tone3:", + ["\U0001f6b5\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_mountain_biking_tone4:", + ["\U0001f6b5\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_mountain_biking_tone5:", + ["\U0001f6b5\u200d\u2640\ufe0f"] = ":woman_mountain_biking:", + ["\U0001f469\U0001f3fb\u200d\U0001f4bc"] = ":woman_office_worker_tone1:", + ["\U0001f469\U0001f3fc\u200d\U0001f4bc"] = ":woman_office_worker_tone2:", + ["\U0001f469\U0001f3fd\u200d\U0001f4bc"] = ":woman_office_worker_tone3:", + ["\U0001f469\U0001f3fe\u200d\U0001f4bc"] = ":woman_office_worker_tone4:", + ["\U0001f469\U0001f3ff\u200d\U0001f4bc"] = ":woman_office_worker_tone5:", + ["\U0001f469\u200d\U0001f4bc"] = ":woman_office_worker:", + ["\U0001f469\U0001f3fb\u200d\u2708\ufe0f"] = ":woman_pilot_tone1:", + ["\U0001f469\U0001f3fc\u200d\u2708\ufe0f"] = ":woman_pilot_tone2:", + ["\U0001f469\U0001f3fd\u200d\u2708\ufe0f"] = ":woman_pilot_tone3:", + ["\U0001f469\U0001f3fe\u200d\u2708\ufe0f"] = ":woman_pilot_tone4:", + ["\U0001f469\U0001f3ff\u200d\u2708\ufe0f"] = ":woman_pilot_tone5:", + ["\U0001f469\u200d\u2708\ufe0f"] = ":woman_pilot:", + ["\U0001f93e\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_playing_handball_tone1:", + ["\U0001f93e\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_playing_handball_tone2:", + ["\U0001f93e\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_playing_handball_tone3:", + ["\U0001f93e\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_playing_handball_tone4:", + ["\U0001f93e\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_playing_handball_tone5:", + ["\U0001f93e\u200d\u2640\ufe0f"] = ":woman_playing_handball:", + ["\U0001f93d\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_playing_water_polo_tone1:", + ["\U0001f93d\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_playing_water_polo_tone2:", + ["\U0001f93d\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_playing_water_polo_tone3:", + ["\U0001f93d\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_playing_water_polo_tone4:", + ["\U0001f93d\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_playing_water_polo_tone5:", + ["\U0001f93d\u200d\u2640\ufe0f"] = ":woman_playing_water_polo:", + ["\U0001f46e\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_police_officer_tone1:", + ["\U0001f46e\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_police_officer_tone2:", + ["\U0001f46e\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_police_officer_tone3:", + ["\U0001f46e\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_police_officer_tone4:", + ["\U0001f46e\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_police_officer_tone5:", + ["\U0001f46e\u200d\u2640\ufe0f"] = ":woman_police_officer:", + ["\U0001f64e\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_pouting_tone1:", + ["\U0001f64e\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_pouting_tone2:", + ["\U0001f64e\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_pouting_tone3:", + ["\U0001f64e\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_pouting_tone4:", + ["\U0001f64e\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_pouting_tone5:", + ["\U0001f64e\u200d\u2640\ufe0f"] = ":woman_pouting:", + ["\U0001f64b\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_raising_hand_tone1:", + ["\U0001f64b\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_raising_hand_tone2:", + ["\U0001f64b\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_raising_hand_tone3:", + ["\U0001f64b\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_raising_hand_tone4:", + ["\U0001f64b\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_raising_hand_tone5:", + ["\U0001f64b\u200d\u2640\ufe0f"] = ":woman_raising_hand:", + ["\U0001f469\U0001f3fb\u200d\U0001f9b0"] = ":woman_red_haired_tone1:", + ["\U0001f469\U0001f3fc\u200d\U0001f9b0"] = ":woman_red_haired_tone2:", + ["\U0001f469\U0001f3fd\u200d\U0001f9b0"] = ":woman_red_haired_tone3:", + ["\U0001f469\U0001f3fe\u200d\U0001f9b0"] = ":woman_red_haired_tone4:", + ["\U0001f469\U0001f3ff\u200d\U0001f9b0"] = ":woman_red_haired_tone5:", + ["\U0001f469\u200d\U0001f9b0"] = ":woman_red_haired:", + ["\U0001f6a3\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_rowing_boat_tone1:", + ["\U0001f6a3\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_rowing_boat_tone2:", + ["\U0001f6a3\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_rowing_boat_tone3:", + ["\U0001f6a3\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_rowing_boat_tone4:", + ["\U0001f6a3\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_rowing_boat_tone5:", + ["\U0001f6a3\u200d\u2640\ufe0f"] = ":woman_rowing_boat:", + ["\U0001f3c3\U0001f3fb\u200d\u2640\ufe0f\u200d\u27a1\ufe0f"] = ":woman_running_facing_right_tone1:", + ["\U0001f3c3\U0001f3fc\u200d\u2640\ufe0f\u200d\u27a1\ufe0f"] = ":woman_running_facing_right_tone2:", + ["\U0001f3c3\U0001f3fd\u200d\u2640\ufe0f\u200d\u27a1\ufe0f"] = ":woman_running_facing_right_tone3:", + ["\U0001f3c3\U0001f3fe\u200d\u2640\ufe0f\u200d\u27a1\ufe0f"] = ":woman_running_facing_right_tone4:", + ["\U0001f3c3\U0001f3ff\u200d\u2640\ufe0f\u200d\u27a1\ufe0f"] = ":woman_running_facing_right_tone5:", + ["\U0001f3c3\u200d\u2640\ufe0f\u200d\u27a1\ufe0f"] = ":woman_running_facing_right:", + ["\U0001f3c3\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_running_tone1:", + ["\U0001f3c3\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_running_tone2:", + ["\U0001f3c3\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_running_tone3:", + ["\U0001f3c3\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_running_tone4:", + ["\U0001f3c3\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_running_tone5:", + ["\U0001f3c3\u200d\u2640\ufe0f"] = ":woman_running:", + ["\U0001f469\U0001f3fb\u200d\U0001f52c"] = ":woman_scientist_tone1:", + ["\U0001f469\U0001f3fc\u200d\U0001f52c"] = ":woman_scientist_tone2:", + ["\U0001f469\U0001f3fd\u200d\U0001f52c"] = ":woman_scientist_tone3:", + ["\U0001f469\U0001f3fe\u200d\U0001f52c"] = ":woman_scientist_tone4:", + ["\U0001f469\U0001f3ff\u200d\U0001f52c"] = ":woman_scientist_tone5:", + ["\U0001f469\u200d\U0001f52c"] = ":woman_scientist:", + ["\U0001f937\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_shrugging_tone1:", + ["\U0001f937\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_shrugging_tone2:", + ["\U0001f937\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_shrugging_tone3:", + ["\U0001f937\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_shrugging_tone4:", + ["\U0001f937\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_shrugging_tone5:", + ["\U0001f937\u200d\u2640\ufe0f"] = ":woman_shrugging:", + ["\U0001f469\U0001f3fb\u200d\U0001f3a4"] = ":woman_singer_tone1:", + ["\U0001f469\U0001f3fc\u200d\U0001f3a4"] = ":woman_singer_tone2:", + ["\U0001f469\U0001f3fd\u200d\U0001f3a4"] = ":woman_singer_tone3:", + ["\U0001f469\U0001f3fe\u200d\U0001f3a4"] = ":woman_singer_tone4:", + ["\U0001f469\U0001f3ff\u200d\U0001f3a4"] = ":woman_singer_tone5:", + ["\U0001f469\u200d\U0001f3a4"] = ":woman_singer:", + ["\U0001f9cd\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_standing_tone1:", + ["\U0001f9cd\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_standing_tone2:", + ["\U0001f9cd\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_standing_tone3:", + ["\U0001f9cd\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_standing_tone4:", + ["\U0001f9cd\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_standing_tone5:", + ["\U0001f9cd\u200d\u2640\ufe0f"] = ":woman_standing:", + ["\U0001f469\U0001f3fb\u200d\U0001f393"] = ":woman_student_tone1:", + ["\U0001f469\U0001f3fc\u200d\U0001f393"] = ":woman_student_tone2:", + ["\U0001f469\U0001f3fd\u200d\U0001f393"] = ":woman_student_tone3:", + ["\U0001f469\U0001f3fe\u200d\U0001f393"] = ":woman_student_tone4:", + ["\U0001f469\U0001f3ff\u200d\U0001f393"] = ":woman_student_tone5:", + ["\U0001f469\u200d\U0001f393"] = ":woman_student:", + ["\U0001f9b8\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_superhero_tone1:", + ["\U0001f9b8\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_superhero_tone2:", + ["\U0001f9b8\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_superhero_tone3:", + ["\U0001f9b8\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_superhero_tone4:", + ["\U0001f9b8\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_superhero_tone5:", + ["\U0001f9b8\u200d\u2640\ufe0f"] = ":woman_superhero:", + ["\U0001f9b9\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_supervillain_tone1:", + ["\U0001f9b9\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_supervillain_tone2:", + ["\U0001f9b9\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_supervillain_tone3:", + ["\U0001f9b9\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_supervillain_tone4:", + ["\U0001f9b9\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_supervillain_tone5:", + ["\U0001f9b9\u200d\u2640\ufe0f"] = ":woman_supervillain:", + ["\U0001f3c4\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_surfing_tone1:", + ["\U0001f3c4\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_surfing_tone2:", + ["\U0001f3c4\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_surfing_tone3:", + ["\U0001f3c4\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_surfing_tone4:", + ["\U0001f3c4\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_surfing_tone5:", + ["\U0001f3c4\u200d\u2640\ufe0f"] = ":woman_surfing:", + ["\U0001f3ca\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_swimming_tone1:", + ["\U0001f3ca\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_swimming_tone2:", + ["\U0001f3ca\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_swimming_tone3:", + ["\U0001f3ca\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_swimming_tone4:", + ["\U0001f3ca\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_swimming_tone5:", + ["\U0001f3ca\u200d\u2640\ufe0f"] = ":woman_swimming:", + ["\U0001f469\U0001f3fb\u200d\U0001f3eb"] = ":woman_teacher_tone1:", + ["\U0001f469\U0001f3fc\u200d\U0001f3eb"] = ":woman_teacher_tone2:", + ["\U0001f469\U0001f3fd\u200d\U0001f3eb"] = ":woman_teacher_tone3:", + ["\U0001f469\U0001f3fe\u200d\U0001f3eb"] = ":woman_teacher_tone4:", + ["\U0001f469\U0001f3ff\u200d\U0001f3eb"] = ":woman_teacher_tone5:", + ["\U0001f469\u200d\U0001f3eb"] = ":woman_teacher:", + ["\U0001f469\U0001f3fb\u200d\U0001f4bb"] = ":woman_technologist_tone1:", + ["\U0001f469\U0001f3fc\u200d\U0001f4bb"] = ":woman_technologist_tone2:", + ["\U0001f469\U0001f3fd\u200d\U0001f4bb"] = ":woman_technologist_tone3:", + ["\U0001f469\U0001f3fe\u200d\U0001f4bb"] = ":woman_technologist_tone4:", + ["\U0001f469\U0001f3ff\u200d\U0001f4bb"] = ":woman_technologist_tone5:", + ["\U0001f469\u200d\U0001f4bb"] = ":woman_technologist:", + ["\U0001f481\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_tipping_hand_tone1:", + ["\U0001f481\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_tipping_hand_tone2:", + ["\U0001f481\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_tipping_hand_tone3:", + ["\U0001f481\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_tipping_hand_tone4:", + ["\U0001f481\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_tipping_hand_tone5:", + ["\U0001f481\u200d\u2640\ufe0f"] = ":woman_tipping_hand:", + ["\U0001f9d4\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_tone1_beard:", + ["\U0001f469\U0001f3fb"] = ":woman_tone1:", + ["\U0001f9d4\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_tone2_beard:", + ["\U0001f469\U0001f3fc"] = ":woman_tone2:", + ["\U0001f9d4\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_tone3_beard:", + ["\U0001f469\U0001f3fd"] = ":woman_tone3:", + ["\U0001f9d4\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_tone4_beard:", + ["\U0001f469\U0001f3fe"] = ":woman_tone4:", + ["\U0001f9d4\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_tone5_beard:", + ["\U0001f469\U0001f3ff"] = ":woman_tone5:", + ["\U0001f9db\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_vampire_tone1:", + ["\U0001f9db\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_vampire_tone2:", + ["\U0001f9db\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_vampire_tone3:", + ["\U0001f9db\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_vampire_tone4:", + ["\U0001f9db\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_vampire_tone5:", + ["\U0001f9db\u200d\u2640\ufe0f"] = ":woman_vampire:", + ["\U0001f6b6\U0001f3fb\u200d\u2640\ufe0f\u200d\u27a1\ufe0f"] = ":woman_walking_facing_right_tone1:", + ["\U0001f6b6\U0001f3fc\u200d\u2640\ufe0f\u200d\u27a1\ufe0f"] = ":woman_walking_facing_right_tone2:", + ["\U0001f6b6\U0001f3fd\u200d\u2640\ufe0f\u200d\u27a1\ufe0f"] = ":woman_walking_facing_right_tone3:", + ["\U0001f6b6\U0001f3fe\u200d\u2640\ufe0f\u200d\u27a1\ufe0f"] = ":woman_walking_facing_right_tone4:", + ["\U0001f6b6\U0001f3ff\u200d\u2640\ufe0f\u200d\u27a1\ufe0f"] = ":woman_walking_facing_right_tone5:", + ["\U0001f6b6\u200d\u2640\ufe0f\u200d\u27a1\ufe0f"] = ":woman_walking_facing_right:", + ["\U0001f6b6\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_walking_tone1:", + ["\U0001f6b6\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_walking_tone2:", + ["\U0001f6b6\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_walking_tone3:", + ["\U0001f6b6\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_walking_tone4:", + ["\U0001f6b6\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_walking_tone5:", + ["\U0001f6b6\u200d\u2640\ufe0f"] = ":woman_walking:", + ["\U0001f473\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_wearing_turban_tone1:", + ["\U0001f473\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_wearing_turban_tone2:", + ["\U0001f473\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_wearing_turban_tone3:", + ["\U0001f473\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_wearing_turban_tone4:", + ["\U0001f473\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_wearing_turban_tone5:", + ["\U0001f473\u200d\u2640\ufe0f"] = ":woman_wearing_turban:", + ["\U0001f469\U0001f3fb\u200d\U0001f9b3"] = ":woman_white_haired_tone1:", + ["\U0001f469\U0001f3fc\u200d\U0001f9b3"] = ":woman_white_haired_tone2:", + ["\U0001f469\U0001f3fd\u200d\U0001f9b3"] = ":woman_white_haired_tone3:", + ["\U0001f469\U0001f3fe\u200d\U0001f9b3"] = ":woman_white_haired_tone4:", + ["\U0001f469\U0001f3ff\u200d\U0001f9b3"] = ":woman_white_haired_tone5:", + ["\U0001f469\u200d\U0001f9b3"] = ":woman_white_haired:", + ["\U0001f9d5\U0001f3fb"] = ":woman_with_headscarf_tone1:", + ["\U0001f9d5\U0001f3fc"] = ":woman_with_headscarf_tone2:", + ["\U0001f9d5\U0001f3fd"] = ":woman_with_headscarf_tone3:", + ["\U0001f9d5\U0001f3fe"] = ":woman_with_headscarf_tone4:", + ["\U0001f9d5\U0001f3ff"] = ":woman_with_headscarf_tone5:", + ["\U0001f9d5"] = ":woman_with_headscarf:", + ["\U0001f469\U0001f3fb\u200d\U0001f9af"] = ":woman_with_probing_cane_tone1:", + ["\U0001f469\U0001f3fc\u200d\U0001f9af"] = ":woman_with_probing_cane_tone2:", + ["\U0001f469\U0001f3fd\u200d\U0001f9af"] = ":woman_with_probing_cane_tone3:", + ["\U0001f469\U0001f3fe\u200d\U0001f9af"] = ":woman_with_probing_cane_tone4:", + ["\U0001f469\U0001f3ff\u200d\U0001f9af"] = ":woman_with_probing_cane_tone5:", + ["\U0001f469\u200d\U0001f9af"] = ":woman_with_probing_cane:", + ["\U0001f470\U0001f3fb\u200d\u2640\ufe0f"] = ":woman_with_veil_tone1:", + ["\U0001f470\U0001f3fc\u200d\u2640\ufe0f"] = ":woman_with_veil_tone2:", + ["\U0001f470\U0001f3fd\u200d\u2640\ufe0f"] = ":woman_with_veil_tone3:", + ["\U0001f470\U0001f3fe\u200d\u2640\ufe0f"] = ":woman_with_veil_tone4:", + ["\U0001f470\U0001f3ff\u200d\u2640\ufe0f"] = ":woman_with_veil_tone5:", + ["\U0001f470\u200d\u2640\ufe0f"] = ":woman_with_veil:", + ["\U0001f469\U0001f3fb\u200d\U0001f9af\u200d\u27a1\ufe0f"] = ":woman_with_white_cane_facing_right_tone1:", + ["\U0001f469\U0001f3fc\u200d\U0001f9af\u200d\u27a1\ufe0f"] = ":woman_with_white_cane_facing_right_tone2:", + ["\U0001f469\U0001f3fd\u200d\U0001f9af\u200d\u27a1\ufe0f"] = ":woman_with_white_cane_facing_right_tone3:", + ["\U0001f469\U0001f3fe\u200d\U0001f9af\u200d\u27a1\ufe0f"] = ":woman_with_white_cane_facing_right_tone4:", + ["\U0001f469\U0001f3ff\u200d\U0001f9af\u200d\u27a1\ufe0f"] = ":woman_with_white_cane_facing_right_tone5:", + ["\U0001f469\u200d\U0001f9af\u200d\u27a1\ufe0f"] = ":woman_with_white_cane_facing_right:", + ["\U0001f9df\u200d\u2640\ufe0f"] = ":woman_zombie:", + ["\U0001f469"] = ":woman:", + ["\U0001f45a"] = ":womans_clothes:", + ["\U0001f97f"] = ":womans_flat_shoe:", + ["\U0001f452"] = ":womans_hat:", + ["\U0001f46d"] = ":women_holding_hands_tone5_tone4:", + ["\U0001f46f\u200d\u2640\ufe0f"] = ":women_with_bunny_ears_partying:", + ["\U0001f93c\u200d\u2640\ufe0f"] = ":women_wrestling:", + ["\U0001f6ba"] = ":womens:", + ["\U0001fab5"] = ":wood:", + ["\U0001f974"] = ":woozy_face:", + ["\U0001fab1"] = ":worm:", + ["\U0001f61f"] = ":worried:", + ["\U0001f527"] = ":wrench:", + ["\u270d\U0001f3fb"] = ":writing_hand_tone1:", + ["\u270d\U0001f3fc"] = ":writing_hand_tone2:", + ["\u270d\U0001f3fd"] = ":writing_hand_tone3:", + ["\u270d\U0001f3fe"] = ":writing_hand_tone4:", + ["\u270d\U0001f3ff"] = ":writing_hand_tone5:", + ["\u270d\ufe0f"] = ":writing_hand:", + ["\u270d"] = ":writing_hand:", + ["\U0001fa7b"] = ":x_ray:", + ["\u274c"] = ":x:", + ["\U0001f9f6"] = ":yarn:", + ["\U0001f971"] = ":yawning_face:", + ["\U0001f7e1"] = ":yellow_circle:", + ["\U0001f49b"] = ":yellow_heart:", + ["\U0001f7e8"] = ":yellow_square:", + ["\U0001f4b4"] = ":yen:", + ["\u262f\ufe0f"] = ":yin_yang:", + ["\u262f"] = ":yin_yang:", + ["\U0001fa80"] = ":yo_yo:", + ["\U0001f60b"] = ":yum:", + ["\U0001f92a"] = ":zany_face:", + ["\u26a1"] = ":zap:", + ["\U0001f993"] = ":zebra:", + ["\u0030\ufe0f\u20e3"] = ":zero:", + ["\u0030\u20e3"] = ":zero:", + ["\U0001f910"] = ":zipper_mouth:", + ["\U0001f9df"] = ":zombie:", + ["\U0001f4a4"] = ":zzz:", + }.ToFrozenDictionary(); + #endregion + } +} diff --git a/DSharpPlus/Entities/Emoji/DiscordEmoji.cs b/DSharpPlus/Entities/Emoji/DiscordEmoji.cs index 098c6af84f..9632d24cb5 100644 --- a/DSharpPlus/Entities/Emoji/DiscordEmoji.cs +++ b/DSharpPlus/Entities/Emoji/DiscordEmoji.cs @@ -1,366 +1,366 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents a Discord emoji. -/// -public partial class DiscordEmoji : SnowflakeObject, IEquatable -{ - /// - /// Gets the name of this emoji. - /// - [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] - public string Name { get; internal set; } - - /// - /// Gets IDs the roles this emoji is enabled for. - /// - [JsonIgnore] - public IReadOnlyList Roles => this.roles; - - [JsonProperty("roles", NullValueHandling = NullValueHandling.Ignore)] - internal List roles; - - /// - /// Gets the user who uploaded this emoji. - /// - /// This property only applies to application-owned emojis. - [JsonProperty("user", NullValueHandling = NullValueHandling.Ignore)] - public DiscordUser? User { get; internal set; } - - /// - /// Gets whether this emoji requires colons to use. - /// - [JsonProperty("require_colons")] - public bool RequiresColons { get; internal set; } - - /// - /// Gets whether this emoji is managed by an integration. - /// - [JsonProperty("managed")] - public bool IsManaged { get; internal set; } - - /// - /// Gets whether this emoji is animated. - /// - [JsonProperty("animated")] - public bool IsAnimated { get; internal set; } - - /// - /// Gets the image URL of this emoji. - /// - [JsonIgnore] - public string Url => this.Id == 0 - ? throw new InvalidOperationException("Cannot get URL of unicode emojis.") - : this.IsAnimated - ? $"https://cdn.discordapp.com/emojis/{this.Id.ToString(CultureInfo.InvariantCulture)}.gif" - : $"https://cdn.discordapp.com/emojis/{this.Id.ToString(CultureInfo.InvariantCulture)}.png"; - - /// - /// Gets whether the emoji is available for use. - /// An emoji may not be available due to loss of server boost. - /// - [JsonProperty("available", NullValueHandling = NullValueHandling.Ignore)] - public bool IsAvailable { get; internal set; } - - internal DiscordEmoji() { } - - /// - /// Gets emoji's name in non-Unicode format (eg. :thinking: instead of the Unicode representation of the emoji). - /// - public string GetDiscordName() - { - DiscordNameLookup.TryGetValue(this.Name, out string? name); - - return name ?? $":{this.Name}:"; - } - - /// - /// Returns a string representation of this emoji. - /// - /// String representation of this emoji. - public override string ToString() => this.Id != 0 - ? this.IsAnimated - ? $"" - : $"<:{this.Name}:{this.Id.ToString(CultureInfo.InvariantCulture)}>" - : this.Name; - - /// - /// Checks whether this is equal to another object. - /// - /// Object to compare to. - /// Whether the object is equal to this . - public override bool Equals(object obj) => Equals(obj as DiscordEmoji); - - /// - /// Checks whether this is equal to another . - /// - /// to compare to. - /// Whether the is equal to this . - public bool Equals(DiscordEmoji e) => e is not null && (ReferenceEquals(this, e) || (this.Id == e.Id && (this.Id != 0 || this.Name == e.Name))); - - /// - /// Gets the hash code for this . - /// - /// The hash code for this . - public override int GetHashCode() - { - int hash = 13; - hash = (hash * 7) + this.Id.GetHashCode(); - hash = (hash * 7) + this.Name.GetHashCode(); - - return hash; - } - - internal string ToReactionString() - => this.Id != 0 ? $"{this.Name}:{this.Id.ToString(CultureInfo.InvariantCulture)}" : this.Name; - - /// - /// Gets whether the two objects are equal. - /// - /// First emoji to compare. - /// Second emoji to compare. - /// Whether the two emoji are equal. - public static bool operator ==(DiscordEmoji e1, DiscordEmoji e2) - { - object? o1 = e1; - object? o2 = e2; - - return o1 != null ^ o2 == null - && ((o1 == null && o2 == null) || (e1.Id == e2.Id && (e1.Id != 0 || e1.Name == e2.Name))); - } - - /// - /// Gets whether the two objects are not equal. - /// - /// First emoji to compare. - /// Second emoji to compare. - /// Whether the two emoji are not equal. - public static bool operator !=(DiscordEmoji e1, DiscordEmoji e2) - => !(e1 == e2); - - /// - /// Implicitly converts this emoji to its string representation. - /// - /// Emoji to convert. - public static implicit operator string(DiscordEmoji e1) - => e1.ToString(); - - /// - /// Checks whether specified unicode entity is a valid unicode emoji. - /// - /// Entity to check. - /// Whether it's a valid emoji. - public static bool IsValidUnicode(string unicodeEntity) - => DiscordNameLookup.ContainsKey(unicodeEntity); - - /// - /// Creates an emoji object from a unicode entity. - /// - /// to attach to the object. - /// Unicode entity to create the object from. - /// Create object. - public static DiscordEmoji FromUnicode(BaseDiscordClient client, string unicodeEntity) => !IsValidUnicode(unicodeEntity) - ? throw new ArgumentException("Specified unicode entity is not a valid unicode emoji.", nameof(unicodeEntity)) - : new DiscordEmoji { Name = unicodeEntity, Discord = client }; - - /// - /// Creates an emoji object from a unicode entity. - /// - /// Unicode entity to create the object from. - /// Create object. - public static DiscordEmoji FromUnicode(string unicodeEntity) - => FromUnicode(null, unicodeEntity); - - /// - /// Attempts to create an emoji object from a unicode entity. - /// - /// to attach to the object. - /// Unicode entity to create the object from. - /// Resulting object. - /// Whether the operation was successful. - public static bool TryFromUnicode(BaseDiscordClient client, string unicodeEntity, out DiscordEmoji emoji) - { - // this is a round-trip operation because of FE0F inconsistencies. - // through this, the inconsistency is normalized. - - emoji = null; - if (!DiscordNameLookup.TryGetValue(unicodeEntity, out string? discordName)) - { - return false; - } - - if (!UnicodeEmojis.TryGetValue(discordName, out unicodeEntity)) - { - return false; - } - - emoji = new DiscordEmoji { Name = unicodeEntity, Discord = client }; - return true; - } - - /// - /// Attempts to create an emoji object from a unicode entity. - /// - /// Unicode entity to create the object from. - /// Resulting object. - /// Whether the operation was successful. - public static bool TryFromUnicode(string unicodeEntity, out DiscordEmoji emoji) - => TryFromUnicode(null, unicodeEntity, out emoji); - - /// - /// Creates an emoji object from a guild emote. - /// - /// to attach to the object. - /// Id of the emote. - /// Create object. - public static DiscordEmoji FromGuildEmote(BaseDiscordClient client, ulong id) - { - if (client == null) - { - throw new ArgumentNullException(nameof(client), "Client cannot be null."); - } - - foreach (DiscordGuild guild in client.Guilds.Values) - { - if (guild.Emojis.TryGetValue(id, out DiscordEmoji? found)) - { - return found; - } - } - - throw new KeyNotFoundException("Given emote was not found."); - } - - /// - /// Attempts to create an emoji object from a guild emote. - /// - /// to attach to the object. - /// Id of the emote. - /// Resulting object. - /// Whether the operation was successful. - public static bool TryFromGuildEmote(BaseDiscordClient client, ulong id, out DiscordEmoji emoji) - { - if (client == null) - { - throw new ArgumentNullException(nameof(client), "Client cannot be null."); - } - - foreach (DiscordGuild guild in client.Guilds.Values) - { - if (guild.Emojis.TryGetValue(id, out emoji)) - { - return true; - } - } - - emoji = null; - return false; - } - - /// - /// Creates an emoji object from emote name that includes colons (eg. :thinking:). This method also supports - /// skin tone variations (eg. :ok_hand::skin-tone-2:), standard emoticons (eg. :D), as well as guild emoji - /// (still specified by :name:). - /// - /// to attach to the object. - /// Name of the emote to find, including colons (eg. :thinking:). - /// Should guild emojis be included in the search. - /// Create object. - public static DiscordEmoji FromName(BaseDiscordClient client, string name, bool includeGuilds = true) - { - if (client == null) - { - throw new ArgumentNullException(nameof(client), "Client cannot be null."); - } - else if (string.IsNullOrWhiteSpace(name)) - { - throw new ArgumentNullException(nameof(name), "Name cannot be empty or null."); - } - else if (name.Length < 2 || name[0] != ':' || name[^1] != ':') - { - throw new ArgumentException("Invalid emoji name specified. Ensure the emoji name starts and ends with ':'", nameof(name)); - } - - if (UnicodeEmojis.TryGetValue(name, out string? unicodeEntity)) - { - return new DiscordEmoji { Discord = client, Name = unicodeEntity }; - } - else if (includeGuilds) - { - name = name[1..^1]; // remove colons - foreach (DiscordGuild guild in client.Guilds.Values) - { - DiscordEmoji? found = guild.Emojis.Values.FirstOrDefault(emoji => emoji.Name == name); - if (found != null) - { - return found; - } - } - } - - throw new ArgumentException("Invalid emoji name specified.", nameof(name)); - } - - /// - /// Attempts to create an emoji object from emote name that includes colons (eg. :thinking:). This method also - /// supports skin tone variations (eg. :ok_hand::skin-tone-2:), standard emoticons (eg. :D), as well as guild - /// emoji (still specified by :name:). - /// - /// to attach to the object. - /// Name of the emote to find, including colons (eg. :thinking:). - /// Resulting object. - /// Whether the operation was successful. - public static bool TryFromName(BaseDiscordClient client, string name, out DiscordEmoji emoji) - => TryFromName(client, name, true, out emoji); - - /// - /// Attempts to create an emoji object from emote name that includes colons (eg. :thinking:). This method also - /// supports skin tone variations (eg. :ok_hand::skin-tone-2:), standard emoticons (eg. :D), as well as guild - /// emoji (still specified by :name:). - /// - /// to attach to the object. - /// Name of the emote to find, including colons (eg. :thinking:). - /// Should guild emojis be included in the search. - /// Resulting object. - /// Whether the operation was successful. - public static bool TryFromName(BaseDiscordClient client, string name, bool includeGuilds, out DiscordEmoji emoji) - { - if (client == null) - { - throw new ArgumentNullException(nameof(client), "Client cannot be null."); - } - // Checks if the emoji name is null - else if (string.IsNullOrWhiteSpace(name) || name.Length < 2 || name[0] != ':' || name[^1] != ':') - { - emoji = null; - return false; // invalid name - } - - if (UnicodeEmojis.TryGetValue(name, out string? unicodeEntity)) - { - emoji = new DiscordEmoji { Discord = client, Name = unicodeEntity }; - return true; - } - else if (includeGuilds) - { - name = name[1..^1]; // remove colons - foreach (DiscordGuild guild in client.Guilds.Values) - { - emoji = guild.Emojis.Values.FirstOrDefault(emoji => emoji.Name == name); - if (emoji != null) - { - return true; - } - } - } - - emoji = null; - return false; - } -} +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Represents a Discord emoji. +/// +public partial class DiscordEmoji : SnowflakeObject, IEquatable +{ + /// + /// Gets the name of this emoji. + /// + [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] + public string Name { get; internal set; } + + /// + /// Gets IDs the roles this emoji is enabled for. + /// + [JsonIgnore] + public IReadOnlyList Roles => this.roles; + + [JsonProperty("roles", NullValueHandling = NullValueHandling.Ignore)] + internal List roles; + + /// + /// Gets the user who uploaded this emoji. + /// + /// This property only applies to application-owned emojis. + [JsonProperty("user", NullValueHandling = NullValueHandling.Ignore)] + public DiscordUser? User { get; internal set; } + + /// + /// Gets whether this emoji requires colons to use. + /// + [JsonProperty("require_colons")] + public bool RequiresColons { get; internal set; } + + /// + /// Gets whether this emoji is managed by an integration. + /// + [JsonProperty("managed")] + public bool IsManaged { get; internal set; } + + /// + /// Gets whether this emoji is animated. + /// + [JsonProperty("animated")] + public bool IsAnimated { get; internal set; } + + /// + /// Gets the image URL of this emoji. + /// + [JsonIgnore] + public string Url => this.Id == 0 + ? throw new InvalidOperationException("Cannot get URL of unicode emojis.") + : this.IsAnimated + ? $"https://cdn.discordapp.com/emojis/{this.Id.ToString(CultureInfo.InvariantCulture)}.gif" + : $"https://cdn.discordapp.com/emojis/{this.Id.ToString(CultureInfo.InvariantCulture)}.png"; + + /// + /// Gets whether the emoji is available for use. + /// An emoji may not be available due to loss of server boost. + /// + [JsonProperty("available", NullValueHandling = NullValueHandling.Ignore)] + public bool IsAvailable { get; internal set; } + + internal DiscordEmoji() { } + + /// + /// Gets emoji's name in non-Unicode format (eg. :thinking: instead of the Unicode representation of the emoji). + /// + public string GetDiscordName() + { + DiscordNameLookup.TryGetValue(this.Name, out string? name); + + return name ?? $":{this.Name}:"; + } + + /// + /// Returns a string representation of this emoji. + /// + /// String representation of this emoji. + public override string ToString() => this.Id != 0 + ? this.IsAnimated + ? $"" + : $"<:{this.Name}:{this.Id.ToString(CultureInfo.InvariantCulture)}>" + : this.Name; + + /// + /// Checks whether this is equal to another object. + /// + /// Object to compare to. + /// Whether the object is equal to this . + public override bool Equals(object obj) => Equals(obj as DiscordEmoji); + + /// + /// Checks whether this is equal to another . + /// + /// to compare to. + /// Whether the is equal to this . + public bool Equals(DiscordEmoji e) => e is not null && (ReferenceEquals(this, e) || (this.Id == e.Id && (this.Id != 0 || this.Name == e.Name))); + + /// + /// Gets the hash code for this . + /// + /// The hash code for this . + public override int GetHashCode() + { + int hash = 13; + hash = (hash * 7) + this.Id.GetHashCode(); + hash = (hash * 7) + this.Name.GetHashCode(); + + return hash; + } + + internal string ToReactionString() + => this.Id != 0 ? $"{this.Name}:{this.Id.ToString(CultureInfo.InvariantCulture)}" : this.Name; + + /// + /// Gets whether the two objects are equal. + /// + /// First emoji to compare. + /// Second emoji to compare. + /// Whether the two emoji are equal. + public static bool operator ==(DiscordEmoji e1, DiscordEmoji e2) + { + object? o1 = e1; + object? o2 = e2; + + return o1 != null ^ o2 == null + && ((o1 == null && o2 == null) || (e1.Id == e2.Id && (e1.Id != 0 || e1.Name == e2.Name))); + } + + /// + /// Gets whether the two objects are not equal. + /// + /// First emoji to compare. + /// Second emoji to compare. + /// Whether the two emoji are not equal. + public static bool operator !=(DiscordEmoji e1, DiscordEmoji e2) + => !(e1 == e2); + + /// + /// Implicitly converts this emoji to its string representation. + /// + /// Emoji to convert. + public static implicit operator string(DiscordEmoji e1) + => e1.ToString(); + + /// + /// Checks whether specified unicode entity is a valid unicode emoji. + /// + /// Entity to check. + /// Whether it's a valid emoji. + public static bool IsValidUnicode(string unicodeEntity) + => DiscordNameLookup.ContainsKey(unicodeEntity); + + /// + /// Creates an emoji object from a unicode entity. + /// + /// to attach to the object. + /// Unicode entity to create the object from. + /// Create object. + public static DiscordEmoji FromUnicode(BaseDiscordClient client, string unicodeEntity) => !IsValidUnicode(unicodeEntity) + ? throw new ArgumentException("Specified unicode entity is not a valid unicode emoji.", nameof(unicodeEntity)) + : new DiscordEmoji { Name = unicodeEntity, Discord = client }; + + /// + /// Creates an emoji object from a unicode entity. + /// + /// Unicode entity to create the object from. + /// Create object. + public static DiscordEmoji FromUnicode(string unicodeEntity) + => FromUnicode(null, unicodeEntity); + + /// + /// Attempts to create an emoji object from a unicode entity. + /// + /// to attach to the object. + /// Unicode entity to create the object from. + /// Resulting object. + /// Whether the operation was successful. + public static bool TryFromUnicode(BaseDiscordClient client, string unicodeEntity, out DiscordEmoji emoji) + { + // this is a round-trip operation because of FE0F inconsistencies. + // through this, the inconsistency is normalized. + + emoji = null; + if (!DiscordNameLookup.TryGetValue(unicodeEntity, out string? discordName)) + { + return false; + } + + if (!UnicodeEmojis.TryGetValue(discordName, out unicodeEntity)) + { + return false; + } + + emoji = new DiscordEmoji { Name = unicodeEntity, Discord = client }; + return true; + } + + /// + /// Attempts to create an emoji object from a unicode entity. + /// + /// Unicode entity to create the object from. + /// Resulting object. + /// Whether the operation was successful. + public static bool TryFromUnicode(string unicodeEntity, out DiscordEmoji emoji) + => TryFromUnicode(null, unicodeEntity, out emoji); + + /// + /// Creates an emoji object from a guild emote. + /// + /// to attach to the object. + /// Id of the emote. + /// Create object. + public static DiscordEmoji FromGuildEmote(BaseDiscordClient client, ulong id) + { + if (client == null) + { + throw new ArgumentNullException(nameof(client), "Client cannot be null."); + } + + foreach (DiscordGuild guild in client.Guilds.Values) + { + if (guild.Emojis.TryGetValue(id, out DiscordEmoji? found)) + { + return found; + } + } + + throw new KeyNotFoundException("Given emote was not found."); + } + + /// + /// Attempts to create an emoji object from a guild emote. + /// + /// to attach to the object. + /// Id of the emote. + /// Resulting object. + /// Whether the operation was successful. + public static bool TryFromGuildEmote(BaseDiscordClient client, ulong id, out DiscordEmoji emoji) + { + if (client == null) + { + throw new ArgumentNullException(nameof(client), "Client cannot be null."); + } + + foreach (DiscordGuild guild in client.Guilds.Values) + { + if (guild.Emojis.TryGetValue(id, out emoji)) + { + return true; + } + } + + emoji = null; + return false; + } + + /// + /// Creates an emoji object from emote name that includes colons (eg. :thinking:). This method also supports + /// skin tone variations (eg. :ok_hand::skin-tone-2:), standard emoticons (eg. :D), as well as guild emoji + /// (still specified by :name:). + /// + /// to attach to the object. + /// Name of the emote to find, including colons (eg. :thinking:). + /// Should guild emojis be included in the search. + /// Create object. + public static DiscordEmoji FromName(BaseDiscordClient client, string name, bool includeGuilds = true) + { + if (client == null) + { + throw new ArgumentNullException(nameof(client), "Client cannot be null."); + } + else if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentNullException(nameof(name), "Name cannot be empty or null."); + } + else if (name.Length < 2 || name[0] != ':' || name[^1] != ':') + { + throw new ArgumentException("Invalid emoji name specified. Ensure the emoji name starts and ends with ':'", nameof(name)); + } + + if (UnicodeEmojis.TryGetValue(name, out string? unicodeEntity)) + { + return new DiscordEmoji { Discord = client, Name = unicodeEntity }; + } + else if (includeGuilds) + { + name = name[1..^1]; // remove colons + foreach (DiscordGuild guild in client.Guilds.Values) + { + DiscordEmoji? found = guild.Emojis.Values.FirstOrDefault(emoji => emoji.Name == name); + if (found != null) + { + return found; + } + } + } + + throw new ArgumentException("Invalid emoji name specified.", nameof(name)); + } + + /// + /// Attempts to create an emoji object from emote name that includes colons (eg. :thinking:). This method also + /// supports skin tone variations (eg. :ok_hand::skin-tone-2:), standard emoticons (eg. :D), as well as guild + /// emoji (still specified by :name:). + /// + /// to attach to the object. + /// Name of the emote to find, including colons (eg. :thinking:). + /// Resulting object. + /// Whether the operation was successful. + public static bool TryFromName(BaseDiscordClient client, string name, out DiscordEmoji emoji) + => TryFromName(client, name, true, out emoji); + + /// + /// Attempts to create an emoji object from emote name that includes colons (eg. :thinking:). This method also + /// supports skin tone variations (eg. :ok_hand::skin-tone-2:), standard emoticons (eg. :D), as well as guild + /// emoji (still specified by :name:). + /// + /// to attach to the object. + /// Name of the emote to find, including colons (eg. :thinking:). + /// Should guild emojis be included in the search. + /// Resulting object. + /// Whether the operation was successful. + public static bool TryFromName(BaseDiscordClient client, string name, bool includeGuilds, out DiscordEmoji emoji) + { + if (client == null) + { + throw new ArgumentNullException(nameof(client), "Client cannot be null."); + } + // Checks if the emoji name is null + else if (string.IsNullOrWhiteSpace(name) || name.Length < 2 || name[0] != ':' || name[^1] != ':') + { + emoji = null; + return false; // invalid name + } + + if (UnicodeEmojis.TryGetValue(name, out string? unicodeEntity)) + { + emoji = new DiscordEmoji { Discord = client, Name = unicodeEntity }; + return true; + } + else if (includeGuilds) + { + name = name[1..^1]; // remove colons + foreach (DiscordGuild guild in client.Guilds.Values) + { + emoji = guild.Emojis.Values.FirstOrDefault(emoji => emoji.Name == name); + if (emoji != null) + { + return true; + } + } + } + + emoji = null; + return false; + } +} diff --git a/DSharpPlus/Entities/Guild/DiscordBan.cs b/DSharpPlus/Entities/Guild/DiscordBan.cs index 59e0b44fc9..3689d67364 100644 --- a/DSharpPlus/Entities/Guild/DiscordBan.cs +++ b/DSharpPlus/Entities/Guild/DiscordBan.cs @@ -1,26 +1,26 @@ -using DSharpPlus.Net.Abstractions; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents a Discord ban -/// -public class DiscordBan -{ - /// - /// Gets the reason for the ban - /// - [JsonProperty("reason", NullValueHandling = NullValueHandling.Ignore)] - public string Reason { get; internal set; } - - /// - /// Gets the banned user - /// - [JsonIgnore] - public DiscordUser User { get; internal set; } - [JsonProperty("user", NullValueHandling = NullValueHandling.Ignore)] - internal TransportUser RawUser { get; set; } - - internal DiscordBan() { } -} +using DSharpPlus.Net.Abstractions; +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Represents a Discord ban +/// +public class DiscordBan +{ + /// + /// Gets the reason for the ban + /// + [JsonProperty("reason", NullValueHandling = NullValueHandling.Ignore)] + public string Reason { get; internal set; } + + /// + /// Gets the banned user + /// + [JsonIgnore] + public DiscordUser User { get; internal set; } + [JsonProperty("user", NullValueHandling = NullValueHandling.Ignore)] + internal TransportUser RawUser { get; set; } + + internal DiscordBan() { } +} diff --git a/DSharpPlus/Entities/Guild/DiscordBulkBan.cs b/DSharpPlus/Entities/Guild/DiscordBulkBan.cs index 8be52a933e..a5996358e5 100644 --- a/DSharpPlus/Entities/Guild/DiscordBulkBan.cs +++ b/DSharpPlus/Entities/Guild/DiscordBulkBan.cs @@ -1,32 +1,32 @@ -using System.Collections.Generic; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Result of a bulk ban. Contains the ids of users that were successfully banned and the ids of users that failed to be banned. -/// -public class DiscordBulkBan -{ - /// - /// Ids of users that were successfully banned. - /// - [JsonProperty("banned_users")] - public IEnumerable BannedUserIds { get; internal set; } - - /// - /// Ids of users that failed to be banned (Already banned or not possible to ban). - /// - [JsonProperty("failed_users")] - public IEnumerable FailedUserIds { get; internal set; } - - /// - /// Users that were successfully banned. - /// - public IEnumerable BannedUsers { get; internal set; } - - /// - /// Users that failed to be banned (Already banned or not possible to ban). - /// - public IEnumerable FailedUsers { get; internal set; } -} +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Result of a bulk ban. Contains the ids of users that were successfully banned and the ids of users that failed to be banned. +/// +public class DiscordBulkBan +{ + /// + /// Ids of users that were successfully banned. + /// + [JsonProperty("banned_users")] + public IEnumerable BannedUserIds { get; internal set; } + + /// + /// Ids of users that failed to be banned (Already banned or not possible to ban). + /// + [JsonProperty("failed_users")] + public IEnumerable FailedUserIds { get; internal set; } + + /// + /// Users that were successfully banned. + /// + public IEnumerable BannedUsers { get; internal set; } + + /// + /// Users that failed to be banned (Already banned or not possible to ban). + /// + public IEnumerable FailedUsers { get; internal set; } +} diff --git a/DSharpPlus/Entities/Guild/DiscordGuild.cs b/DSharpPlus/Entities/Guild/DiscordGuild.cs index bb2c62e54b..a1794083ac 100644 --- a/DSharpPlus/Entities/Guild/DiscordGuild.cs +++ b/DSharpPlus/Entities/Guild/DiscordGuild.cs @@ -1,2767 +1,2767 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Threading; -using System.Threading.Channels; -using System.Threading.Tasks; -using DSharpPlus.Entities.AuditLogs; -using DSharpPlus.EventArgs; -using DSharpPlus.Exceptions; -using DSharpPlus.Net; -using DSharpPlus.Net.Abstractions; -using DSharpPlus.Net.Models; -using DSharpPlus.Net.Serialization; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents a Discord guild. -/// -public class DiscordGuild : SnowflakeObject, IEquatable -{ - /// - /// Gets the guild's name. - /// - [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] - public string Name { get; internal set; } - - /// - /// Gets the guild icon's hash. - /// - [JsonProperty("icon", NullValueHandling = NullValueHandling.Ignore)] - public string IconHash { get; internal set; } - - /// - /// Gets the guild icon's url. - /// - [JsonIgnore] - public string IconUrl - => GetIconUrl(MediaFormat.Auto, 1024); - - /// - /// Gets the guild splash's hash. - /// - [JsonProperty("splash", NullValueHandling = NullValueHandling.Ignore)] - public string SplashHash { get; internal set; } - - /// - /// Gets the guild splash's url. - /// - [JsonIgnore] - public string? SplashUrl - => !string.IsNullOrWhiteSpace(this.SplashHash) ? $"https://cdn.discordapp.com/splashes/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.SplashHash}.jpg" : null; - - /// - /// Gets the guild discovery splash's hash. - /// - [JsonProperty("discovery_splash", NullValueHandling = NullValueHandling.Ignore)] - public string DiscoverySplashHash { get; internal set; } - - /// - /// Gets the guild discovery splash's url. - /// - [JsonIgnore] - public string? DiscoverySplashUrl - => !string.IsNullOrWhiteSpace(this.DiscoverySplashHash) ? $"https://cdn.discordapp.com/discovery-splashes/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.DiscoverySplashHash}.jpg" : null; - - /// - /// Gets the preferred locale of this guild. - /// This is used for server discovery and notices from Discord. Defaults to en-US. - /// - [JsonProperty("preferred_locale", NullValueHandling = NullValueHandling.Ignore)] - public string PreferredLocale { get; internal set; } - - /// - /// Gets the ID of the guild's owner. - /// - [JsonProperty("owner_id", NullValueHandling = NullValueHandling.Ignore)] - public ulong OwnerId { get; internal set; } - - /// - /// Gets permissions for the user in the guild (does not include channel overrides) - /// - [JsonProperty("permissions", NullValueHandling = NullValueHandling.Ignore)] - public DiscordPermissions? Permissions { get; set; } - - /// - /// Gets the guild's owner. - /// - public async Task GetGuildOwnerAsync() - { - return this.Members.TryGetValue(this.OwnerId, out DiscordMember? owner) - ? owner - : await this.Discord.ApiClient.GetGuildMemberAsync(this.Id, this.OwnerId); - } - - /// - /// Gets the guild's voice region ID. - /// - [JsonProperty("region", NullValueHandling = NullValueHandling.Ignore)] - internal string voiceRegionId { get; set; } - - /// - /// Gets the guild's voice region. - /// - [JsonIgnore] - public DiscordVoiceRegion VoiceRegion - => this.Discord.VoiceRegions[this.voiceRegionId]; - - /// - /// Gets the guild's AFK voice channel ID. - /// - [JsonProperty("afk_channel_id", NullValueHandling = NullValueHandling.Ignore)] - public ulong? AfkChannelId { get; internal set; } - - /// - /// Gets the guild's AFK voice channel. - /// - /// If set to true this method will skip all caches and always perform a rest api call - /// Returns null if the guild has no AFK channel - public async Task GetAfkChannelAsync(bool skipCache = false) - { - if (this.AfkChannelId is null) - { - return null; - } - - return await GetChannelAsync(this.AfkChannelId.Value); - } - - /// - /// Gets the guild's AFK timeout. - /// - [JsonProperty("afk_timeout", NullValueHandling = NullValueHandling.Ignore)] - public int AfkTimeout { get; internal set; } - - /// - /// Gets the guild's verification level. - /// - [JsonProperty("verification_level", NullValueHandling = NullValueHandling.Ignore)] - public DiscordVerificationLevel VerificationLevel { get; internal set; } - - /// - /// Gets the guild's default notification settings. - /// - [JsonProperty("default_message_notifications", NullValueHandling = NullValueHandling.Ignore)] - public DiscordDefaultMessageNotifications DefaultMessageNotifications { get; internal set; } - - /// - /// Gets the guild's explicit content filter settings. - /// - [JsonProperty("explicit_content_filter")] - public DiscordExplicitContentFilter ExplicitContentFilter { get; internal set; } - - /// - /// Gets the guild's nsfw level. - /// - [JsonProperty("nsfw_level")] - public DiscordNsfwLevel NsfwLevel { get; internal set; } - - /// - /// Id of the channel where system messages (such as boost and welcome messages) are sent. - /// - [JsonProperty("system_channel_id", NullValueHandling = NullValueHandling.Include)] - public ulong? SystemChannelId { get; internal set; } - - /// - /// Gets the channel where system messages (such as boost and welcome messages) are sent. - /// - /// If set to true this method will skip all caches and always perform a rest api call - /// Returns null if the guild has no configured system channel. - public async Task GetSystemChannelAsync(bool skipCache = false) - { - if (this.SystemChannelId is null) - { - return null; - } - - return await GetChannelAsync(this.SystemChannelId.Value); - } - - /// - /// Gets the settings for this guild's system channel. - /// - [JsonProperty("system_channel_flags")] - public DiscordSystemChannelFlags SystemChannelFlags { get; internal set; } - - /// - /// Id of the channel where safety alerts are sent to - /// - [JsonProperty("safety_alerts_channel_id")] - public ulong? SafetyAlertsChannelId { get; internal set; } - - /// - /// Gets the guild's safety alerts channel. - /// - /// If set to true this method will skip all caches and always perform a rest api call - ///Returns null if the guild has no configured safety alerts channel. - public async Task GetSafetyAlertsChannelAsync(bool skipCache = false) - { - if (this.SafetyAlertsChannelId is null) - { - return null; - } - - return await GetChannelAsync(this.SafetyAlertsChannelId.Value); - } - - /// - /// Gets whether this guild's widget is enabled. - /// - [JsonProperty("widget_enabled", NullValueHandling = NullValueHandling.Ignore)] - public bool? WidgetEnabled { get; internal set; } - - /// - /// Id of the widget channel - /// - [JsonProperty("widget_channel_id", NullValueHandling = NullValueHandling.Ignore)] - public ulong? WidgetChannelId { get; internal set; } - - /// - /// Gets the widget channel for this guild. - /// - /// If set to true this method will skip all caches and always perform a rest api call - /// Returns null if the guild has no widget channel configured. - public async Task GetWidgetChannelAsync(bool skipCache = false) - { - if (this.WidgetChannelId is null) - { - return null; - } - - return await GetChannelAsync(this.WidgetChannelId.Value); - } - - /// - /// Id of the rules channel of this guild. Null if the guild has no configured rules channel. - /// - [JsonProperty("rules_channel_id")] - public ulong? RulesChannelId { get; internal set; } - - /// - /// Gets the rules channel for this guild. - /// This is only available if the guild is considered "discoverable". - /// - /// If set to true this method will skip all caches and always perform a rest api call - /// Returns null if the guild has no rules channel configured - public async Task GetRulesChannelAsync(bool skipCache = false) - { - if (this.RulesChannelId is null) - { - return null; - } - - return await GetChannelAsync(this.RulesChannelId.Value); - } - - /// - /// Id of the channel where admins and moderators receive messages from Discord - /// - [JsonProperty("public_updates_channel_id")] - public ulong? PublicUpdatesChannelId { get; internal set; } - - /// - /// Gets the public updates channel (where admins and moderators receive messages from Discord) for this guild. - /// This is only available if the guild is considered "discoverable". - /// - /// If set to true this method will skip all caches and always perform a rest api call - /// Returns null if the guild has no public updates channel configured - public async Task GetPublicUpdatesChannelAsync(bool skipCache = false) - { - if (this.PublicUpdatesChannelId is null) - { - return null; - } - - return await GetChannelAsync(this.PublicUpdatesChannelId.Value); - } - - /// - /// Gets the application ID of this guild if it is bot created. - /// - [JsonProperty("application_id")] - public ulong? ApplicationId { get; internal set; } - - /// - /// Scheduled events for this guild. - /// - public IReadOnlyDictionary ScheduledEvents - => this.scheduledEvents; - - [JsonProperty("guild_scheduled_events")] - [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] - internal ConcurrentDictionary scheduledEvents = new(); - - /// - /// Gets a collection of this guild's roles. - /// - [JsonIgnore] - public IReadOnlyDictionary Roles => this.roles; - - [JsonProperty("roles", NullValueHandling = NullValueHandling.Ignore)] - [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] - internal ConcurrentDictionary roles; - - /// - /// Gets a collection of this guild's stickers. - /// - [JsonIgnore] - public IReadOnlyDictionary Stickers => this.stickers; - - [JsonProperty("stickers", NullValueHandling = NullValueHandling.Ignore)] - [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] - internal ConcurrentDictionary stickers = new(); - - /// - /// Gets a collection of this guild's emojis. - /// - [JsonIgnore] - public IReadOnlyDictionary Emojis => this.emojis; - - [JsonProperty("emojis", NullValueHandling = NullValueHandling.Ignore)] - [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] - internal ConcurrentDictionary emojis; - - /// - /// Gets a collection of this guild's features. - /// - [JsonProperty("features", NullValueHandling = NullValueHandling.Ignore)] - public IReadOnlyList Features { get; internal set; } - - /// - /// Gets the required multi-factor authentication level for this guild. - /// - [JsonProperty("mfa_level", NullValueHandling = NullValueHandling.Ignore)] - public DiscordMfaLevel MfaLevel { get; internal set; } - - /// - /// Gets this guild's join date. - /// - [JsonProperty("joined_at", NullValueHandling = NullValueHandling.Ignore)] - public DateTimeOffset JoinedAt { get; internal set; } - - /// - /// Gets whether this guild is considered to be a large guild. - /// - [JsonProperty("large", NullValueHandling = NullValueHandling.Ignore)] - public bool IsLarge { get; internal set; } - - /// - /// Gets whether this guild is unavailable. - /// - [JsonProperty("unavailable", NullValueHandling = NullValueHandling.Ignore)] - public bool IsUnavailable { get; internal set; } - - /// - /// Gets the total number of members in this guild. - /// - [JsonProperty("member_count", NullValueHandling = NullValueHandling.Ignore)] - public int MemberCount { get; internal set; } - - /// - /// Gets the maximum amount of members allowed for this guild. - /// - [JsonProperty("max_members")] - public int? MaxMembers { get; internal set; } - - /// - /// Gets the maximum amount of presences allowed for this guild. - /// - [JsonProperty("max_presences")] - public int? MaxPresences { get; internal set; } - -#pragma warning disable CS1734 - /// - /// Gets the approximate number of members in this guild, when using and having set to true. - /// - [JsonProperty("approximate_member_count", NullValueHandling = NullValueHandling.Ignore)] - public int? ApproximateMemberCount { get; internal set; } - - /// - /// Gets the approximate number of presences in this guild, when using and having set to true. - /// - [JsonProperty("approximate_presence_count", NullValueHandling = NullValueHandling.Ignore)] - public int? ApproximatePresenceCount { get; internal set; } -#pragma warning restore CS1734 - - /// - /// Gets the maximum amount of users allowed per video channel. - /// - [JsonProperty("max_video_channel_users", NullValueHandling = NullValueHandling.Ignore)] - public int? MaxVideoChannelUsers { get; internal set; } - - /// - /// Gets a dictionary of all the voice states for this guild. The key for this dictionary is the ID of the user - /// the voice state corresponds to. - /// - [JsonIgnore] - public IReadOnlyDictionary VoiceStates => this.voiceStates; - - [JsonProperty("voice_states", NullValueHandling = NullValueHandling.Ignore)] - [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] - internal ConcurrentDictionary voiceStates = new(); - - /// - /// Gets a dictionary of all the members that belong to this guild. The dictionary's key is the member ID. - /// - [JsonIgnore] - public IReadOnlyDictionary Members => this.members; - - [JsonProperty("members", NullValueHandling = NullValueHandling.Ignore)] - [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] - internal ConcurrentDictionary members; - - /// - /// Gets a dictionary of all the channels associated with this guild. The dictionary's key is the channel ID. - /// - [JsonIgnore] - public IReadOnlyDictionary Channels => this.channels; - - [JsonProperty("channels", NullValueHandling = NullValueHandling.Ignore)] - [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] - internal ConcurrentDictionary channels; - - /// - /// Gets a dictionary of all the active threads associated with this guild the user has permission to view. The dictionary's key is the channel ID. - /// - [JsonIgnore] - public IReadOnlyDictionary Threads => this.threads; - - [JsonProperty("threads", NullValueHandling = NullValueHandling.Ignore)] - [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] - internal ConcurrentDictionary threads = new(); - - internal ConcurrentDictionary invites; - - /// - /// Gets the guild member for current user. - /// - [JsonIgnore] - public DiscordMember CurrentMember => this.members != null && this.members.TryGetValue(this.Discord.CurrentUser.Id, out DiscordMember? member) ? member : null; - - /// - /// Gets the @everyone role for this guild. - /// - [JsonIgnore] - public DiscordRole EveryoneRole - => this.Roles.GetValueOrDefault(this.Id)!; - - [JsonIgnore] - internal bool isOwner; - - /// - /// Gets whether the current user is the guild's owner. - /// - [JsonProperty("owner", NullValueHandling = NullValueHandling.Ignore)] - public bool IsOwner - { - get => this.isOwner || this.OwnerId == this.Discord.CurrentUser.Id; - internal set => this.isOwner = value; - } - - /// - /// Gets the vanity URL code for this guild, when applicable. - /// - [JsonProperty("vanity_url_code")] - public string VanityUrlCode { get; internal set; } - - /// - /// Gets the guild description, when applicable. - /// - [JsonProperty("description")] - public string Description { get; internal set; } - - /// - /// Gets this guild's banner hash, when applicable. - /// - [JsonProperty("banner")] - public string Banner { get; internal set; } - - /// - /// Gets this guild's banner in url form. - /// - [JsonIgnore] - public string? BannerUrl - => !string.IsNullOrWhiteSpace(this.Banner) ? $"https://cdn.discordapp.com/banners/{this.Id}/{this.Banner}" : null; - - /// - /// Gets this guild's premium tier (Nitro boosting). - /// - [JsonProperty("premium_tier")] - public DiscordPremiumTier PremiumTier { get; internal set; } - - /// - /// Gets the amount of members that boosted this guild. - /// - [JsonProperty("premium_subscription_count", NullValueHandling = NullValueHandling.Ignore)] - public int? PremiumSubscriptionCount { get; internal set; } - - /// - /// Whether the guild has the boost progress bar enabled. - /// - [JsonProperty("premium_progress_bar_enabled", NullValueHandling = NullValueHandling.Ignore)] - public bool PremiumProgressBarEnabled { get; internal set; } - - /// - /// Gets whether this guild is designated as NSFW. - /// - [JsonProperty("nsfw", NullValueHandling = NullValueHandling.Ignore)] - public bool IsNSFW { get; internal set; } - - /// - /// Gets the stage instances in this guild. - /// - [JsonIgnore] - public IReadOnlyDictionary StageInstances => this.stageInstances; - - [JsonProperty("stage_instances", NullValueHandling = NullValueHandling.Ignore)] - [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] - internal ConcurrentDictionary stageInstances; - - // Failed attempts so far: 8 - // Velvet got it working in one attempt. I'm not mad, why would I be mad. - Lunar - /// - /// Gets channels ordered in a manner in which they'd be ordered in the UI of the discord client. - /// - [JsonIgnore] - // Group the channels by category or parent id - public IEnumerable OrderedChannels => this.channels.Values.GroupBy(channel => channel.IsCategory ? channel.Id : channel.ParentId) - // Order the channel by the category's position - .OrderBy(channels => channels.FirstOrDefault(channel => channel.IsCategory)?.Position) - // Select the category's channels - // Order them by text, shoving voice or stage types to the bottom - // Then order them by their position - .Select(channel => channel.OrderBy(channel => channel.Type is DiscordChannelType.Voice or DiscordChannelType.Stage).ThenBy(channel => channel.Position)) - // Group them all back together into a single enumerable. - .SelectMany(channel => channel); - - [JsonIgnore] - internal bool isSynced { get; set; } - - internal DiscordGuild() => this.invites = new ConcurrentDictionary(); - - #region Guild Methods - - /// - /// Gets guild's icon URL, in requested format and size. - /// - /// The image format of the icon to get. - /// The maximum size of the icon. Must be a power of two, minimum 16, maximum 4096. - /// The URL of the guild's icon. - public string? GetIconUrl(MediaFormat imageFormat, ushort imageSize = 1024) - { - - if (string.IsNullOrWhiteSpace(this.IconHash)) - { - return null; - } - - if (imageFormat == MediaFormat.Unknown) - { - throw new ArgumentException("You must specify valid image format.", nameof(imageFormat)); - } - - // Makes sure the image size is in between Discord's allowed range. - if (imageSize is < 16 or > 4096) - { - throw new ArgumentOutOfRangeException(nameof(imageSize), imageSize, "Image Size is not in between 16 and 4096."); - } - - // Checks to see if the image size is not a power of two. - if (!(imageSize is not 0 && (imageSize & (imageSize - 1)) is 0)) - { - throw new ArgumentOutOfRangeException(nameof(imageSize), imageSize, "Image size is not a power of two."); - } - - // Get the string variants of the method parameters to use in the urls. - string stringImageFormat = imageFormat switch - { - MediaFormat.Gif => "gif", - MediaFormat.Jpeg => "jpg", - MediaFormat.Png => "png", - MediaFormat.WebP => "webp", - MediaFormat.Auto => !string.IsNullOrWhiteSpace(this.IconHash) ? this.IconHash.StartsWith("a_") ? "gif" : "png" : "png", - _ => throw new ArgumentOutOfRangeException(nameof(imageFormat)), - }; - string stringImageSize = imageSize.ToString(CultureInfo.InvariantCulture); - - return $"https://cdn.discordapp.com/{Endpoints.ICONS}/{this.Id}/{this.IconHash}.{stringImageFormat}?size={stringImageSize}"; - - } - - /// - /// Creates a new scheduled event in this guild. - /// - /// The name of the event to create, up to 100 characters. - /// The description of the event, up to 1000 characters. - /// If a or , the id of the channel the event will be hosted in - /// The type of the event. must be supplied if not an external event. - /// The privacy level of thi - /// When this event starts. Must be in the future, and before the end date. - /// When this event ends. If supplied, must be in the future and after the end date. This is required for . - /// Where this event takes place, up to 100 characters. Only applicable if the type is - /// A cover image for this event. - /// Reason for audit log. - /// The created event. - public async Task CreateEventAsync(string name, string description, ulong? channelId, DiscordScheduledGuildEventType type, DiscordScheduledGuildEventPrivacyLevel privacyLevel, DateTimeOffset start, DateTimeOffset? end, string? location = null, Stream? image = null, string? reason = null) - { - ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(start, DateTimeOffset.Now); - if (end != null && end <= start) - { - throw new ArgumentOutOfRangeException(nameof(end), "The end time for an event must be after the start time."); - } - - DiscordScheduledGuildEventMetadata? metadata = null; - switch (type) - { - case DiscordScheduledGuildEventType.StageInstance or DiscordScheduledGuildEventType.VoiceChannel when channelId == null: - throw new ArgumentException($"{nameof(channelId)} must not be null when type is {type}", nameof(channelId)); - case DiscordScheduledGuildEventType.External when channelId != null: - throw new ArgumentException($"{nameof(channelId)} must be null when using external event type", nameof(channelId)); - case DiscordScheduledGuildEventType.External when location == null: - throw new ArgumentException($"{nameof(location)} must not be null when using external event type", nameof(location)); - case DiscordScheduledGuildEventType.External when end == null: - throw new ArgumentException($"{nameof(end)} must not be null when using external event type", nameof(end)); - } - - if (!string.IsNullOrEmpty(location)) - { - metadata = new DiscordScheduledGuildEventMetadata() - { - Location = location - }; - } - - return await this.Discord.ApiClient.CreateScheduledGuildEventAsync(this.Id, name, description, start, type, privacyLevel, metadata, end, channelId, image, reason); - } - - /// - /// Starts a scheduled event in this guild. - /// - /// The event to cancel. - /// - /// - public Task StartEventAsync(DiscordScheduledGuildEvent guildEvent) => guildEvent.Status is not DiscordScheduledGuildEventStatus.Scheduled - ? throw new InvalidOperationException("The event must be scheduled for it to be started.") - : ModifyEventAsync(guildEvent, m => m.Status = DiscordScheduledGuildEventStatus.Active); - - /// - /// Cancels an event. The event must be scheduled for it to be cancelled. - /// - /// The event to delete. - public Task CancelEventAsync(DiscordScheduledGuildEvent guildEvent) => guildEvent.Status is not DiscordScheduledGuildEventStatus.Scheduled - ? throw new InvalidOperationException("The event must be scheduled for it to be cancelled.") - : ModifyEventAsync(guildEvent, m => m.Status = DiscordScheduledGuildEventStatus.Cancelled); - - /// - /// Modifies an existing scheduled event in this guild. - /// - /// The event to modify. - /// The action to perform on this event - /// The reason this event is being modified - /// The modified object - /// - public async Task ModifyEventAsync(DiscordScheduledGuildEvent guildEvent, Action mdl, string? reason = null) - { - ScheduledGuildEventEditModel model = new(); - mdl(model); - - if (model.Type.HasValue && model.Type.Value is not DiscordScheduledGuildEventType.External) - { - if (!model.Channel.HasValue) - { - throw new ArgumentException("Channel must be supplied if the event is a stage instance or voice channel event."); - } - - if (model.Type.Value is DiscordScheduledGuildEventType.StageInstance && model.Channel.Value.Type is not DiscordChannelType.Stage) - { - throw new ArgumentException("Channel must be a stage channel if the event is a stage instance event."); - } - - if (model.Type.Value is DiscordScheduledGuildEventType.VoiceChannel && model.Channel.Value.Type is not DiscordChannelType.Voice) - { - throw new ArgumentException("Channel must be a voice channel if the event is a voice channel event."); - } - - if (model.EndTime.HasValue && model.EndTime.Value < guildEvent.StartTime) - { - throw new ArgumentException("End time must be after the start time."); - } - } - - if (model.Type.HasValue && model.Type.Value is DiscordScheduledGuildEventType.External) - { - if (!model.EndTime.HasValue) - { - throw new ArgumentException("End must be supplied if the event is an external event."); - } - - if (!model.Metadata.HasValue || string.IsNullOrEmpty(model.Metadata.Value.Location)) - { - throw new ArgumentException("Location must be supplied if the event is an external event."); - } - - if (model.Channel.HasValue && model.Channel.Value != null) - { - throw new ArgumentException("Channel must not be supplied if the event is an external event."); - } - } - - if (guildEvent.Status is DiscordScheduledGuildEventStatus.Completed) - { - throw new ArgumentException("The event must not be completed for it to be modified."); - } - - if (guildEvent.Status is DiscordScheduledGuildEventStatus.Cancelled) - { - throw new ArgumentException("The event must not be cancelled for it to be modified."); - } - - if (model.Status.HasValue) - { - switch (model.Status.Value) - { - case DiscordScheduledGuildEventStatus.Scheduled: - throw new ArgumentException("Status must not be set to scheduled."); - case DiscordScheduledGuildEventStatus.Active when guildEvent.Status is not DiscordScheduledGuildEventStatus.Scheduled: - throw new ArgumentException("Event status must be scheduled to progress to active."); - case DiscordScheduledGuildEventStatus.Completed when guildEvent.Status is not DiscordScheduledGuildEventStatus.Active: - throw new ArgumentException("Event status must be active to progress to completed."); - case DiscordScheduledGuildEventStatus.Cancelled when guildEvent.Status is not DiscordScheduledGuildEventStatus.Scheduled: - throw new ArgumentException("Event status must be scheduled to progress to cancelled."); - } - } - - DiscordScheduledGuildEvent modifiedEvent = await this.Discord.ApiClient.ModifyScheduledGuildEventAsync - ( - this.Id, - guildEvent.Id, - model.Name, - model.Description, - model.Channel.IfPresent(c => c?.Id), - model.StartTime, - model.EndTime, - model.Type, - model.PrivacyLevel, - model.Metadata, - model.Status, - model.CoverImage, - reason - ); - - this.scheduledEvents[modifiedEvent.Id] = modifiedEvent; - } - - /// - /// Deletes an exising scheduled event in this guild. - /// - /// - /// The reason which should be used for the audit log - /// - public async Task DeleteEventAsync(DiscordScheduledGuildEvent guildEvent, string? reason = null) - { - this.scheduledEvents.TryRemove(guildEvent.Id, out _); - await this.Discord.ApiClient.DeleteScheduledGuildEventAsync(this.Id, guildEvent.Id, reason); - } - - /// - /// Deletes an exising scheduled event in this guild. - /// - /// The Id of the event which should be deleted. - /// The reason which should be used for the audit log - /// - public async Task DeleteEventAsync(ulong guildEventId, string? reason = null) - { - this.scheduledEvents.TryRemove(guildEventId, out _); - await this.Discord.ApiClient.DeleteScheduledGuildEventAsync(this.Id, guildEventId, reason); - } - - /// - /// Gets the currently active or scheduled events in this guild. - /// - /// Whether to include number of users subscribed to each event - /// The active and scheduled events on the server, if any. - public async Task> GetEventsAsync(bool withUserCounts = false) - { - IReadOnlyList events = await this.Discord.ApiClient.GetScheduledGuildEventsAsync(this.Id, withUserCounts); - - foreach (DiscordScheduledGuildEvent @event in events) - { - this.scheduledEvents[@event.Id] = @event; - } - - return events; - } - - /// - /// Gets a list of users who are interested in this event. - /// - /// The event to query users from - /// How many users to fetch. - /// Fetch users after this id. Mutually exclusive with before - /// Fetch users before this id. Mutually exclusive with after - public IAsyncEnumerable GetEventUsersAsync - ( - DiscordScheduledGuildEvent guildEvent, - int limit = 100, - ulong? after = null, - ulong? before = null - ) - => GetEventUsersAsync(guildEvent.Id, limit, after, before); - - /// - /// Gets a list of users who are interested in this event. - /// - /// The id of the event to query users from - /// How many users to fetch. The method performs one api call per 100 users - /// Fetch users after this id. Mutually exclusive with before - /// Fetch users before this id. Mutually exclusive with after - public async IAsyncEnumerable GetEventUsersAsync(ulong guildEventId, int limit = 100, ulong? after = null, ulong? before = null) - { - if (after.HasValue && before.HasValue) - { - throw new ArgumentException("after and before are mutually exclusive"); - } - - int remaining = limit; - ulong? last = null; - bool isBefore = before != null; - int lastCount; - do - { - int fetchSize = remaining > 100 ? 100 : remaining; - IReadOnlyList fetch = await this.Discord.ApiClient.GetScheduledGuildEventUsersAsync(this.Id, guildEventId, true, fetchSize, isBefore ? last ?? before : null, !isBefore ? last ?? after : null); - - lastCount = fetch.Count; - remaining -= lastCount; - - if (isBefore) - { - for (int i = lastCount - 1; i >= 0; i--) - { - yield return fetch[i]; - } - last = fetch.FirstOrDefault()?.Id; - } - else - { - for (int i = 0; i < lastCount; i++) - { - yield return fetch[i]; - } - last = fetch.LastOrDefault()?.Id; - } - } - while (remaining > 0 && lastCount > 0); - } - - /// - /// Searches the current guild for members who's display name start with the specified name. - /// - /// The name to search for. - /// The maximum amount of members to return. Max 1000. Defaults to 1. - /// The members found, if any. - public async Task> SearchMembersAsync(string name, int? limit = 1) - => await this.Discord.ApiClient.SearchMembersAsync(this.Id, name, limit); - - /// - /// Adds a new member to this guild - /// - /// User to add - /// User's access token (OAuth2) - /// new nickname - /// whether this user has to be muted - /// whether this user has to be deafened - /// Only returns the member if they were not already in the guild - /// Thrown when the client does not have the permission. - /// Thrown when the or is not found. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public async Task AddMemberAsync - ( - DiscordUser user, - string accessToken, - string? nickname = null, - bool muted = false, - bool deaf = false - ) - => await this.Discord.ApiClient.AddGuildMemberAsync(this.Id, user.Id, accessToken, muted, deaf, nickname, null); - - /// - /// Adds a new member to this guild - /// - /// The id of the User to add - /// User's access token (OAuth2) - /// new nickname - /// whether this user has to be muted - /// whether this user has to be deafened - /// Only returns the member if they were not already in the guild - /// Thrown when the client does not have the permission. - /// Thrown when the or is not found. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public async Task AddMemberAsync - ( - ulong userId, - string accessToken, - string? nickname = null, - bool muted = false, - bool deaf = false - ) - => await this.Discord.ApiClient.AddGuildMemberAsync(this.Id, userId, accessToken, muted, deaf, nickname, null); - - /// - /// Adds a new member to this guild - /// - /// User to add - /// User's access token (OAuth2) - /// new nickname - /// Ids of roles to add to the new member. - /// whether this user has to be muted - /// whether this user has to be deafened - /// Only returns the member if they were not already in the guild - /// Thrown when the client does not have the permission. - /// Thrown when the or is not found. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public async Task AddMemberWithRolesAsync - ( - DiscordUser user, - string accessToken, - IEnumerable roles, - string? nickname = null, - bool muted = false, - bool deaf = false - ) - => await this.Discord.ApiClient.AddGuildMemberAsync(this.Id, user.Id, accessToken, muted, deaf, nickname, roles); - - /// - /// Adds a new member to this guild - /// - /// The id of the User to add - /// User's access token (OAuth2) - /// new nickname - /// Ids of roles to add to the new member. - /// whether this user has to be muted - /// whether this user has to be deafened - /// Only returns the member if they were not already in the guild - /// Thrown when the client does not have the permission. - /// Thrown when the or is not found. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public async Task AddMemberWithRolesAsync - ( - ulong userId, - string accessToken, - IEnumerable roles, - string? nickname = null, - bool muted = false, - bool deaf = false - ) - => await this.Discord.ApiClient.AddGuildMemberAsync(this.Id, userId, accessToken, muted, deaf, nickname, roles); - - /// - /// Adds a new member to this guild - /// - /// User to add - /// User's access token (OAuth2) - /// new nickname - /// Collection of roles to add to the new member. - /// whether this user has to be muted - /// whether this user has to be deafened - /// Only returns the member if they were not already in the guild - /// Thrown when the client does not have the permission. - /// Thrown when the or is not found. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public async Task AddMemberWithRolesAsync - ( - DiscordUser user, - string accessToken, - IEnumerable roles, - string? nickname = null, - bool muted = false, - bool deaf = false - ) - => await this.Discord.ApiClient.AddGuildMemberAsync(this.Id, user.Id, accessToken, muted, deaf, nickname, roles?.Select(x => x.Id)); - - /// - /// Adds a new member to this guild - /// - /// The id of the User to add - /// User's access token (OAuth2) - /// new nickname - /// Collection of roles to add to the new member. - /// whether this user has to be muted - /// whether this user has to be deafened - /// Only returns the member if they were not already in the guild - /// Thrown when the client does not have the permission. - /// Thrown when the or is not found. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public async Task AddMemberWithRolesAsync - ( - ulong userId, - string accessToken, - IEnumerable roles, - string? nickname = null, - bool muted = false, - bool deaf = false - ) - => await this.Discord.ApiClient.AddGuildMemberAsync(this.Id, userId, accessToken, muted, deaf, nickname, roles?.Select(x => x.Id)); - - /// - /// Deletes this guild. Requires the caller to be the owner of the guild. - /// - /// - /// Thrown when the client is not the owner of the guild. - /// Thrown when Discord is unable to process the request. - public async Task DeleteAsync() - => await this.Discord.ApiClient.DeleteGuildAsync(this.Id); - - /// - /// Modifies this guild. - /// - /// Action to perform on this guild.. - /// The modified guild object. - /// Thrown when the client does not have the permission. - /// Thrown when the guild does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public async Task ModifyAsync(Action action) - { - GuildEditModel mdl = new(); - action(mdl); - - if (mdl.AfkChannel.HasValue && mdl.AfkChannel.Value.Type != DiscordChannelType.Voice) - { - throw new ArgumentException("AFK channel needs to be a voice channel."); - } - - Optional iconb64 = Optional.FromNoValue(); - - if (mdl.Icon.HasValue && mdl.Icon.Value != null) - { - using InlineMediaTool imgtool = new(mdl.Icon.Value); - iconb64 = imgtool.GetBase64(); - } - else if (mdl.Icon.HasValue) - { - iconb64 = null; - } - - Optional splashb64 = Optional.FromNoValue(); - - if (mdl.Splash.HasValue && mdl.Splash.Value != null) - { - using InlineMediaTool imgtool = new(mdl.Splash.Value); - splashb64 = imgtool.GetBase64(); - } - else if (mdl.Splash.HasValue) - { - splashb64 = null; - } - - Optional bannerb64 = Optional.FromNoValue(); - - if (mdl.Banner.HasValue) - { - if (mdl.Banner.Value == null) - { - bannerb64 = null; - } - else - { - using InlineMediaTool imgtool = new(mdl.Banner.Value); - bannerb64 = imgtool.GetBase64(); - } - } - - return await this.Discord.ApiClient.ModifyGuildAsync(this.Id, mdl.Name, mdl.Region.IfPresent(e => e.Id), - mdl.VerificationLevel, mdl.DefaultMessageNotifications, mdl.MfaLevel, mdl.ExplicitContentFilter, - mdl.AfkChannel.IfPresent(e => e?.Id), mdl.AfkTimeout, iconb64, mdl.Owner.IfPresent(e => e.Id), splashb64, - mdl.SystemChannel.IfPresent(e => e?.Id), bannerb64, - mdl.Description, mdl.DiscoverySplash, mdl.Features, mdl.PreferredLocale, - mdl.PublicUpdatesChannel.IfPresent(e => e?.Id), mdl.RulesChannel.IfPresent(e => e?.Id), - mdl.SystemChannelFlags, mdl.AuditLogReason); - } - - /// - /// Gets the roles in this guild. - /// - /// All the roles in the guild. - public async Task> GetRolesAsync() - { - IReadOnlyList roles = await this.Discord.ApiClient.GetGuildRolesAsync(this.Id); - this.roles = new ConcurrentDictionary(roles.ToDictionary(x => x.Id)); - return roles; - } - - /// - /// Gets a singular role from this guild by its ID. - /// - /// The ID of the role. - /// Whether to skip checking cache for the role. - /// The role from the guild if it exists. - public async Task GetRoleAsync(ulong roleId, bool skipCache = false) - { - if (!skipCache && this.roles.TryGetValue(roleId, out DiscordRole? role)) - { - return role; - } - - role = await this.Discord.ApiClient.GetGuildRoleAsync(this.Id, roleId); - this.roles[role.Id] = role; - return role; - } - - /// - /// Batch modifies the role order in the guild. - /// - /// A dictionary of guild roles indexed by their new role positions. - /// An optional Audit log reason on why this action was done. - /// A list of all the current guild roles ordered in their new role positions. - public async Task> ModifyRolePositionsAsync(IDictionary roles, string? reason = null) - { - if (roles.Count == 0) - { - throw new ArgumentException("Roles cannot be empty.", nameof(roles)); - } - - // Sort the roles by position and create skeleton roles for the payload. - IReadOnlyList returnedRoles = await this.Discord.ApiClient.ModifyGuildRolePositionsAsync(this.Id, roles.Select(x => new RestGuildRoleReorderPayload() { RoleId = x.Value.Id, Position = x.Key }), reason); - - // Update the cache as the endpoint returns all roles in the order they were sent. - this.roles = new(returnedRoles.Select(x => new KeyValuePair(x.Id, x))); - return returnedRoles; - } - - /// - /// Removes a specified member from this guild. - /// - /// Member to remove. - /// Reason for audit logs. - public async Task RemoveMemberAsync(DiscordUser member, string? reason = null) - => await this.Discord.ApiClient.RemoveGuildMemberAsync(this.Id, member.Id, reason); - - /// - /// Removes a specified member by ID. - /// - /// ID of the user to remove. - /// Reason for audit logs. - public async Task RemoveMemberAsync(ulong userId, string? reason = null) - => await this.Discord.ApiClient.RemoveGuildMemberAsync(this.Id, userId, reason); - - /// - /// Bans a specified member from this guild. - /// - /// Member to ban. - /// The duration in which discord should delete messages from the banned user. - /// Reason for audit logs. - /// - /// Thrown when the client does not have the permission. - /// Thrown when the member does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public async Task BanMemberAsync(DiscordUser member, TimeSpan messageDeleteDuration = default, string? reason = null) - => await this.Discord.ApiClient.CreateGuildBanAsync(this.Id, member.Id, (int)messageDeleteDuration.TotalSeconds, reason); - - /// - /// Bans a specified user by ID. This doesn't require the user to be in this guild. - /// - /// ID of the user to ban. - /// The duration in which discord should delete messages from the banned user. - /// Reason for audit logs. - /// - /// Thrown when the client does not have the permission. - /// Thrown when the member does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public async Task BanMemberAsync(ulong userId, TimeSpan messageDeleteDuration = default, string? reason = null) - => await this.Discord.ApiClient.CreateGuildBanAsync(this.Id, userId, (int)messageDeleteDuration.TotalSeconds, reason); - - /// - /// Bans multiple users from this guild. - /// - /// Collection of users to ban - /// Timespan in seconds to delete messages from the banned users - /// Reason for audit logs. - /// Response contains a which users were banned and which were not. - public async Task BulkBanMembersAsync(IEnumerable users, int deleteMessageSeconds = 0, string? reason = null) - { - IEnumerable userIds = users.Select(x => x.Id); - return await this.Discord.ApiClient.CreateGuildBulkBanAsync(this.Id, userIds, deleteMessageSeconds, reason); - } - - /// - /// Bans multiple users from this guild by their id - /// - /// Collection of user ids to ban - /// Timespan in seconds to delete messages from the banned users - /// Reason for audit logs. - /// Response contains a which users were banned and which were not. - public async Task BulkBanMembersAsync(IEnumerable userIds, int deleteMessageSeconds = 0, string? reason = null) - => await this.Discord.ApiClient.CreateGuildBulkBanAsync(this.Id, userIds, deleteMessageSeconds, reason); - - /// - /// Unbans a user from this guild. - /// - /// User to unban. - /// Reason for audit logs. - /// - /// Thrown when the client does not have the permission. - /// Thrown when the user does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public async Task UnbanMemberAsync(DiscordUser user, string? reason = null) - => await this.Discord.ApiClient.RemoveGuildBanAsync(this.Id, user.Id, reason); - - /// - /// Unbans a user by ID. - /// - /// ID of the user to unban. - /// Reason for audit logs. - /// - /// Thrown when the client does not have the permission. - /// Thrown when the user does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public async Task UnbanMemberAsync(ulong userId, string? reason = null) - => await this.Discord.ApiClient.RemoveGuildBanAsync(this.Id, userId, reason); - - /// - /// Leaves this guild. - /// - /// - /// Thrown when Discord is unable to process the request. - public async Task LeaveAsync() - => await this.Discord.ApiClient.LeaveGuildAsync(this.Id); - - /// - /// Gets the bans for this guild. - /// - /// The number of users to return (up to maximum 1000, default 1000). - /// Consider only users before the given user id. - /// Consider only users after the given user id. - /// Collection of bans in this guild. - /// Thrown when the client does not have the permission. - /// Thrown when Discord is unable to process the request. - public async Task> GetBansAsync(int? limit = null, ulong? before = null, ulong? after = null) - => await this.Discord.ApiClient.GetGuildBansAsync(this.Id, limit, before, after); - - /// - /// Gets a ban for a specific user. - /// - /// The ID of the user to get the ban for. - /// Thrown when the specified user is not banned. - /// The requested ban object. - public async Task GetBanAsync(ulong userId) - => await this.Discord.ApiClient.GetGuildBanAsync(this.Id, userId); - - /// - /// Gets a ban for a specific user. - /// - /// The user to get the ban for. - /// Thrown when the specified user is not banned. - /// The requested ban object. - public async Task GetBanAsync(DiscordUser user) - => await this.Discord.ApiClient.GetGuildBanAsync(this.Id, user.Id); - - /// - /// Creates a new text channel in this guild. - /// - /// Name of the new channel. - /// Category to put this channel in. - /// Topic of the channel. - /// Permission overwrites for this channel. - /// Whether the channel is to be flagged as not safe for work. - /// Sorting position of the channel. - /// Reason for audit logs. - /// Slow mode timeout for users. - /// The newly-created channel. - /// Thrown when the client does not have the permission. - /// Thrown when the guild does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public Task CreateTextChannelAsync(string name, DiscordChannel? parent = null, Optional topic = default, IEnumerable? overwrites = null, bool? nsfw = null, Optional perUserRateLimit = default, int? position = null, string? reason = null) - => CreateChannelAsync(name, DiscordChannelType.Text, parent, topic, null, null, overwrites, nsfw, perUserRateLimit, null, position, reason); - - /// - /// Creates a new channel category in this guild. - /// - /// Name of the new category. - /// Permission overwrites for this category. - /// Sorting position of the channel. - /// Reason for audit logs. - /// The newly-created channel category. - /// Thrown when the client does not have the permission. - /// Thrown when the guild does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public Task CreateChannelCategoryAsync(string name, IEnumerable? overwrites = null, int? position = null, string? reason = null) - => CreateChannelAsync(name, DiscordChannelType.Category, null, Optional.FromNoValue(), null, null, overwrites, null, Optional.FromNoValue(), null, position, reason); - - /// - /// Creates a new voice channel in this guild. - /// - /// Name of the new channel. - /// Category to put this channel in. - /// Bitrate of the channel. - /// Maximum number of users in the channel. - /// Permission overwrites for this channel. - /// Video quality mode of the channel. - /// Sorting position of the channel. - /// Reason for audit logs. - /// The newly-created channel. - /// Thrown when the client does not have the permission. - /// Thrown when the guild does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public async Task CreateVoiceChannelAsync - ( - string name, - DiscordChannel? parent = null, - int? bitrate = null, - int? userLimit = null, - IEnumerable? overwrites = null, - DiscordVideoQualityMode? qualityMode = null, - int? position = null, - string? reason = null - ) => await CreateChannelAsync - ( - name, - DiscordChannelType.Voice, - parent, - Optional.FromNoValue(), - bitrate, - userLimit, - overwrites, - null, - Optional.FromNoValue(), - qualityMode, - position, - reason - ); - - /// - /// Creates a new channel in this guild. - /// - /// Name of the new channel. - /// Type of the new channel. - /// Category to put this channel in. - /// Topic of the channel. - /// Bitrate of the channel. Applies to voice only. - /// Maximum number of users in the channel. Applies to voice only. - /// Permission overwrites for this channel. - /// Whether the channel is to be flagged as not safe for work. Applies to text only. - /// Slow mode timeout for users. - /// Video quality mode of the channel. Applies to voice only. - /// Sorting position of the channel. - /// Reason for audit logs. - /// The default duration in which threads (or posts) will archive. - /// If applied to a forum, the default emoji to use for forum post reactions. - /// The tags available for a post in this channel. - /// The default sorting order. - /// The newly-created channel. - /// Thrown when the client does not have the permission. - /// Thrown when the guild does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public async Task CreateChannelAsync - ( - string name, - DiscordChannelType type, - DiscordChannel? parent = null, - Optional topic = default, - int? bitrate = null, - int? userLimit = null, - IEnumerable? overwrites = null, - bool? nsfw = null, - Optional perUserRateLimit = default, - DiscordVideoQualityMode? qualityMode = null, - int? position = null, - string? reason = null, - DiscordAutoArchiveDuration? defaultAutoArchiveDuration = null, - DefaultReaction? defaultReactionEmoji = null, - IEnumerable? availableTags = null, - DiscordDefaultSortOrder? defaultSortOrder = null - ) => - // technically you can create news/store channels but not always - type is not (DiscordChannelType.Text or DiscordChannelType.Voice or DiscordChannelType.Category or DiscordChannelType.News or DiscordChannelType.Stage or DiscordChannelType.GuildForum) - ? throw new ArgumentException("Channel type must be text, voice, stage, category, or a forum.", nameof(type)) - : type == DiscordChannelType.Category && parent is not null - ? throw new ArgumentException("Cannot specify parent of a channel category.", nameof(parent)) - : await this.Discord.ApiClient.CreateGuildChannelAsync - ( - this.Id, - name, - type, - parent?.Id, - topic, - bitrate, - userLimit, - overwrites, - nsfw, - perUserRateLimit, - qualityMode, - position, - reason, - defaultAutoArchiveDuration, - defaultReactionEmoji, - availableTags, - defaultSortOrder - ); - - // this is to commemorate the Great DAPI Channel Massacre of 2017-11-19. - /// - /// Deletes all channels in this guild. - /// Note that this is irreversible. Use carefully! - /// - /// - public Task DeleteAllChannelsAsync() - { - IEnumerable tasks = this.Channels.Values.Select(xc => xc.DeleteAsync()); - return Task.WhenAll(tasks); - } - - /// - /// Estimates the number of users to be pruned. - /// - /// Minimum number of inactivity days required for users to be pruned. Defaults to 7. - /// The roles to be included in the prune. - /// Number of users that will be pruned. - /// Thrown when the client does not have the permission. - /// Thrown when the guild does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public async Task GetPruneCountAsync(int days = 7, IEnumerable includedRoles = null) - { - if (includedRoles != null) - { - includedRoles = includedRoles.Where(r => r != null); - int roleCount = includedRoles.Count(); - DiscordRole[] roleArr = includedRoles.ToArray(); - List rawRoleIds = []; - - for (int i = 0; i < roleCount; i++) - { - if (this.roles.ContainsKey(roleArr[i].Id)) - { - rawRoleIds.Add(roleArr[i].Id); - } - } - - return await this.Discord.ApiClient.GetGuildPruneCountAsync(this.Id, days, rawRoleIds); - } - - return await this.Discord.ApiClient.GetGuildPruneCountAsync(this.Id, days, null); - } - - /// - /// Estimates the number of users to be pruned. - /// - /// Minimum number of inactivity days required for users to be pruned. Defaults to 7. - /// The ids of roles to be included in the prune. - /// Number of users that will be pruned. - /// Thrown when the client does not have the permission. - /// Thrown when the guild does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public async Task GetPruneCountAsync(int days = 7, IEnumerable? includedRoleIds = null) - => await this.Discord.ApiClient.GetGuildPruneCountAsync(this.Id, days, includedRoleIds?.Where(x => this.roles.ContainsKey(x))); - - /// - /// Prunes inactive users from this guild. - /// - /// Minimum number of inactivity days required for users to be pruned. Defaults to 7. - /// Whether to return the prune count after this method completes. This is discouraged for larger guilds. - /// The roles to be included in the prune. - /// Reason for audit logs. - /// Number of users pruned. - /// Thrown when the client does not have the permission. - /// Thrown when the guild does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public async Task PruneAsync(int days = 7, bool computePruneCount = true, IEnumerable? includedRoles = null, string? reason = null) - { - if (includedRoles != null) - { - includedRoles = includedRoles.Where(r => r != null); - int roleCount = includedRoles.Count(); - DiscordRole[] roleArr = includedRoles.ToArray(); - List rawRoleIds = []; - - for (int i = 0; i < roleCount; i++) - { - if (this.roles.ContainsKey(roleArr[i].Id)) - { - rawRoleIds.Add(roleArr[i].Id); - } - } - - return await this.Discord.ApiClient.BeginGuildPruneAsync(this.Id, days, computePruneCount, rawRoleIds, reason); - } - - return await this.Discord.ApiClient.BeginGuildPruneAsync(this.Id, days, computePruneCount, null, reason); - } - - /// - /// Prunes inactive users from this guild. - /// - /// Minimum number of inactivity days required for users to be pruned. Defaults to 7. - /// Whether to return the prune count after this method completes. This is discouraged for larger guilds. - /// The ids of roles to be included in the prune. - /// Reason for audit logs. - /// Number of users pruned. - /// Thrown when the client does not have the permission. - /// Thrown when the guild does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public async Task PruneAsync(int days = 7, bool computePruneCount = true, IEnumerable? includedRoleIds = null, string? reason = null) - => await this.Discord.ApiClient.BeginGuildPruneAsync(this.Id, days, computePruneCount, includedRoleIds?.Where(x => this.roles.ContainsKey(x)), reason); - - /// - /// Gets integrations attached to this guild. - /// - /// Collection of integrations attached to this guild. - /// Thrown when the client does not have the permission. - /// Thrown when the guild does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public async Task> GetIntegrationsAsync() - => await this.Discord.ApiClient.GetGuildIntegrationsAsync(this.Id); - - /// - /// Attaches an integration from current user to this guild. - /// - /// Integration to attach. - /// The integration after being attached to the guild. - /// Thrown when the client does not have the permission. - /// Thrown when the guild does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public async Task AttachUserIntegrationAsync(DiscordIntegration integration) - => await this.Discord.ApiClient.CreateGuildIntegrationAsync(this.Id, integration.Type, integration.Id); - - /// - /// Modifies an integration in this guild. - /// - /// Integration to modify. - /// Number of days after which the integration expires. - /// Length of grace period which allows for renewing the integration. - /// Whether emotes should be synced from this integration. - /// The modified integration. - /// Thrown when the client does not have the permission. - /// Thrown when the guild does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public async Task ModifyIntegrationAsync(DiscordIntegration integration, int expireBehaviour, int expireGracePeriod, bool enableEmoticons) - => await this.Discord.ApiClient.ModifyGuildIntegrationAsync(this.Id, integration.Id, expireBehaviour, expireGracePeriod, enableEmoticons); - - /// - /// Modifies an integration in this guild. - /// - /// The id of the Integration to modify. - /// Number of days after which the integration expires. - /// Length of grace period which allows for renewing the integration. - /// Whether emotes should be synced from this integration. - /// The modified integration. - /// Thrown when the client does not have the permission. - /// Thrown when the guild does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public async Task ModifyIntegrationAsync(ulong integrationId, int expireBehaviour, int expireGracePeriod, bool enableEmoticons) - => await this.Discord.ApiClient.ModifyGuildIntegrationAsync(this.Id, integrationId, expireBehaviour, expireGracePeriod, enableEmoticons); - - /// - /// Removes an integration from this guild. - /// - /// Integration to remove. - /// Reason for audit logs. - /// - /// Thrown when the client does not have the permission. - /// Thrown when the guild does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public async Task DeleteIntegrationAsync(DiscordIntegration integration, string? reason = null) - => await this.Discord.ApiClient.DeleteGuildIntegrationAsync(this.Id, integration.Id, reason); - - /// - /// Removes an integration from this guild. - /// - /// The id of the Integration to remove. - /// Reason for audit logs. - /// - /// Thrown when the client does not have the permission. - /// Thrown when the guild does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public async Task DeleteIntegrationAsync(ulong integrationId, string? reason = null) - => await this.Discord.ApiClient.DeleteGuildIntegrationAsync(this.Id, integrationId, reason); - - /// - /// Forces re-synchronization of an integration for this guild. - /// - /// Integration to synchronize. - /// - /// Thrown when the client does not have the permission. - /// Thrown when the guild does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public async Task SyncIntegrationAsync(DiscordIntegration integration) - => await this.Discord.ApiClient.SyncGuildIntegrationAsync(this.Id, integration.Id); - - /// - /// Forces re-synchronization of an integration for this guild. - /// - /// The id of the Integration to synchronize. - /// - /// Thrown when the client does not have the permission. - /// Thrown when the guild does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public async Task SyncIntegrationAsync(ulong integrationId) - => await this.Discord.ApiClient.SyncGuildIntegrationAsync(this.Id, integrationId); - - /// - /// Gets the voice regions for this guild. - /// - /// Voice regions available for this guild. - /// Thrown when Discord is unable to process the request. - public async Task> ListVoiceRegionsAsync() - { - IReadOnlyList vrs = await this.Discord.ApiClient.GetGuildVoiceRegionsAsync(this.Id); - foreach (DiscordVoiceRegion xvr in vrs) - { - this.Discord.InternalVoiceRegions.TryAdd(xvr.Id, xvr); - } - - return vrs; - } - - /// - /// Gets the active and private threads for this guild. - /// - /// A list of all the active and private threads the user can access in the server. - /// Thrown when Discord is unable to process the request. - public async Task ListActiveThreadsAsync() - { - ThreadQueryResult threads = await this.Discord.ApiClient.ListActiveThreadsAsync(this.Id); - // Gateway handles thread cache (if it does it properly - /*foreach (var thread in threads) - this.threads[thread.Id] = thread;*/ - return threads; - } - - /// - /// Gets an invite from this guild from an invite code. - /// - /// The invite code - /// An invite, or null if not in cache. - public DiscordInvite GetInvite(string code) - => this.invites.TryGetValue(code, out DiscordInvite? invite) ? invite : null; - - /// - /// Gets all the invites created for all the channels in this guild. - /// - /// A collection of invites. - /// Thrown when Discord is unable to process the request. - public async Task> GetInvitesAsync() - { - IReadOnlyList res = await this.Discord.ApiClient.GetGuildInvitesAsync(this.Id); - - DiscordIntents intents = this.Discord.Intents; - - if (!intents.HasIntent(DiscordIntents.GuildInvites)) - { - for (int i = 0; i < res.Count; i++) - { - this.invites[res[i].Code] = res[i]; - } - } - - return res; - } - - /// - /// Gets the vanity invite for this guild. - /// - /// A partial vanity invite. - /// Thrown when the client does not have the permission. - /// Thrown when Discord is unable to process the request. - public async Task GetVanityInviteAsync() - => await this.Discord.ApiClient.GetGuildVanityUrlAsync(this.Id); - - /// - /// Gets all the webhooks created for all the channels in this guild. - /// - /// A collection of webhooks this guild has. - /// Thrown when the client does not have the permission. - /// Thrown when Discord is unable to process the request. - public async Task> GetWebhooksAsync() - => await this.Discord.ApiClient.GetGuildWebhooksAsync(this.Id); - - /// - /// Gets this guild's widget image. - /// - /// The format of the widget. - /// The URL of the widget image. - public string GetWidgetImage(DiscordWidgetType bannerType = DiscordWidgetType.Shield) - { - string param = bannerType switch - { - DiscordWidgetType.Banner1 => "banner1", - DiscordWidgetType.Banner2 => "banner2", - DiscordWidgetType.Banner3 => "banner3", - DiscordWidgetType.Banner4 => "banner4", - _ => "shield", - }; - return $"{Endpoints.BASE_URI}/{Endpoints.GUILDS}/{this.Id}/{Endpoints.WIDGET_PNG}?style={param}"; - } - - /// - /// Gets a member of this guild by their user ID. - /// - /// ID of the member to get. - /// Whether to always make a REST request and update the member cache. - /// The requested member. - /// Thrown when Discord is unable to process the request. - /// Thrown when the member does not exist in this guild. - public async Task GetMemberAsync(ulong userId, bool updateCache = false) - { - if (!updateCache && this.members != null && this.members.TryGetValue(userId, out DiscordMember? mbr)) - { - return mbr; - } - - mbr = await this.Discord.ApiClient.GetGuildMemberAsync(this.Id, userId); - - DiscordIntents intents = this.Discord.Intents; - - if (intents.HasIntent(DiscordIntents.GuildMembers)) - { - if (this.members != null) - { - this.members[userId] = mbr; - } - } - - return mbr; - } - - /// - /// Retrieves a full list of members from Discord. This method will bypass cache. This will execute one API request per 1000 entities. - /// - /// Cancels the enumeration before the next api request - /// A collection of all members in this guild. - /// Thrown when Discord is unable to process the request. - public async IAsyncEnumerable GetAllMembersAsync - ( - [EnumeratorCancellation] - CancellationToken cancellationToken = default - ) - { - int recievedLastCall = 1000; - ulong last = 0ul; - while (recievedLastCall == 1000) - { - if (cancellationToken.IsCancellationRequested) - { - yield break; - } - - IReadOnlyList transportMembers = await this.Discord.ApiClient.ListGuildMembersAsync(this.Id, 1000, last == 0 ? null : last); - recievedLastCall = transportMembers.Count; - - foreach (TransportMember transportMember in transportMembers) - { - this.Discord.UpdateUserCache(new(transportMember.User) - { - Discord = this.Discord - }); - - yield return new(transportMember) - { - Discord = this.Discord, - guild_id = this.Id - }; - } - - TransportMember? lastMember = transportMembers.LastOrDefault(); - last = lastMember?.User.Id ?? 0; - } - } - - /// - /// Requests that Discord send a list of guild members based on the specified arguments. This method will fire the GuildMembersChunked event. - /// If no arguments aside from and are specified, this will request all guild members. - /// - /// Filters the returned members based on what the username starts with. Either this or must not be null. - /// The must also be greater than 0 if this is specified. - /// Total number of members to request. This must be greater than 0 if is specified. - /// Whether to include the associated with the fetched members. - /// Whether to limit the request to the specified user ids. Either this or must not be null. - /// The unique string to identify the response. This must be unique per-guild if multiple requests to the same guild are made. - /// A cancellation token to cancel the iterator with. - /// An asynchronous iterator that will return all members. - public async IAsyncEnumerable EnumerateRequestMembersAsync - ( - string query = "", - int limit = 0, - bool? presences = null, - IEnumerable? userIds = null, - string? nonce = null, - [EnumeratorCancellation] CancellationToken cancellationToken = default - ) - { - if (this.Discord is not DiscordClient client) - { - throw new InvalidOperationException("This operation is only valid for regular Discord clients."); - } - - ChannelReader reader = client.RegisterGuildMemberChunksEnumerator(this.Id, nonce); - - await RequestMembersAsync(query, limit, presences, userIds, nonce); - - await foreach (var evt in reader.ReadAllAsync(cancellationToken)) - { - foreach (DiscordMember member in evt.Members) - { - yield return member; - } - } - } - - /// - /// Requests that Discord send a list of guild members based on the specified arguments. This method will fire the GuildMembersChunked event. - /// If no arguments aside from and are specified, this will request all guild members. - /// - /// Filters the returned members based on what the username starts with. Either this or must not be null. - /// The must also be greater than 0 if this is specified. - /// Total number of members to request. This must be greater than 0 if is specified. - /// Whether to include the associated with the fetched members. - /// Whether to limit the request to the specified user ids. Either this or must not be null. - /// The unique string to identify the response. - public async Task RequestMembersAsync(string query = "", int limit = 0, bool? presences = null, IEnumerable? userIds = null, string? nonce = null) - { - if (this.Discord is not DiscordClient client) - { - throw new InvalidOperationException("This operation is only valid for regular Discord clients."); - } - - if (query == null && userIds == null) - { - throw new ArgumentException("The query and user IDs cannot both be null."); - } - - if (query != null && userIds != null) - { - query = null; - } - - GatewayRequestGuildMembers gatewayRequestGuildMembers = new(this) - { - Query = query, - Limit = limit >= 0 ? limit : 0, - Presences = presences, - UserIds = userIds, - Nonce = nonce - }; - -#pragma warning disable DSP0004 - await client.SendPayloadAsync(GatewayOpCode.RequestGuildMembers, gatewayRequestGuildMembers, this.Id); -#pragma warning restore DSP0004 - } - - /// - /// Gets all the channels this guild has. - /// - /// A collection of this guild's channels. - /// Thrown when Discord is unable to process the request. - public async Task> GetChannelsAsync() - => await this.Discord.ApiClient.GetGuildChannelsAsync(this.Id); - - /// - /// Creates a new role in this guild. - /// - /// Name of the role. - /// Permissions for the role. - /// Color for the role. - /// Whether the role is to be hoisted. - /// Whether the role is to be mentionable. - /// Reason for audit logs. - /// The icon to add to this role - /// The emoji to add to this role. Must be unicode. - /// The newly-created role. - /// Thrown when the client does not have the permission. - /// Thrown when Discord is unable to process the request. - public async Task CreateRoleAsync(string? name = null, DiscordPermissions? permissions = null, DiscordColor? color = null, bool? hoist = null, bool? mentionable = null, string? reason = null, Stream? icon = null, DiscordEmoji? emoji = null) - => await this.Discord.ApiClient.CreateGuildRoleAsync(this.Id, name, permissions, color?.Value, hoist, mentionable, icon, emoji?.ToString(), reason); - - /// - /// Gets a channel from this guild by its ID. - /// - /// ID of the channel to get. - /// Requested channel. - internal DiscordChannel? GetChannel(ulong id) - => this.channels != null && this.channels.TryGetValue(id, out DiscordChannel? channel) ? channel : null; - - /// - /// Gets a channel from this guild by its ID. - /// - /// ID of the channel to get. - /// If set to true this method will skip all caches and always perform a rest api call - /// Requested channel. - /// Thrown when Discord is unable to process the request. - /// Thrown when this channel does not exists - /// Thrown when the channel exists but does not belong to this guild instance. - public async Task GetChannelAsync(ulong id, bool skipCache = false) - { - DiscordChannel? channel; - if (skipCache) - { - channel = await this.Discord.ApiClient.GetChannelAsync(id); - - if (channel.GuildId is null || (channel.GuildId is not null && channel.GuildId.Value != this.Id)) - { - throw new InvalidOperationException("The channel exists but does not belong to this guild."); - } - - return channel; - } - - if (this.channels is not null && this.channels.TryGetValue(id, out channel)) - { - return channel; - } - - if (this.threads.TryGetValue(id, out DiscordThreadChannel? threadChannel)) - { - return threadChannel; - } - - channel = await this.Discord.ApiClient.GetChannelAsync(id); - - if (channel.GuildId is null || (channel.GuildId is not null && channel.GuildId.Value != this.Id)) - { - throw new InvalidOperationException("The channel exists but does not belong to this guild."); - } - - return channel; - } - - /// - /// Gets audit log entries for this guild. - /// - /// Maximum number of entries to fetch. Defaults to 100 - /// Filter by member responsible. - /// Filter by action type. - /// A collection of requested audit log entries. - /// Thrown when the client does not have the permission. - /// Thrown when Discord is unable to process the request. - /// If you set to null, it will fetch all entries. This may take a while as it will result in multiple api calls - public async IAsyncEnumerable GetAuditLogsAsync - ( - int? limit = 100, - DiscordMember? byMember = null, - DiscordAuditLogActionType? actionType = null - ) - { - //Get all entries from api - int entriesAcquiredLastCall = 1, totalEntriesCollected = 0, remainingEntries; - ulong last = 0; - while (entriesAcquiredLastCall > 0) - { - remainingEntries = limit != null ? limit.Value - totalEntriesCollected : 100; - remainingEntries = Math.Min(100, remainingEntries); - if (remainingEntries <= 0) - { - break; - } - - AuditLog guildAuditLog = await this.Discord.ApiClient.GetAuditLogsAsync(this.Id, remainingEntries, null, - last == 0 ? null : last, byMember?.Id, actionType); - entriesAcquiredLastCall = guildAuditLog.Entries.Count(); - totalEntriesCollected += entriesAcquiredLastCall; - if (entriesAcquiredLastCall > 0) - { - last = guildAuditLog.Entries.Last().Id; - IAsyncEnumerable parsedEntries = AuditLogParser.ParseAuditLogToEntriesAsync(this, guildAuditLog); - await foreach (DiscordAuditLogEntry discordAuditLogEntry in parsedEntries) - { - yield return discordAuditLogEntry; - } - } - - if (limit.HasValue) - { - int remaining = limit.Value - totalEntriesCollected; - if (remaining < 1) - { - break; - } - } - else if (entriesAcquiredLastCall < 100) - { - break; - } - } - } - - /// - /// Gets all of this guild's custom emojis. - /// - /// All of this guild's custom emojis. - /// Thrown when Discord is unable to process the request. - public async Task> GetEmojisAsync() - => await this.Discord.ApiClient.GetGuildEmojisAsync(this.Id); - - /// - /// Gets this guild's specified custom emoji. - /// - /// ID of the emoji to get. - /// The requested custom emoji. - /// Thrown when Discord is unable to process the request. - public async Task GetEmojiAsync(ulong id) - => await this.Discord.ApiClient.GetGuildEmojiAsync(this.Id, id); - - /// - /// Creates a new custom emoji for this guild. - /// - /// Name of the new emoji. - /// Image to use as the emoji. - /// Roles for which the emoji will be available. This works only if your application is whitelisted as integration. - /// Reason for audit log. - /// The newly-created emoji. - /// Thrown when the client does not have the permission. - /// Thrown when Discord is unable to process the request. - public async Task CreateEmojiAsync(string name, Stream image, IEnumerable? roles = null, string? reason = null) - { - if (string.IsNullOrWhiteSpace(name)) - { - throw new ArgumentNullException(nameof(name)); - } - - name = name.Trim(); - if (name.Length is < 2 or > 50) - { - throw new ArgumentException("Emoji name needs to be between 2 and 50 characters long."); - } - - ArgumentNullException.ThrowIfNull(image); - - string? image64 = null; - using (InlineMediaTool imgtool = new(image)) - { - image64 = imgtool.GetBase64(); - } - - return await this.Discord.ApiClient.CreateGuildEmojiAsync(this.Id, name, image64, roles?.Select(xr => xr.Id), reason); - } - - /// - /// Modifies a this guild's custom emoji. - /// - /// Emoji to modify. - /// New name for the emoji. - /// Roles for which the emoji will be available. This works only if your application is whitelisted as integration. - /// Reason for audit log. - /// The modified emoji. - /// Thrown when the client does not have the permission. - /// Thrown when Discord is unable to process the request. - public async Task ModifyEmojiAsync(DiscordGuildEmoji emoji, string name, IEnumerable? roles = null, string? reason = null) - { - ArgumentNullException.ThrowIfNull(emoji); - if (emoji.Guild.Id != this.Id) - { - throw new ArgumentException("This emoji does not belong to this guild."); - } - - if (string.IsNullOrWhiteSpace(name)) - { - throw new ArgumentNullException(nameof(name)); - } - - name = name.Trim(); - return name.Length is < 2 or > 50 - ? throw new ArgumentException("Emoji name needs to be between 2 and 50 characters long.") - : await this.Discord.ApiClient.ModifyGuildEmojiAsync(this.Id, emoji.Id, name, roles?.Select(xr => xr.Id), reason); - } - - /// - /// Deletes this guild's custom emoji. - /// - /// Emoji to delete. - /// Reason for audit log. - /// - /// Thrown when the client does not have the permission. - /// Thrown when Discord is unable to process the request. - /// Thrown when the emoji does not exist on this guild - public async Task DeleteEmojiAsync(DiscordGuildEmoji emoji, string? reason = null) - { - ArgumentNullException.ThrowIfNull(emoji); - - if (emoji.Guild.Id != this.Id) - { - throw new ArgumentException("This emoji does not belong to this guild."); - } - else - { - await this.Discord.ApiClient.DeleteGuildEmojiAsync(this.Id, emoji.Id, reason); - } - } - - /// - /// Deletes this guild's custom emoji. - /// - /// Emoji to delete. - /// Reason for audit log. - /// - /// Thrown when the client does not have the permission. - /// Thrown when Discord is unable to process the request. - /// Thrown when the emoji does not exist on this guild - public async Task DeleteEmojiAsync(ulong emojiId, string? reason = null) - => await this.Discord.ApiClient.DeleteGuildEmojiAsync(this.Id, emojiId, reason); - - /// - /// Gets the default channel for this guild. - /// Default channel is the first channel current member can see. - /// - /// This member's default guild. - /// Thrown when Discord is unable to process the request. - public DiscordChannel? GetDefaultChannel() - { - return this.channels?.Values.Where(xc => xc.Type == DiscordChannelType.Text) - .OrderBy(xc => xc.Position) - .FirstOrDefault(xc => xc.PermissionsFor(this.CurrentMember).HasPermission(DiscordPermission.ViewChannel)); - } - - /// - /// Gets the guild's widget - /// - /// The guild's widget - public async Task GetWidgetAsync() - => await this.Discord.ApiClient.GetGuildWidgetAsync(this.Id); - - /// - /// Gets the guild's widget settings - /// - /// The guild's widget settings - public async Task GetWidgetSettingsAsync() - => await this.Discord.ApiClient.GetGuildWidgetSettingsAsync(this.Id); - - /// - /// Modifies the guild's widget settings - /// - /// If the widget is enabled or not - /// Widget channel - /// Reason the widget settings were modified - /// The newly modified widget settings - public async Task ModifyWidgetSettingsAsync(bool? isEnabled = null, DiscordChannel? channel = null, string? reason = null) - => await this.Discord.ApiClient.ModifyGuildWidgetSettingsAsync(this.Id, isEnabled, channel?.Id, reason); - - /// - /// Gets all of this guild's templates. - /// - /// All of the guild's templates. - /// Throws when the client does not have the permission. - /// Thrown when Discord is unable to process the request. - public async Task> GetTemplatesAsync() - => await this.Discord.ApiClient.GetGuildTemplatesAsync(this.Id); - - /// - /// Creates a guild template. - /// - /// Name of the template. - /// Description of the template. - /// The template created. - /// Throws when a template already exists for the guild or a null parameter is provided for the name. - /// Throws when the client does not have the permission. - /// Thrown when Discord is unable to process the request. - public async Task CreateTemplateAsync(string name, string? description = null) - => await this.Discord.ApiClient.CreateGuildTemplateAsync(this.Id, name, description); - - /// - /// Syncs the template to the current guild's state. - /// - /// The code of the template to sync. - /// The template synced. - /// Throws when the template for the code cannot be found - /// Throws when the client does not have the permission. - /// Thrown when Discord is unable to process the request. - public async Task SyncTemplateAsync(string code) - => await this.Discord.ApiClient.SyncGuildTemplateAsync(this.Id, code); - - /// - /// Modifies the template's metadata. - /// - /// The template's code. - /// Name of the template. - /// Description of the template. - /// The template modified. - /// Throws when the template for the code cannot be found - /// Throws when the client does not have the permission. - /// Thrown when Discord is unable to process the request. - public async Task ModifyTemplateAsync(string code, string? name = null, string? description = null) - => await this.Discord.ApiClient.ModifyGuildTemplateAsync(this.Id, code, name, description); - - /// - /// Deletes the template. - /// - /// The code of the template to delete. - /// The deleted template. - /// Throws when the template for the code cannot be found - /// Throws when the client does not have the permission. - /// Thrown when Discord is unable to process the request. - public async Task DeleteTemplateAsync(string code) - => await this.Discord.ApiClient.DeleteGuildTemplateAsync(this.Id, code); - - /// - /// Gets this guild's membership screening form. - /// - /// This guild's membership screening form. - /// Thrown when Discord is unable to process the request. - public async Task GetMembershipScreeningFormAsync() - => await this.Discord.ApiClient.GetGuildMembershipScreeningFormAsync(this.Id); - - /// - /// Modifies this guild's membership screening form. - /// - /// Action to perform - /// The modified screening form. - /// Thrown when the client doesn't have the permission, or community is not enabled on this guild. - /// Thrown when Discord is unable to process the request. - public async Task ModifyMembershipScreeningFormAsync(Action action) - { - MembershipScreeningEditModel editModel = new(); - action(editModel); - return await this.Discord.ApiClient.ModifyGuildMembershipScreeningFormAsync(this.Id, editModel.Enabled, editModel.Fields, editModel.Description); - } - - /// - /// Gets a list of stickers from this guild. - /// - /// - public async Task> GetStickersAsync() - => await this.Discord.ApiClient.GetGuildStickersAsync(this.Id); - - /// - /// Gets a sticker from this guild. - /// - /// The id of the sticker. - /// - public async Task GetStickerAsync(ulong stickerId) - => await this.Discord.ApiClient.GetGuildStickerAsync(this.Id, stickerId); - - /// - /// Creates a sticker in this guild. Lottie stickers can only be created on verified and/or partnered servers. - /// - /// The name of the sticker. - /// The description of the sticker. - /// The tags of the sticker. This must be a unicode emoji. - /// The image content of the sticker. - /// The image format of the sticker. - /// The reason this sticker is being created. - - public async Task CreateStickerAsync(string name, string description, string tags, Stream imageContents, DiscordStickerFormat format, string? reason = null) - { - string contentType, extension; - if (format is DiscordStickerFormat.PNG or DiscordStickerFormat.APNG) - { - contentType = "image/png"; - extension = "png"; - } - else - { - if (!this.Features.Contains("PARTNERED") && !this.Features.Contains("VERIFIED")) - { - throw new InvalidOperationException("Lottie stickers can only be created on partnered or verified guilds."); - } - - contentType = "application/json"; - extension = "json"; - } - - return await this.Discord.ApiClient.CreateGuildStickerAsync(this.Id, name, description ?? string.Empty, tags, new DiscordMessageFile(null, imageContents, null, extension, contentType), reason); - } - - /// - /// Modifies a sticker in this guild. - /// - /// The id of the sticker. - /// Action to perform. - /// Reason for audit log. - public async Task ModifyStickerAsync(ulong stickerId, Action action, string? reason = null) - { - StickerEditModel editModel = new(); - action(editModel); - return await this.Discord.ApiClient.ModifyStickerAsync(this.Id, stickerId, editModel.Name, editModel.Description, editModel.Tags, reason ?? editModel.AuditLogReason); - } - - /// - /// Modifies a sticker in this guild. - /// - /// Sticker to modify. - /// Action to perform. - /// Reason for audit log. - public async Task ModifyStickerAsync(DiscordMessageSticker sticker, Action action, string? reason = null) - { - StickerEditModel editModel = new(); - action(editModel); - return await this.Discord.ApiClient.ModifyStickerAsync(this.Id, sticker.Id, editModel.Name, editModel.Description, editModel.Tags, reason ?? editModel.AuditLogReason); - } - - /// - /// Deletes a sticker in this guild. - /// - /// The id of the sticker. - /// Reason for audit log. - public async Task DeleteStickerAsync(ulong stickerId, string? reason = null) - => await this.Discord.ApiClient.DeleteStickerAsync(this.Id, stickerId, reason); - - /// - /// Deletes a sticker in this guild. - /// - /// Sticker to delete. - /// Reason for audit log. - public async Task DeleteStickerAsync(DiscordMessageSticker sticker, string? reason = null) - => await this.Discord.ApiClient.DeleteStickerAsync(this.Id, sticker.Id, reason); - - /// - /// Gets all the application commands in this guild. - /// - /// Whether to include localizations in the response. - /// A list of application commands in this guild. - public async Task> GetApplicationCommandsAsync(bool withLocalizations = false) => - await this.Discord.ApiClient.GetGuildApplicationCommandsAsync(this.Discord.CurrentApplication.Id, this.Id, withLocalizations); - - /// - /// Overwrites the existing application commands in this guild. New commands are automatically created and missing commands are automatically delete - /// - /// The list of commands to overwrite with. - /// The list of guild commands - public async Task> BulkOverwriteApplicationCommandsAsync(IEnumerable commands) => - await this.Discord.ApiClient.BulkOverwriteGuildApplicationCommandsAsync(this.Discord.CurrentApplication.Id, this.Id, commands); - - /// - /// Creates or overwrites a application command in this guild. - /// - /// The command to create. - /// The created command. - public async Task CreateApplicationCommandAsync(DiscordApplicationCommand command) => - await this.Discord.ApiClient.CreateGuildApplicationCommandAsync(this.Discord.CurrentApplication.Id, this.Id, command); - - /// - /// Edits a application command in this guild. - /// - /// The id of the command to edit. - /// Action to perform. - /// The edit command. - public async Task EditApplicationCommandAsync(ulong commandId, Action action) - { - ApplicationCommandEditModel editModel = new(); - action(editModel); - return await this.Discord.ApiClient.EditGuildApplicationCommandAsync(this.Discord.CurrentApplication.Id, this.Id, commandId, editModel.Name, editModel.Description, editModel.Options, editModel.DefaultPermission, editModel.NSFW, default, default, editModel.AllowDMUsage, editModel.DefaultMemberPermissions); - } - - /// - /// Gets a application command in this guild by its id. - /// - /// The ID of the command to get. - /// The command with the ID. - public async Task GetApplicationCommandAsync(ulong commandId) => - await this.Discord.ApiClient.GetGlobalApplicationCommandAsync(this.Discord.CurrentApplication.Id, commandId); - - /// - /// Gets a application command in this guild by its name. - /// - /// The name of the command to get. - /// Whether to include localizations in the response. - /// The command with the name. This is null when the command is not found - public async Task GetApplicationCommandAsync(string commandName, bool withLocalizations = false) - { - foreach (DiscordApplicationCommand command in await this.Discord.ApiClient.GetGlobalApplicationCommandsAsync(this.Discord.CurrentApplication.Id, withLocalizations)) - { - if (command.Name == commandName) - { - return command; - } - } - - return null; - } - - /// - /// Gets this guild's welcome screen. - /// - /// This guild's welcome screen object. - /// Thrown when Discord is unable to process the request. - public async Task GetWelcomeScreenAsync() => - await this.Discord.ApiClient.GetGuildWelcomeScreenAsync(this.Id); - - /// - /// Modifies this guild's welcome screen. - /// - /// Action to perform. - /// Reason for audit log. - /// The modified welcome screen. - /// Thrown when the client doesn't have the permission, or community is not enabled on this guild. - /// Thrown when Discord is unable to process the request. - public async Task ModifyWelcomeScreenAsync(Action action, string? reason = null) - { - WelcomeScreenEditModel editModel = new(); - action(editModel); - return await this.Discord.ApiClient.ModifyGuildWelcomeScreenAsync(this.Id, editModel.Enabled, editModel.WelcomeChannels, editModel.Description, reason); - } - - /// - /// Gets all application command permissions in this guild. - /// - /// A list of permissions. - public async Task> GetApplicationCommandsPermissionsAsync() - => await this.Discord.ApiClient.GetGuildApplicationCommandPermissionsAsync(this.Discord.CurrentApplication.Id, this.Id); - - /// - /// Gets permissions for a application command in this guild. - /// - /// The command to get them for. - /// The permissions. - public async Task GetApplicationCommandPermissionsAsync(DiscordApplicationCommand command) - => await this.Discord.ApiClient.GetApplicationCommandPermissionsAsync(this.Discord.CurrentApplication.Id, this.Id, command.Id); - - /// - /// Edits permissions for a application command in this guild. - /// - /// The command to edit permissions for. - /// The list of permissions to use. - /// The edited permissions. - public async Task EditApplicationCommandPermissionsAsync(DiscordApplicationCommand command, IEnumerable permissions) - => await this.Discord.ApiClient.EditApplicationCommandPermissionsAsync(this.Discord.CurrentApplication.Id, this.Id, command.Id, permissions); - - /// - /// Batch edits permissions for a application command in this guild. - /// - /// The list of permissions to use. - /// A list of edited permissions. - public async Task> BatchEditApplicationCommandPermissionsAsync(IEnumerable permissions) - => await this.Discord.ApiClient.BatchEditApplicationCommandPermissionsAsync(this.Discord.CurrentApplication.Id, this.Id, permissions); - - /// - /// Creates an auto-moderation rule in the guild. - /// - /// The rule name. - /// The event in which the rule should be triggered. - /// The type of content which can trigger the rule. - /// Metadata used to determine whether a rule should be triggered. This argument can be skipped depending eventType value. - /// Actions that will execute after the trigger of the rule. - /// Whether the rule is enabled or not. - /// Roles that will not trigger the rule. - /// Channels which will not trigger the rule. - /// Reason for audit logs. - /// The created rule. - public async Task CreateAutoModerationRuleAsync - ( - string name, - DiscordRuleEventType eventType, - DiscordRuleTriggerType triggerType, - DiscordRuleTriggerMetadata triggerMetadata, - IReadOnlyList actions, - Optional enabled = default, - Optional> exemptRoles = default, - Optional> exemptChannels = default, - string? reason = null - ) - { - return await this.Discord.ApiClient.CreateGuildAutoModerationRuleAsync - ( - this.Id, - name, - eventType, - triggerType, - triggerMetadata, - actions, - enabled, - exemptRoles, - exemptChannels, - reason - ); - } - - /// - /// Gets an auto-moderation rule by an id. - /// - /// The rule id. - /// The found rule. - public async Task GetAutoModerationRuleAsync(ulong ruleId) - => await this.Discord.ApiClient.GetGuildAutoModerationRuleAsync(this.Id, ruleId); - - /// - /// Gets all auto-moderation rules in the guild. - /// - /// All rules available in the guild. - public async Task> GetAutoModerationRulesAsync() - => await this.Discord.ApiClient.GetGuildAutoModerationRulesAsync(this.Id); - - /// - /// Modify an auto-moderation rule in the guild. - /// - /// The id of the rule that will be edited. - /// Action to perform on this rule. - /// The modified rule. - /// All arguments are optionals. - public async Task ModifyAutoModerationRuleAsync(ulong ruleId, Action action) - { - AutoModerationRuleEditModel model = new(); - - action(model); - - return await this.Discord.ApiClient.ModifyGuildAutoModerationRuleAsync - ( - this.Id, - ruleId, - model.Name, - model.EventType, - model.TriggerMetadata, - model.Actions, - model.Enable, - model.ExemptRoles, - model.ExemptChannels, - model.AuditLogReason - ); - } - - /// - /// Deletes a auto-moderation rule by an id. - /// - /// The rule id. - /// Reason for audit logs. - /// - public async Task DeleteAutoModerationRuleAsync(ulong ruleId, string? reason = null) - => await this.Discord.ApiClient.DeleteGuildAutoModerationRuleAsync(this.Id, ruleId, reason); - - /// - /// Gets the current user's voice state in this guild. - /// - /// Whether to skip the cache or not. - /// Returns the users voicestate. This is null if the user is in no voice channel - public async Task GetCurrentUserVoiceStateAsync(bool skipCache = false) - { - if (!skipCache && this.VoiceStates.TryGetValue(this.Discord.CurrentUser.Id, out DiscordVoiceState? voiceState)) - { - return voiceState; - } - - try - { - return await this.Discord.ApiClient.GetCurrentUserVoiceStateAsync(this.Id); - } - catch (NotFoundException) - { - return null; - } - } - - /// - /// Gets user's voice state in this guild. - /// - /// The member to get the voice state for. - /// Whether to skip the cache or not. - /// Returns the users voicestate. This is null if the user is in no voice channel - public Task GetMemberVoiceStateAsync(DiscordUser member, bool skipCache = false) - => GetMemberVoiceStateAsync(member.Id, skipCache); - - /// - /// Gets user's voice state in this guild. - /// - /// The member ID to get the voice state for. - /// Whether to skip the cache or not. - /// Returns the users voicestate. This is null if the user is in no voice channel - public async Task GetMemberVoiceStateAsync(ulong memberId, bool skipCache = false) - { - if (!skipCache && this.VoiceStates.TryGetValue(memberId, out DiscordVoiceState? voiceState)) - { - return voiceState; - } - - try - { - return await this.Discord.ApiClient.GetUserVoiceStateAsync(this.Id, memberId); - } - catch (NotFoundException) - { - return null; - } - } - - #endregion - - /// - /// Returns a string representation of this guild. - /// - /// String representation of this guild. - public override string ToString() => $"Guild {this.Id}; {this.Name}"; - - /// - /// Checks whether this is equal to another object. - /// - /// Object to compare to. - /// Whether the object is equal to this . - public override bool Equals(object obj) => Equals(obj as DiscordGuild); - - /// - /// Checks whether this is equal to another . - /// - /// to compare to. - /// Whether the is equal to this . - public bool Equals(DiscordGuild e) => e is not null && (ReferenceEquals(this, e) || this.Id == e.Id); - - /// - /// Gets the hash code for this . - /// - /// The hash code for this . - public override int GetHashCode() => this.Id.GetHashCode(); - - /// - /// Gets whether the two objects are equal. - /// - /// First member to compare. - /// Second member to compare. - /// Whether the two members are equal. - public static bool operator ==(DiscordGuild e1, DiscordGuild e2) - { - object? o1 = e1; - object? o2 = e2; - - 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 member to compare. - /// Second member to compare. - /// Whether the two members are not equal. - public static bool operator !=(DiscordGuild e1, DiscordGuild e2) - => !(e1 == e2); -} - -/// -/// Represents guild verification level. -/// -public enum DiscordVerificationLevel : int -{ - /// - /// No verification. Anyone can join and chat right away. - /// - None = 0, - - /// - /// Low verification level. Users are required to have a verified email attached to their account in order to be able to chat. - /// - Low = 1, - - /// - /// Medium verification level. Users are required to have a verified email attached to their account, and account age need to be at least 5 minutes in order to be able to chat. - /// - Medium = 2, - - /// - /// (╯°□°)╯︵ ┻━┻ verification level. Users are required to have a verified email attached to their account, account age need to be at least 5 minutes, and they need to be in the server for at least 10 minutes in order to be able to chat. - /// - High = 3, - - /// - /// ┻━┻ ミヽ(ಠ益ಠ)ノ彡┻━┻ verification level. Users are required to have a verified phone number attached to their account. - /// - Highest = 4 -} - -/// -/// Represents default notification level for a guild. -/// -public enum DiscordDefaultMessageNotifications : int -{ - /// - /// All messages will trigger push notifications. - /// - AllMessages = 0, - - /// - /// Only messages that mention the user (or a role he's in) will trigger push notifications. - /// - MentionsOnly = 1 -} - -/// -/// Represents multi-factor authentication level required by a guild to use administrator functionality. -/// -public enum DiscordMfaLevel : int -{ - /// - /// Multi-factor authentication is not required to use administrator functionality. - /// - Disabled = 0, - - /// - /// Multi-factor authentication is required to use administrator functionality. - /// - Enabled = 1 -} - -/// -/// Represents the value of explicit content filter in a guild. -/// -public enum DiscordExplicitContentFilter : int -{ - /// - /// Explicit content filter is disabled. - /// - Disabled = 0, - - /// - /// Only messages from members without any roles are scanned. - /// - MembersWithoutRoles = 1, - - /// - /// Messages from all members are scanned. - /// - AllMembers = 2 -} - -/// -/// Represents the formats for a guild widget. -/// -public enum DiscordWidgetType : int -{ - /// - /// The widget is represented in shield format. - /// This is the default widget type. - /// - Shield = 0, - - /// - /// The widget is represented as the first banner type. - /// - Banner1 = 1, - - /// - /// The widget is represented as the second banner type. - /// - Banner2 = 2, - - /// - /// The widget is represented as the third banner type. - /// - Banner3 = 3, - - /// - /// The widget is represented in the fourth banner type. - /// - Banner4 = 4 -} +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; +using DSharpPlus.Entities.AuditLogs; +using DSharpPlus.EventArgs; +using DSharpPlus.Exceptions; +using DSharpPlus.Net; +using DSharpPlus.Net.Abstractions; +using DSharpPlus.Net.Models; +using DSharpPlus.Net.Serialization; +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Represents a Discord guild. +/// +public class DiscordGuild : SnowflakeObject, IEquatable +{ + /// + /// Gets the guild's name. + /// + [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] + public string Name { get; internal set; } + + /// + /// Gets the guild icon's hash. + /// + [JsonProperty("icon", NullValueHandling = NullValueHandling.Ignore)] + public string IconHash { get; internal set; } + + /// + /// Gets the guild icon's url. + /// + [JsonIgnore] + public string IconUrl + => GetIconUrl(MediaFormat.Auto, 1024); + + /// + /// Gets the guild splash's hash. + /// + [JsonProperty("splash", NullValueHandling = NullValueHandling.Ignore)] + public string SplashHash { get; internal set; } + + /// + /// Gets the guild splash's url. + /// + [JsonIgnore] + public string? SplashUrl + => !string.IsNullOrWhiteSpace(this.SplashHash) ? $"https://cdn.discordapp.com/splashes/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.SplashHash}.jpg" : null; + + /// + /// Gets the guild discovery splash's hash. + /// + [JsonProperty("discovery_splash", NullValueHandling = NullValueHandling.Ignore)] + public string DiscoverySplashHash { get; internal set; } + + /// + /// Gets the guild discovery splash's url. + /// + [JsonIgnore] + public string? DiscoverySplashUrl + => !string.IsNullOrWhiteSpace(this.DiscoverySplashHash) ? $"https://cdn.discordapp.com/discovery-splashes/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.DiscoverySplashHash}.jpg" : null; + + /// + /// Gets the preferred locale of this guild. + /// This is used for server discovery and notices from Discord. Defaults to en-US. + /// + [JsonProperty("preferred_locale", NullValueHandling = NullValueHandling.Ignore)] + public string PreferredLocale { get; internal set; } + + /// + /// Gets the ID of the guild's owner. + /// + [JsonProperty("owner_id", NullValueHandling = NullValueHandling.Ignore)] + public ulong OwnerId { get; internal set; } + + /// + /// Gets permissions for the user in the guild (does not include channel overrides) + /// + [JsonProperty("permissions", NullValueHandling = NullValueHandling.Ignore)] + public DiscordPermissions? Permissions { get; set; } + + /// + /// Gets the guild's owner. + /// + public async Task GetGuildOwnerAsync() + { + return this.Members.TryGetValue(this.OwnerId, out DiscordMember? owner) + ? owner + : await this.Discord.ApiClient.GetGuildMemberAsync(this.Id, this.OwnerId); + } + + /// + /// Gets the guild's voice region ID. + /// + [JsonProperty("region", NullValueHandling = NullValueHandling.Ignore)] + public string VoiceRegionId { get; set; } + + /// + /// Gets the guild's voice region. + /// + [JsonIgnore] + public DiscordVoiceRegion VoiceRegion + => this.Discord.VoiceRegions[this.VoiceRegionId]; + + /// + /// Gets the guild's AFK voice channel ID. + /// + [JsonProperty("afk_channel_id", NullValueHandling = NullValueHandling.Ignore)] + public ulong? AfkChannelId { get; internal set; } + + /// + /// Gets the guild's AFK voice channel. + /// + /// If set to true this method will skip all caches and always perform a rest api call + /// Returns null if the guild has no AFK channel + public async Task GetAfkChannelAsync(bool skipCache = false) + { + if (this.AfkChannelId is null) + { + return null; + } + + return await GetChannelAsync(this.AfkChannelId.Value); + } + + /// + /// Gets the guild's AFK timeout. + /// + [JsonProperty("afk_timeout", NullValueHandling = NullValueHandling.Ignore)] + public int AfkTimeout { get; internal set; } + + /// + /// Gets the guild's verification level. + /// + [JsonProperty("verification_level", NullValueHandling = NullValueHandling.Ignore)] + public DiscordVerificationLevel VerificationLevel { get; internal set; } + + /// + /// Gets the guild's default notification settings. + /// + [JsonProperty("default_message_notifications", NullValueHandling = NullValueHandling.Ignore)] + public DiscordDefaultMessageNotifications DefaultMessageNotifications { get; internal set; } + + /// + /// Gets the guild's explicit content filter settings. + /// + [JsonProperty("explicit_content_filter")] + public DiscordExplicitContentFilter ExplicitContentFilter { get; internal set; } + + /// + /// Gets the guild's nsfw level. + /// + [JsonProperty("nsfw_level")] + public DiscordNsfwLevel NsfwLevel { get; internal set; } + + /// + /// Id of the channel where system messages (such as boost and welcome messages) are sent. + /// + [JsonProperty("system_channel_id", NullValueHandling = NullValueHandling.Include)] + public ulong? SystemChannelId { get; internal set; } + + /// + /// Gets the channel where system messages (such as boost and welcome messages) are sent. + /// + /// If set to true this method will skip all caches and always perform a rest api call + /// Returns null if the guild has no configured system channel. + public async Task GetSystemChannelAsync(bool skipCache = false) + { + if (this.SystemChannelId is null) + { + return null; + } + + return await GetChannelAsync(this.SystemChannelId.Value); + } + + /// + /// Gets the settings for this guild's system channel. + /// + [JsonProperty("system_channel_flags")] + public DiscordSystemChannelFlags SystemChannelFlags { get; internal set; } + + /// + /// Id of the channel where safety alerts are sent to + /// + [JsonProperty("safety_alerts_channel_id")] + public ulong? SafetyAlertsChannelId { get; internal set; } + + /// + /// Gets the guild's safety alerts channel. + /// + /// If set to true this method will skip all caches and always perform a rest api call + ///Returns null if the guild has no configured safety alerts channel. + public async Task GetSafetyAlertsChannelAsync(bool skipCache = false) + { + if (this.SafetyAlertsChannelId is null) + { + return null; + } + + return await GetChannelAsync(this.SafetyAlertsChannelId.Value); + } + + /// + /// Gets whether this guild's widget is enabled. + /// + [JsonProperty("widget_enabled", NullValueHandling = NullValueHandling.Ignore)] + public bool? WidgetEnabled { get; internal set; } + + /// + /// Id of the widget channel + /// + [JsonProperty("widget_channel_id", NullValueHandling = NullValueHandling.Ignore)] + public ulong? WidgetChannelId { get; internal set; } + + /// + /// Gets the widget channel for this guild. + /// + /// If set to true this method will skip all caches and always perform a rest api call + /// Returns null if the guild has no widget channel configured. + public async Task GetWidgetChannelAsync(bool skipCache = false) + { + if (this.WidgetChannelId is null) + { + return null; + } + + return await GetChannelAsync(this.WidgetChannelId.Value); + } + + /// + /// Id of the rules channel of this guild. Null if the guild has no configured rules channel. + /// + [JsonProperty("rules_channel_id")] + public ulong? RulesChannelId { get; internal set; } + + /// + /// Gets the rules channel for this guild. + /// This is only available if the guild is considered "discoverable". + /// + /// If set to true this method will skip all caches and always perform a rest api call + /// Returns null if the guild has no rules channel configured + public async Task GetRulesChannelAsync(bool skipCache = false) + { + if (this.RulesChannelId is null) + { + return null; + } + + return await GetChannelAsync(this.RulesChannelId.Value); + } + + /// + /// Id of the channel where admins and moderators receive messages from Discord + /// + [JsonProperty("public_updates_channel_id")] + public ulong? PublicUpdatesChannelId { get; internal set; } + + /// + /// Gets the public updates channel (where admins and moderators receive messages from Discord) for this guild. + /// This is only available if the guild is considered "discoverable". + /// + /// If set to true this method will skip all caches and always perform a rest api call + /// Returns null if the guild has no public updates channel configured + public async Task GetPublicUpdatesChannelAsync(bool skipCache = false) + { + if (this.PublicUpdatesChannelId is null) + { + return null; + } + + return await GetChannelAsync(this.PublicUpdatesChannelId.Value); + } + + /// + /// Gets the application ID of this guild if it is bot created. + /// + [JsonProperty("application_id")] + public ulong? ApplicationId { get; internal set; } + + /// + /// Scheduled events for this guild. + /// + public IReadOnlyDictionary ScheduledEvents + => this.scheduledEvents; + + [JsonProperty("guild_scheduled_events")] + [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] + internal ConcurrentDictionary scheduledEvents = new(); + + /// + /// Gets a collection of this guild's roles. + /// + [JsonIgnore] + public IReadOnlyDictionary Roles => this.roles; + + [JsonProperty("roles", NullValueHandling = NullValueHandling.Ignore)] + [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] + internal ConcurrentDictionary roles; + + /// + /// Gets a collection of this guild's stickers. + /// + [JsonIgnore] + public IReadOnlyDictionary Stickers => this.stickers; + + [JsonProperty("stickers", NullValueHandling = NullValueHandling.Ignore)] + [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] + internal ConcurrentDictionary stickers = new(); + + /// + /// Gets a collection of this guild's emojis. + /// + [JsonIgnore] + public IReadOnlyDictionary Emojis => this.emojis; + + [JsonProperty("emojis", NullValueHandling = NullValueHandling.Ignore)] + [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] + internal ConcurrentDictionary emojis; + + /// + /// Gets a collection of this guild's features. + /// + [JsonProperty("features", NullValueHandling = NullValueHandling.Ignore)] + public IReadOnlyList Features { get; internal set; } + + /// + /// Gets the required multi-factor authentication level for this guild. + /// + [JsonProperty("mfa_level", NullValueHandling = NullValueHandling.Ignore)] + public DiscordMfaLevel MfaLevel { get; internal set; } + + /// + /// Gets this guild's join date. + /// + [JsonProperty("joined_at", NullValueHandling = NullValueHandling.Ignore)] + public DateTimeOffset JoinedAt { get; internal set; } + + /// + /// Gets whether this guild is considered to be a large guild. + /// + [JsonProperty("large", NullValueHandling = NullValueHandling.Ignore)] + public bool IsLarge { get; internal set; } + + /// + /// Gets whether this guild is unavailable. + /// + [JsonProperty("unavailable", NullValueHandling = NullValueHandling.Ignore)] + public bool IsUnavailable { get; internal set; } + + /// + /// Gets the total number of members in this guild. + /// + [JsonProperty("member_count", NullValueHandling = NullValueHandling.Ignore)] + public int MemberCount { get; internal set; } + + /// + /// Gets the maximum amount of members allowed for this guild. + /// + [JsonProperty("max_members")] + public int? MaxMembers { get; internal set; } + + /// + /// Gets the maximum amount of presences allowed for this guild. + /// + [JsonProperty("max_presences")] + public int? MaxPresences { get; internal set; } + +#pragma warning disable CS1734 + /// + /// Gets the approximate number of members in this guild, when using and having set to true. + /// + [JsonProperty("approximate_member_count", NullValueHandling = NullValueHandling.Ignore)] + public int? ApproximateMemberCount { get; internal set; } + + /// + /// Gets the approximate number of presences in this guild, when using and having set to true. + /// + [JsonProperty("approximate_presence_count", NullValueHandling = NullValueHandling.Ignore)] + public int? ApproximatePresenceCount { get; internal set; } +#pragma warning restore CS1734 + + /// + /// Gets the maximum amount of users allowed per video channel. + /// + [JsonProperty("max_video_channel_users", NullValueHandling = NullValueHandling.Ignore)] + public int? MaxVideoChannelUsers { get; internal set; } + + /// + /// Gets a dictionary of all the voice states for this guild. The key for this dictionary is the ID of the user + /// the voice state corresponds to. + /// + [JsonIgnore] + public IReadOnlyDictionary VoiceStates => this.voiceStates; + + [JsonProperty("voice_states", NullValueHandling = NullValueHandling.Ignore)] + [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] + internal ConcurrentDictionary voiceStates = new(); + + /// + /// Gets a dictionary of all the members that belong to this guild. The dictionary's key is the member ID. + /// + [JsonIgnore] + public IReadOnlyDictionary Members => this.members; + + [JsonProperty("members", NullValueHandling = NullValueHandling.Ignore)] + [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] + internal ConcurrentDictionary members; + + /// + /// Gets a dictionary of all the channels associated with this guild. The dictionary's key is the channel ID. + /// + [JsonIgnore] + public IReadOnlyDictionary Channels => this.channels; + + [JsonProperty("channels", NullValueHandling = NullValueHandling.Ignore)] + [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] + internal ConcurrentDictionary channels; + + /// + /// Gets a dictionary of all the active threads associated with this guild the user has permission to view. The dictionary's key is the channel ID. + /// + [JsonIgnore] + public IReadOnlyDictionary Threads => this.threads; + + [JsonProperty("threads", NullValueHandling = NullValueHandling.Ignore)] + [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] + internal ConcurrentDictionary threads = new(); + + internal ConcurrentDictionary invites; + + /// + /// Gets the guild member for current user. + /// + [JsonIgnore] + public DiscordMember CurrentMember => this.members != null && this.members.TryGetValue(this.Discord.CurrentUser.Id, out DiscordMember? member) ? member : null; + + /// + /// Gets the @everyone role for this guild. + /// + [JsonIgnore] + public DiscordRole EveryoneRole + => this.Roles.GetValueOrDefault(this.Id)!; + + [JsonIgnore] + internal bool isOwner; + + /// + /// Gets whether the current user is the guild's owner. + /// + [JsonProperty("owner", NullValueHandling = NullValueHandling.Ignore)] + public bool IsOwner + { + get => this.isOwner || this.OwnerId == this.Discord.CurrentUser.Id; + internal set => this.isOwner = value; + } + + /// + /// Gets the vanity URL code for this guild, when applicable. + /// + [JsonProperty("vanity_url_code")] + public string VanityUrlCode { get; internal set; } + + /// + /// Gets the guild description, when applicable. + /// + [JsonProperty("description")] + public string Description { get; internal set; } + + /// + /// Gets this guild's banner hash, when applicable. + /// + [JsonProperty("banner")] + public string Banner { get; internal set; } + + /// + /// Gets this guild's banner in url form. + /// + [JsonIgnore] + public string? BannerUrl + => !string.IsNullOrWhiteSpace(this.Banner) ? $"https://cdn.discordapp.com/banners/{this.Id}/{this.Banner}" : null; + + /// + /// Gets this guild's premium tier (Nitro boosting). + /// + [JsonProperty("premium_tier")] + public DiscordPremiumTier PremiumTier { get; internal set; } + + /// + /// Gets the amount of members that boosted this guild. + /// + [JsonProperty("premium_subscription_count", NullValueHandling = NullValueHandling.Ignore)] + public int? PremiumSubscriptionCount { get; internal set; } + + /// + /// Whether the guild has the boost progress bar enabled. + /// + [JsonProperty("premium_progress_bar_enabled", NullValueHandling = NullValueHandling.Ignore)] + public bool PremiumProgressBarEnabled { get; internal set; } + + /// + /// Gets whether this guild is designated as NSFW. + /// + [JsonProperty("nsfw", NullValueHandling = NullValueHandling.Ignore)] + public bool IsNSFW { get; internal set; } + + /// + /// Gets the stage instances in this guild. + /// + [JsonIgnore] + public IReadOnlyDictionary StageInstances => this.stageInstances; + + [JsonProperty("stage_instances", NullValueHandling = NullValueHandling.Ignore)] + [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] + internal ConcurrentDictionary stageInstances; + + // Failed attempts so far: 8 + // Velvet got it working in one attempt. I'm not mad, why would I be mad. - Lunar + /// + /// Gets channels ordered in a manner in which they'd be ordered in the UI of the discord client. + /// + [JsonIgnore] + // Group the channels by category or parent id + public IEnumerable OrderedChannels => this.channels.Values.GroupBy(channel => channel.IsCategory ? channel.Id : channel.ParentId) + // Order the channel by the category's position + .OrderBy(channels => channels.FirstOrDefault(channel => channel.IsCategory)?.Position) + // Select the category's channels + // Order them by text, shoving voice or stage types to the bottom + // Then order them by their position + .Select(channel => channel.OrderBy(channel => channel.Type is DiscordChannelType.Voice or DiscordChannelType.Stage).ThenBy(channel => channel.Position)) + // Group them all back together into a single enumerable. + .SelectMany(channel => channel); + + [JsonIgnore] + internal bool isSynced { get; set; } + + internal DiscordGuild() => this.invites = new ConcurrentDictionary(); + + #region Guild Methods + + /// + /// Gets guild's icon URL, in requested format and size. + /// + /// The image format of the icon to get. + /// The maximum size of the icon. Must be a power of two, minimum 16, maximum 4096. + /// The URL of the guild's icon. + public string? GetIconUrl(MediaFormat imageFormat, ushort imageSize = 1024) + { + + if (string.IsNullOrWhiteSpace(this.IconHash)) + { + return null; + } + + if (imageFormat == MediaFormat.Unknown) + { + throw new ArgumentException("You must specify valid image format.", nameof(imageFormat)); + } + + // Makes sure the image size is in between Discord's allowed range. + if (imageSize is < 16 or > 4096) + { + throw new ArgumentOutOfRangeException(nameof(imageSize), imageSize, "Image Size is not in between 16 and 4096."); + } + + // Checks to see if the image size is not a power of two. + if (!(imageSize is not 0 && (imageSize & (imageSize - 1)) is 0)) + { + throw new ArgumentOutOfRangeException(nameof(imageSize), imageSize, "Image size is not a power of two."); + } + + // Get the string variants of the method parameters to use in the urls. + string stringImageFormat = imageFormat switch + { + MediaFormat.Gif => "gif", + MediaFormat.Jpeg => "jpg", + MediaFormat.Png => "png", + MediaFormat.WebP => "webp", + MediaFormat.Auto => !string.IsNullOrWhiteSpace(this.IconHash) ? this.IconHash.StartsWith("a_") ? "gif" : "png" : "png", + _ => throw new ArgumentOutOfRangeException(nameof(imageFormat)), + }; + string stringImageSize = imageSize.ToString(CultureInfo.InvariantCulture); + + return $"https://cdn.discordapp.com/{Endpoints.ICONS}/{this.Id}/{this.IconHash}.{stringImageFormat}?size={stringImageSize}"; + + } + + /// + /// Creates a new scheduled event in this guild. + /// + /// The name of the event to create, up to 100 characters. + /// The description of the event, up to 1000 characters. + /// If a or , the id of the channel the event will be hosted in + /// The type of the event. must be supplied if not an external event. + /// The privacy level of thi + /// When this event starts. Must be in the future, and before the end date. + /// When this event ends. If supplied, must be in the future and after the end date. This is required for . + /// Where this event takes place, up to 100 characters. Only applicable if the type is + /// A cover image for this event. + /// Reason for audit log. + /// The created event. + public async Task CreateEventAsync(string name, string description, ulong? channelId, DiscordScheduledGuildEventType type, DiscordScheduledGuildEventPrivacyLevel privacyLevel, DateTimeOffset start, DateTimeOffset? end, string? location = null, Stream? image = null, string? reason = null) + { + ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(start, DateTimeOffset.Now); + if (end != null && end <= start) + { + throw new ArgumentOutOfRangeException(nameof(end), "The end time for an event must be after the start time."); + } + + DiscordScheduledGuildEventMetadata? metadata = null; + switch (type) + { + case DiscordScheduledGuildEventType.StageInstance or DiscordScheduledGuildEventType.VoiceChannel when channelId == null: + throw new ArgumentException($"{nameof(channelId)} must not be null when type is {type}", nameof(channelId)); + case DiscordScheduledGuildEventType.External when channelId != null: + throw new ArgumentException($"{nameof(channelId)} must be null when using external event type", nameof(channelId)); + case DiscordScheduledGuildEventType.External when location == null: + throw new ArgumentException($"{nameof(location)} must not be null when using external event type", nameof(location)); + case DiscordScheduledGuildEventType.External when end == null: + throw new ArgumentException($"{nameof(end)} must not be null when using external event type", nameof(end)); + } + + if (!string.IsNullOrEmpty(location)) + { + metadata = new DiscordScheduledGuildEventMetadata() + { + Location = location + }; + } + + return await this.Discord.ApiClient.CreateScheduledGuildEventAsync(this.Id, name, description, start, type, privacyLevel, metadata, end, channelId, image, reason); + } + + /// + /// Starts a scheduled event in this guild. + /// + /// The event to cancel. + /// + /// + public Task StartEventAsync(DiscordScheduledGuildEvent guildEvent) => guildEvent.Status is not DiscordScheduledGuildEventStatus.Scheduled + ? throw new InvalidOperationException("The event must be scheduled for it to be started.") + : ModifyEventAsync(guildEvent, m => m.Status = DiscordScheduledGuildEventStatus.Active); + + /// + /// Cancels an event. The event must be scheduled for it to be cancelled. + /// + /// The event to delete. + public Task CancelEventAsync(DiscordScheduledGuildEvent guildEvent) => guildEvent.Status is not DiscordScheduledGuildEventStatus.Scheduled + ? throw new InvalidOperationException("The event must be scheduled for it to be cancelled.") + : ModifyEventAsync(guildEvent, m => m.Status = DiscordScheduledGuildEventStatus.Cancelled); + + /// + /// Modifies an existing scheduled event in this guild. + /// + /// The event to modify. + /// The action to perform on this event + /// The reason this event is being modified + /// The modified object + /// + public async Task ModifyEventAsync(DiscordScheduledGuildEvent guildEvent, Action mdl, string? reason = null) + { + ScheduledGuildEventEditModel model = new(); + mdl(model); + + if (model.Type.HasValue && model.Type.Value is not DiscordScheduledGuildEventType.External) + { + if (!model.Channel.HasValue) + { + throw new ArgumentException("Channel must be supplied if the event is a stage instance or voice channel event."); + } + + if (model.Type.Value is DiscordScheduledGuildEventType.StageInstance && model.Channel.Value.Type is not DiscordChannelType.Stage) + { + throw new ArgumentException("Channel must be a stage channel if the event is a stage instance event."); + } + + if (model.Type.Value is DiscordScheduledGuildEventType.VoiceChannel && model.Channel.Value.Type is not DiscordChannelType.Voice) + { + throw new ArgumentException("Channel must be a voice channel if the event is a voice channel event."); + } + + if (model.EndTime.HasValue && model.EndTime.Value < guildEvent.StartTime) + { + throw new ArgumentException("End time must be after the start time."); + } + } + + if (model.Type.HasValue && model.Type.Value is DiscordScheduledGuildEventType.External) + { + if (!model.EndTime.HasValue) + { + throw new ArgumentException("End must be supplied if the event is an external event."); + } + + if (!model.Metadata.HasValue || string.IsNullOrEmpty(model.Metadata.Value.Location)) + { + throw new ArgumentException("Location must be supplied if the event is an external event."); + } + + if (model.Channel.HasValue && model.Channel.Value != null) + { + throw new ArgumentException("Channel must not be supplied if the event is an external event."); + } + } + + if (guildEvent.Status is DiscordScheduledGuildEventStatus.Completed) + { + throw new ArgumentException("The event must not be completed for it to be modified."); + } + + if (guildEvent.Status is DiscordScheduledGuildEventStatus.Cancelled) + { + throw new ArgumentException("The event must not be cancelled for it to be modified."); + } + + if (model.Status.HasValue) + { + switch (model.Status.Value) + { + case DiscordScheduledGuildEventStatus.Scheduled: + throw new ArgumentException("Status must not be set to scheduled."); + case DiscordScheduledGuildEventStatus.Active when guildEvent.Status is not DiscordScheduledGuildEventStatus.Scheduled: + throw new ArgumentException("Event status must be scheduled to progress to active."); + case DiscordScheduledGuildEventStatus.Completed when guildEvent.Status is not DiscordScheduledGuildEventStatus.Active: + throw new ArgumentException("Event status must be active to progress to completed."); + case DiscordScheduledGuildEventStatus.Cancelled when guildEvent.Status is not DiscordScheduledGuildEventStatus.Scheduled: + throw new ArgumentException("Event status must be scheduled to progress to cancelled."); + } + } + + DiscordScheduledGuildEvent modifiedEvent = await this.Discord.ApiClient.ModifyScheduledGuildEventAsync + ( + this.Id, + guildEvent.Id, + model.Name, + model.Description, + model.Channel.IfPresent(c => c?.Id), + model.StartTime, + model.EndTime, + model.Type, + model.PrivacyLevel, + model.Metadata, + model.Status, + model.CoverImage, + reason + ); + + this.scheduledEvents[modifiedEvent.Id] = modifiedEvent; + } + + /// + /// Deletes an exising scheduled event in this guild. + /// + /// + /// The reason which should be used for the audit log + /// + public async Task DeleteEventAsync(DiscordScheduledGuildEvent guildEvent, string? reason = null) + { + this.scheduledEvents.TryRemove(guildEvent.Id, out _); + await this.Discord.ApiClient.DeleteScheduledGuildEventAsync(this.Id, guildEvent.Id, reason); + } + + /// + /// Deletes an exising scheduled event in this guild. + /// + /// The Id of the event which should be deleted. + /// The reason which should be used for the audit log + /// + public async Task DeleteEventAsync(ulong guildEventId, string? reason = null) + { + this.scheduledEvents.TryRemove(guildEventId, out _); + await this.Discord.ApiClient.DeleteScheduledGuildEventAsync(this.Id, guildEventId, reason); + } + + /// + /// Gets the currently active or scheduled events in this guild. + /// + /// Whether to include number of users subscribed to each event + /// The active and scheduled events on the server, if any. + public async Task> GetEventsAsync(bool withUserCounts = false) + { + IReadOnlyList events = await this.Discord.ApiClient.GetScheduledGuildEventsAsync(this.Id, withUserCounts); + + foreach (DiscordScheduledGuildEvent @event in events) + { + this.scheduledEvents[@event.Id] = @event; + } + + return events; + } + + /// + /// Gets a list of users who are interested in this event. + /// + /// The event to query users from + /// How many users to fetch. + /// Fetch users after this id. Mutually exclusive with before + /// Fetch users before this id. Mutually exclusive with after + public IAsyncEnumerable GetEventUsersAsync + ( + DiscordScheduledGuildEvent guildEvent, + int limit = 100, + ulong? after = null, + ulong? before = null + ) + => GetEventUsersAsync(guildEvent.Id, limit, after, before); + + /// + /// Gets a list of users who are interested in this event. + /// + /// The id of the event to query users from + /// How many users to fetch. The method performs one api call per 100 users + /// Fetch users after this id. Mutually exclusive with before + /// Fetch users before this id. Mutually exclusive with after + public async IAsyncEnumerable GetEventUsersAsync(ulong guildEventId, int limit = 100, ulong? after = null, ulong? before = null) + { + if (after.HasValue && before.HasValue) + { + throw new ArgumentException("after and before are mutually exclusive"); + } + + int remaining = limit; + ulong? last = null; + bool isBefore = before != null; + int lastCount; + do + { + int fetchSize = remaining > 100 ? 100 : remaining; + IReadOnlyList fetch = await this.Discord.ApiClient.GetScheduledGuildEventUsersAsync(this.Id, guildEventId, true, fetchSize, isBefore ? last ?? before : null, !isBefore ? last ?? after : null); + + lastCount = fetch.Count; + remaining -= lastCount; + + if (isBefore) + { + for (int i = lastCount - 1; i >= 0; i--) + { + yield return fetch[i]; + } + last = fetch.FirstOrDefault()?.Id; + } + else + { + for (int i = 0; i < lastCount; i++) + { + yield return fetch[i]; + } + last = fetch.LastOrDefault()?.Id; + } + } + while (remaining > 0 && lastCount > 0); + } + + /// + /// Searches the current guild for members who's display name start with the specified name. + /// + /// The name to search for. + /// The maximum amount of members to return. Max 1000. Defaults to 1. + /// The members found, if any. + public async Task> SearchMembersAsync(string name, int? limit = 1) + => await this.Discord.ApiClient.SearchMembersAsync(this.Id, name, limit); + + /// + /// Adds a new member to this guild + /// + /// User to add + /// User's access token (OAuth2) + /// new nickname + /// whether this user has to be muted + /// whether this user has to be deafened + /// Only returns the member if they were not already in the guild + /// Thrown when the client does not have the permission. + /// Thrown when the or is not found. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task AddMemberAsync + ( + DiscordUser user, + string accessToken, + string? nickname = null, + bool muted = false, + bool deaf = false + ) + => await this.Discord.ApiClient.AddGuildMemberAsync(this.Id, user.Id, accessToken, muted, deaf, nickname, null); + + /// + /// Adds a new member to this guild + /// + /// The id of the User to add + /// User's access token (OAuth2) + /// new nickname + /// whether this user has to be muted + /// whether this user has to be deafened + /// Only returns the member if they were not already in the guild + /// Thrown when the client does not have the permission. + /// Thrown when the or is not found. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task AddMemberAsync + ( + ulong userId, + string accessToken, + string? nickname = null, + bool muted = false, + bool deaf = false + ) + => await this.Discord.ApiClient.AddGuildMemberAsync(this.Id, userId, accessToken, muted, deaf, nickname, null); + + /// + /// Adds a new member to this guild + /// + /// User to add + /// User's access token (OAuth2) + /// new nickname + /// Ids of roles to add to the new member. + /// whether this user has to be muted + /// whether this user has to be deafened + /// Only returns the member if they were not already in the guild + /// Thrown when the client does not have the permission. + /// Thrown when the or is not found. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task AddMemberWithRolesAsync + ( + DiscordUser user, + string accessToken, + IEnumerable roles, + string? nickname = null, + bool muted = false, + bool deaf = false + ) + => await this.Discord.ApiClient.AddGuildMemberAsync(this.Id, user.Id, accessToken, muted, deaf, nickname, roles); + + /// + /// Adds a new member to this guild + /// + /// The id of the User to add + /// User's access token (OAuth2) + /// new nickname + /// Ids of roles to add to the new member. + /// whether this user has to be muted + /// whether this user has to be deafened + /// Only returns the member if they were not already in the guild + /// Thrown when the client does not have the permission. + /// Thrown when the or is not found. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task AddMemberWithRolesAsync + ( + ulong userId, + string accessToken, + IEnumerable roles, + string? nickname = null, + bool muted = false, + bool deaf = false + ) + => await this.Discord.ApiClient.AddGuildMemberAsync(this.Id, userId, accessToken, muted, deaf, nickname, roles); + + /// + /// Adds a new member to this guild + /// + /// User to add + /// User's access token (OAuth2) + /// new nickname + /// Collection of roles to add to the new member. + /// whether this user has to be muted + /// whether this user has to be deafened + /// Only returns the member if they were not already in the guild + /// Thrown when the client does not have the permission. + /// Thrown when the or is not found. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task AddMemberWithRolesAsync + ( + DiscordUser user, + string accessToken, + IEnumerable roles, + string? nickname = null, + bool muted = false, + bool deaf = false + ) + => await this.Discord.ApiClient.AddGuildMemberAsync(this.Id, user.Id, accessToken, muted, deaf, nickname, roles?.Select(x => x.Id)); + + /// + /// Adds a new member to this guild + /// + /// The id of the User to add + /// User's access token (OAuth2) + /// new nickname + /// Collection of roles to add to the new member. + /// whether this user has to be muted + /// whether this user has to be deafened + /// Only returns the member if they were not already in the guild + /// Thrown when the client does not have the permission. + /// Thrown when the or is not found. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task AddMemberWithRolesAsync + ( + ulong userId, + string accessToken, + IEnumerable roles, + string? nickname = null, + bool muted = false, + bool deaf = false + ) + => await this.Discord.ApiClient.AddGuildMemberAsync(this.Id, userId, accessToken, muted, deaf, nickname, roles?.Select(x => x.Id)); + + /// + /// Deletes this guild. Requires the caller to be the owner of the guild. + /// + /// + /// Thrown when the client is not the owner of the guild. + /// Thrown when Discord is unable to process the request. + public async Task DeleteAsync() + => await this.Discord.ApiClient.DeleteGuildAsync(this.Id); + + /// + /// Modifies this guild. + /// + /// Action to perform on this guild.. + /// The modified guild object. + /// Thrown when the client does not have the permission. + /// Thrown when the guild does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task ModifyAsync(Action action) + { + GuildEditModel mdl = new(); + action(mdl); + + if (mdl.AfkChannel.HasValue && mdl.AfkChannel.Value.Type != DiscordChannelType.Voice) + { + throw new ArgumentException("AFK channel needs to be a voice channel."); + } + + Optional iconb64 = Optional.FromNoValue(); + + if (mdl.Icon.HasValue && mdl.Icon.Value != null) + { + using InlineMediaTool imgtool = new(mdl.Icon.Value); + iconb64 = imgtool.GetBase64(); + } + else if (mdl.Icon.HasValue) + { + iconb64 = null; + } + + Optional splashb64 = Optional.FromNoValue(); + + if (mdl.Splash.HasValue && mdl.Splash.Value != null) + { + using InlineMediaTool imgtool = new(mdl.Splash.Value); + splashb64 = imgtool.GetBase64(); + } + else if (mdl.Splash.HasValue) + { + splashb64 = null; + } + + Optional bannerb64 = Optional.FromNoValue(); + + if (mdl.Banner.HasValue) + { + if (mdl.Banner.Value == null) + { + bannerb64 = null; + } + else + { + using InlineMediaTool imgtool = new(mdl.Banner.Value); + bannerb64 = imgtool.GetBase64(); + } + } + + return await this.Discord.ApiClient.ModifyGuildAsync(this.Id, mdl.Name, mdl.Region.IfPresent(e => e.Id), + mdl.VerificationLevel, mdl.DefaultMessageNotifications, mdl.MfaLevel, mdl.ExplicitContentFilter, + mdl.AfkChannel.IfPresent(e => e?.Id), mdl.AfkTimeout, iconb64, mdl.Owner.IfPresent(e => e.Id), splashb64, + mdl.SystemChannel.IfPresent(e => e?.Id), bannerb64, + mdl.Description, mdl.DiscoverySplash, mdl.Features, mdl.PreferredLocale, + mdl.PublicUpdatesChannel.IfPresent(e => e?.Id), mdl.RulesChannel.IfPresent(e => e?.Id), + mdl.SystemChannelFlags, mdl.AuditLogReason); + } + + /// + /// Gets the roles in this guild. + /// + /// All the roles in the guild. + public async Task> GetRolesAsync() + { + IReadOnlyList roles = await this.Discord.ApiClient.GetGuildRolesAsync(this.Id); + this.roles = new ConcurrentDictionary(roles.ToDictionary(x => x.Id)); + return roles; + } + + /// + /// Gets a singular role from this guild by its ID. + /// + /// The ID of the role. + /// Whether to skip checking cache for the role. + /// The role from the guild if it exists. + public async Task GetRoleAsync(ulong roleId, bool skipCache = false) + { + if (!skipCache && this.roles.TryGetValue(roleId, out DiscordRole? role)) + { + return role; + } + + role = await this.Discord.ApiClient.GetGuildRoleAsync(this.Id, roleId); + this.roles[role.Id] = role; + return role; + } + + /// + /// Batch modifies the role order in the guild. + /// + /// A dictionary of guild roles indexed by their new role positions. + /// An optional Audit log reason on why this action was done. + /// A list of all the current guild roles ordered in their new role positions. + public async Task> ModifyRolePositionsAsync(IDictionary roles, string? reason = null) + { + if (roles.Count == 0) + { + throw new ArgumentException("Roles cannot be empty.", nameof(roles)); + } + + // Sort the roles by position and create skeleton roles for the payload. + IReadOnlyList returnedRoles = await this.Discord.ApiClient.ModifyGuildRolePositionsAsync(this.Id, roles.Select(x => new RestGuildRoleReorderPayload() { RoleId = x.Value.Id, Position = x.Key }), reason); + + // Update the cache as the endpoint returns all roles in the order they were sent. + this.roles = new(returnedRoles.Select(x => new KeyValuePair(x.Id, x))); + return returnedRoles; + } + + /// + /// Removes a specified member from this guild. + /// + /// Member to remove. + /// Reason for audit logs. + public async Task RemoveMemberAsync(DiscordUser member, string? reason = null) + => await this.Discord.ApiClient.RemoveGuildMemberAsync(this.Id, member.Id, reason); + + /// + /// Removes a specified member by ID. + /// + /// ID of the user to remove. + /// Reason for audit logs. + public async Task RemoveMemberAsync(ulong userId, string? reason = null) + => await this.Discord.ApiClient.RemoveGuildMemberAsync(this.Id, userId, reason); + + /// + /// Bans a specified member from this guild. + /// + /// Member to ban. + /// The duration in which discord should delete messages from the banned user. + /// Reason for audit logs. + /// + /// Thrown when the client does not have the permission. + /// Thrown when the member does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task BanMemberAsync(DiscordUser member, TimeSpan messageDeleteDuration = default, string? reason = null) + => await this.Discord.ApiClient.CreateGuildBanAsync(this.Id, member.Id, (int)messageDeleteDuration.TotalSeconds, reason); + + /// + /// Bans a specified user by ID. This doesn't require the user to be in this guild. + /// + /// ID of the user to ban. + /// The duration in which discord should delete messages from the banned user. + /// Reason for audit logs. + /// + /// Thrown when the client does not have the permission. + /// Thrown when the member does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task BanMemberAsync(ulong userId, TimeSpan messageDeleteDuration = default, string? reason = null) + => await this.Discord.ApiClient.CreateGuildBanAsync(this.Id, userId, (int)messageDeleteDuration.TotalSeconds, reason); + + /// + /// Bans multiple users from this guild. + /// + /// Collection of users to ban + /// Timespan in seconds to delete messages from the banned users + /// Reason for audit logs. + /// Response contains a which users were banned and which were not. + public async Task BulkBanMembersAsync(IEnumerable users, int deleteMessageSeconds = 0, string? reason = null) + { + IEnumerable userIds = users.Select(x => x.Id); + return await this.Discord.ApiClient.CreateGuildBulkBanAsync(this.Id, userIds, deleteMessageSeconds, reason); + } + + /// + /// Bans multiple users from this guild by their id + /// + /// Collection of user ids to ban + /// Timespan in seconds to delete messages from the banned users + /// Reason for audit logs. + /// Response contains a which users were banned and which were not. + public async Task BulkBanMembersAsync(IEnumerable userIds, int deleteMessageSeconds = 0, string? reason = null) + => await this.Discord.ApiClient.CreateGuildBulkBanAsync(this.Id, userIds, deleteMessageSeconds, reason); + + /// + /// Unbans a user from this guild. + /// + /// User to unban. + /// Reason for audit logs. + /// + /// Thrown when the client does not have the permission. + /// Thrown when the user does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task UnbanMemberAsync(DiscordUser user, string? reason = null) + => await this.Discord.ApiClient.RemoveGuildBanAsync(this.Id, user.Id, reason); + + /// + /// Unbans a user by ID. + /// + /// ID of the user to unban. + /// Reason for audit logs. + /// + /// Thrown when the client does not have the permission. + /// Thrown when the user does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task UnbanMemberAsync(ulong userId, string? reason = null) + => await this.Discord.ApiClient.RemoveGuildBanAsync(this.Id, userId, reason); + + /// + /// Leaves this guild. + /// + /// + /// Thrown when Discord is unable to process the request. + public async Task LeaveAsync() + => await this.Discord.ApiClient.LeaveGuildAsync(this.Id); + + /// + /// Gets the bans for this guild. + /// + /// The number of users to return (up to maximum 1000, default 1000). + /// Consider only users before the given user id. + /// Consider only users after the given user id. + /// Collection of bans in this guild. + /// Thrown when the client does not have the permission. + /// Thrown when Discord is unable to process the request. + public async Task> GetBansAsync(int? limit = null, ulong? before = null, ulong? after = null) + => await this.Discord.ApiClient.GetGuildBansAsync(this.Id, limit, before, after); + + /// + /// Gets a ban for a specific user. + /// + /// The ID of the user to get the ban for. + /// Thrown when the specified user is not banned. + /// The requested ban object. + public async Task GetBanAsync(ulong userId) + => await this.Discord.ApiClient.GetGuildBanAsync(this.Id, userId); + + /// + /// Gets a ban for a specific user. + /// + /// The user to get the ban for. + /// Thrown when the specified user is not banned. + /// The requested ban object. + public async Task GetBanAsync(DiscordUser user) + => await this.Discord.ApiClient.GetGuildBanAsync(this.Id, user.Id); + + /// + /// Creates a new text channel in this guild. + /// + /// Name of the new channel. + /// Category to put this channel in. + /// Topic of the channel. + /// Permission overwrites for this channel. + /// Whether the channel is to be flagged as not safe for work. + /// Sorting position of the channel. + /// Reason for audit logs. + /// Slow mode timeout for users. + /// The newly-created channel. + /// Thrown when the client does not have the permission. + /// Thrown when the guild does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public Task CreateTextChannelAsync(string name, DiscordChannel? parent = null, Optional topic = default, IEnumerable? overwrites = null, bool? nsfw = null, Optional perUserRateLimit = default, int? position = null, string? reason = null) + => CreateChannelAsync(name, DiscordChannelType.Text, parent, topic, null, null, overwrites, nsfw, perUserRateLimit, null, position, reason); + + /// + /// Creates a new channel category in this guild. + /// + /// Name of the new category. + /// Permission overwrites for this category. + /// Sorting position of the channel. + /// Reason for audit logs. + /// The newly-created channel category. + /// Thrown when the client does not have the permission. + /// Thrown when the guild does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public Task CreateChannelCategoryAsync(string name, IEnumerable? overwrites = null, int? position = null, string? reason = null) + => CreateChannelAsync(name, DiscordChannelType.Category, null, Optional.FromNoValue(), null, null, overwrites, null, Optional.FromNoValue(), null, position, reason); + + /// + /// Creates a new voice channel in this guild. + /// + /// Name of the new channel. + /// Category to put this channel in. + /// Bitrate of the channel. + /// Maximum number of users in the channel. + /// Permission overwrites for this channel. + /// Video quality mode of the channel. + /// Sorting position of the channel. + /// Reason for audit logs. + /// The newly-created channel. + /// Thrown when the client does not have the permission. + /// Thrown when the guild does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task CreateVoiceChannelAsync + ( + string name, + DiscordChannel? parent = null, + int? bitrate = null, + int? userLimit = null, + IEnumerable? overwrites = null, + DiscordVideoQualityMode? qualityMode = null, + int? position = null, + string? reason = null + ) => await CreateChannelAsync + ( + name, + DiscordChannelType.Voice, + parent, + Optional.FromNoValue(), + bitrate, + userLimit, + overwrites, + null, + Optional.FromNoValue(), + qualityMode, + position, + reason + ); + + /// + /// Creates a new channel in this guild. + /// + /// Name of the new channel. + /// Type of the new channel. + /// Category to put this channel in. + /// Topic of the channel. + /// Bitrate of the channel. Applies to voice only. + /// Maximum number of users in the channel. Applies to voice only. + /// Permission overwrites for this channel. + /// Whether the channel is to be flagged as not safe for work. Applies to text only. + /// Slow mode timeout for users. + /// Video quality mode of the channel. Applies to voice only. + /// Sorting position of the channel. + /// Reason for audit logs. + /// The default duration in which threads (or posts) will archive. + /// If applied to a forum, the default emoji to use for forum post reactions. + /// The tags available for a post in this channel. + /// The default sorting order. + /// The newly-created channel. + /// Thrown when the client does not have the permission. + /// Thrown when the guild does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task CreateChannelAsync + ( + string name, + DiscordChannelType type, + DiscordChannel? parent = null, + Optional topic = default, + int? bitrate = null, + int? userLimit = null, + IEnumerable? overwrites = null, + bool? nsfw = null, + Optional perUserRateLimit = default, + DiscordVideoQualityMode? qualityMode = null, + int? position = null, + string? reason = null, + DiscordAutoArchiveDuration? defaultAutoArchiveDuration = null, + DefaultReaction? defaultReactionEmoji = null, + IEnumerable? availableTags = null, + DiscordDefaultSortOrder? defaultSortOrder = null + ) => + // technically you can create news/store channels but not always + type is not (DiscordChannelType.Text or DiscordChannelType.Voice or DiscordChannelType.Category or DiscordChannelType.News or DiscordChannelType.Stage or DiscordChannelType.GuildForum) + ? throw new ArgumentException("Channel type must be text, voice, stage, category, or a forum.", nameof(type)) + : type == DiscordChannelType.Category && parent is not null + ? throw new ArgumentException("Cannot specify parent of a channel category.", nameof(parent)) + : await this.Discord.ApiClient.CreateGuildChannelAsync + ( + this.Id, + name, + type, + parent?.Id, + topic, + bitrate, + userLimit, + overwrites, + nsfw, + perUserRateLimit, + qualityMode, + position, + reason, + defaultAutoArchiveDuration, + defaultReactionEmoji, + availableTags, + defaultSortOrder + ); + + // this is to commemorate the Great DAPI Channel Massacre of 2017-11-19. + /// + /// Deletes all channels in this guild. + /// Note that this is irreversible. Use carefully! + /// + /// + public Task DeleteAllChannelsAsync() + { + IEnumerable tasks = this.Channels.Values.Select(xc => xc.DeleteAsync()); + return Task.WhenAll(tasks); + } + + /// + /// Estimates the number of users to be pruned. + /// + /// Minimum number of inactivity days required for users to be pruned. Defaults to 7. + /// The roles to be included in the prune. + /// Number of users that will be pruned. + /// Thrown when the client does not have the permission. + /// Thrown when the guild does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task GetPruneCountAsync(int days = 7, IEnumerable includedRoles = null) + { + if (includedRoles != null) + { + includedRoles = includedRoles.Where(r => r != null); + int roleCount = includedRoles.Count(); + DiscordRole[] roleArr = includedRoles.ToArray(); + List rawRoleIds = []; + + for (int i = 0; i < roleCount; i++) + { + if (this.roles.ContainsKey(roleArr[i].Id)) + { + rawRoleIds.Add(roleArr[i].Id); + } + } + + return await this.Discord.ApiClient.GetGuildPruneCountAsync(this.Id, days, rawRoleIds); + } + + return await this.Discord.ApiClient.GetGuildPruneCountAsync(this.Id, days, null); + } + + /// + /// Estimates the number of users to be pruned. + /// + /// Minimum number of inactivity days required for users to be pruned. Defaults to 7. + /// The ids of roles to be included in the prune. + /// Number of users that will be pruned. + /// Thrown when the client does not have the permission. + /// Thrown when the guild does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task GetPruneCountAsync(int days = 7, IEnumerable? includedRoleIds = null) + => await this.Discord.ApiClient.GetGuildPruneCountAsync(this.Id, days, includedRoleIds?.Where(x => this.roles.ContainsKey(x))); + + /// + /// Prunes inactive users from this guild. + /// + /// Minimum number of inactivity days required for users to be pruned. Defaults to 7. + /// Whether to return the prune count after this method completes. This is discouraged for larger guilds. + /// The roles to be included in the prune. + /// Reason for audit logs. + /// Number of users pruned. + /// Thrown when the client does not have the permission. + /// Thrown when the guild does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task PruneAsync(int days = 7, bool computePruneCount = true, IEnumerable? includedRoles = null, string? reason = null) + { + if (includedRoles != null) + { + includedRoles = includedRoles.Where(r => r != null); + int roleCount = includedRoles.Count(); + DiscordRole[] roleArr = includedRoles.ToArray(); + List rawRoleIds = []; + + for (int i = 0; i < roleCount; i++) + { + if (this.roles.ContainsKey(roleArr[i].Id)) + { + rawRoleIds.Add(roleArr[i].Id); + } + } + + return await this.Discord.ApiClient.BeginGuildPruneAsync(this.Id, days, computePruneCount, rawRoleIds, reason); + } + + return await this.Discord.ApiClient.BeginGuildPruneAsync(this.Id, days, computePruneCount, null, reason); + } + + /// + /// Prunes inactive users from this guild. + /// + /// Minimum number of inactivity days required for users to be pruned. Defaults to 7. + /// Whether to return the prune count after this method completes. This is discouraged for larger guilds. + /// The ids of roles to be included in the prune. + /// Reason for audit logs. + /// Number of users pruned. + /// Thrown when the client does not have the permission. + /// Thrown when the guild does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task PruneAsync(int days = 7, bool computePruneCount = true, IEnumerable? includedRoleIds = null, string? reason = null) + => await this.Discord.ApiClient.BeginGuildPruneAsync(this.Id, days, computePruneCount, includedRoleIds?.Where(x => this.roles.ContainsKey(x)), reason); + + /// + /// Gets integrations attached to this guild. + /// + /// Collection of integrations attached to this guild. + /// Thrown when the client does not have the permission. + /// Thrown when the guild does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task> GetIntegrationsAsync() + => await this.Discord.ApiClient.GetGuildIntegrationsAsync(this.Id); + + /// + /// Attaches an integration from current user to this guild. + /// + /// Integration to attach. + /// The integration after being attached to the guild. + /// Thrown when the client does not have the permission. + /// Thrown when the guild does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task AttachUserIntegrationAsync(DiscordIntegration integration) + => await this.Discord.ApiClient.CreateGuildIntegrationAsync(this.Id, integration.Type, integration.Id); + + /// + /// Modifies an integration in this guild. + /// + /// Integration to modify. + /// Number of days after which the integration expires. + /// Length of grace period which allows for renewing the integration. + /// Whether emotes should be synced from this integration. + /// The modified integration. + /// Thrown when the client does not have the permission. + /// Thrown when the guild does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task ModifyIntegrationAsync(DiscordIntegration integration, int expireBehaviour, int expireGracePeriod, bool enableEmoticons) + => await this.Discord.ApiClient.ModifyGuildIntegrationAsync(this.Id, integration.Id, expireBehaviour, expireGracePeriod, enableEmoticons); + + /// + /// Modifies an integration in this guild. + /// + /// The id of the Integration to modify. + /// Number of days after which the integration expires. + /// Length of grace period which allows for renewing the integration. + /// Whether emotes should be synced from this integration. + /// The modified integration. + /// Thrown when the client does not have the permission. + /// Thrown when the guild does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task ModifyIntegrationAsync(ulong integrationId, int expireBehaviour, int expireGracePeriod, bool enableEmoticons) + => await this.Discord.ApiClient.ModifyGuildIntegrationAsync(this.Id, integrationId, expireBehaviour, expireGracePeriod, enableEmoticons); + + /// + /// Removes an integration from this guild. + /// + /// Integration to remove. + /// Reason for audit logs. + /// + /// Thrown when the client does not have the permission. + /// Thrown when the guild does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task DeleteIntegrationAsync(DiscordIntegration integration, string? reason = null) + => await this.Discord.ApiClient.DeleteGuildIntegrationAsync(this.Id, integration.Id, reason); + + /// + /// Removes an integration from this guild. + /// + /// The id of the Integration to remove. + /// Reason for audit logs. + /// + /// Thrown when the client does not have the permission. + /// Thrown when the guild does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task DeleteIntegrationAsync(ulong integrationId, string? reason = null) + => await this.Discord.ApiClient.DeleteGuildIntegrationAsync(this.Id, integrationId, reason); + + /// + /// Forces re-synchronization of an integration for this guild. + /// + /// Integration to synchronize. + /// + /// Thrown when the client does not have the permission. + /// Thrown when the guild does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task SyncIntegrationAsync(DiscordIntegration integration) + => await this.Discord.ApiClient.SyncGuildIntegrationAsync(this.Id, integration.Id); + + /// + /// Forces re-synchronization of an integration for this guild. + /// + /// The id of the Integration to synchronize. + /// + /// Thrown when the client does not have the permission. + /// Thrown when the guild does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task SyncIntegrationAsync(ulong integrationId) + => await this.Discord.ApiClient.SyncGuildIntegrationAsync(this.Id, integrationId); + + /// + /// Gets the voice regions for this guild. + /// + /// Voice regions available for this guild. + /// Thrown when Discord is unable to process the request. + public async Task> ListVoiceRegionsAsync() + { + IReadOnlyList vrs = await this.Discord.ApiClient.GetGuildVoiceRegionsAsync(this.Id); + foreach (DiscordVoiceRegion xvr in vrs) + { + this.Discord.InternalVoiceRegions.TryAdd(xvr.Id, xvr); + } + + return vrs; + } + + /// + /// Gets the active and private threads for this guild. + /// + /// A list of all the active and private threads the user can access in the server. + /// Thrown when Discord is unable to process the request. + public async Task ListActiveThreadsAsync() + { + ThreadQueryResult threads = await this.Discord.ApiClient.ListActiveThreadsAsync(this.Id); + // Gateway handles thread cache (if it does it properly + /*foreach (var thread in threads) + this.threads[thread.Id] = thread;*/ + return threads; + } + + /// + /// Gets an invite from this guild from an invite code. + /// + /// The invite code + /// An invite, or null if not in cache. + public DiscordInvite GetInvite(string code) + => this.invites.TryGetValue(code, out DiscordInvite? invite) ? invite : null; + + /// + /// Gets all the invites created for all the channels in this guild. + /// + /// A collection of invites. + /// Thrown when Discord is unable to process the request. + public async Task> GetInvitesAsync() + { + IReadOnlyList res = await this.Discord.ApiClient.GetGuildInvitesAsync(this.Id); + + DiscordIntents intents = this.Discord.Intents; + + if (!intents.HasIntent(DiscordIntents.GuildInvites)) + { + for (int i = 0; i < res.Count; i++) + { + this.invites[res[i].Code] = res[i]; + } + } + + return res; + } + + /// + /// Gets the vanity invite for this guild. + /// + /// A partial vanity invite. + /// Thrown when the client does not have the permission. + /// Thrown when Discord is unable to process the request. + public async Task GetVanityInviteAsync() + => await this.Discord.ApiClient.GetGuildVanityUrlAsync(this.Id); + + /// + /// Gets all the webhooks created for all the channels in this guild. + /// + /// A collection of webhooks this guild has. + /// Thrown when the client does not have the permission. + /// Thrown when Discord is unable to process the request. + public async Task> GetWebhooksAsync() + => await this.Discord.ApiClient.GetGuildWebhooksAsync(this.Id); + + /// + /// Gets this guild's widget image. + /// + /// The format of the widget. + /// The URL of the widget image. + public string GetWidgetImage(DiscordWidgetType bannerType = DiscordWidgetType.Shield) + { + string param = bannerType switch + { + DiscordWidgetType.Banner1 => "banner1", + DiscordWidgetType.Banner2 => "banner2", + DiscordWidgetType.Banner3 => "banner3", + DiscordWidgetType.Banner4 => "banner4", + _ => "shield", + }; + return $"{Endpoints.BASE_URI}/{Endpoints.GUILDS}/{this.Id}/{Endpoints.WIDGET_PNG}?style={param}"; + } + + /// + /// Gets a member of this guild by their user ID. + /// + /// ID of the member to get. + /// Whether to always make a REST request and update the member cache. + /// The requested member. + /// Thrown when Discord is unable to process the request. + /// Thrown when the member does not exist in this guild. + public async Task GetMemberAsync(ulong userId, bool updateCache = false) + { + if (!updateCache && this.members != null && this.members.TryGetValue(userId, out DiscordMember? mbr)) + { + return mbr; + } + + mbr = await this.Discord.ApiClient.GetGuildMemberAsync(this.Id, userId); + + DiscordIntents intents = this.Discord.Intents; + + if (intents.HasIntent(DiscordIntents.GuildMembers)) + { + if (this.members != null) + { + this.members[userId] = mbr; + } + } + + return mbr; + } + + /// + /// Retrieves a full list of members from Discord. This method will bypass cache. This will execute one API request per 1000 entities. + /// + /// Cancels the enumeration before the next api request + /// A collection of all members in this guild. + /// Thrown when Discord is unable to process the request. + public async IAsyncEnumerable GetAllMembersAsync + ( + [EnumeratorCancellation] + CancellationToken cancellationToken = default + ) + { + int recievedLastCall = 1000; + ulong last = 0ul; + while (recievedLastCall == 1000) + { + if (cancellationToken.IsCancellationRequested) + { + yield break; + } + + IReadOnlyList transportMembers = await this.Discord.ApiClient.ListGuildMembersAsync(this.Id, 1000, last == 0 ? null : last); + recievedLastCall = transportMembers.Count; + + foreach (TransportMember transportMember in transportMembers) + { + this.Discord.UpdateUserCache(new(transportMember.User) + { + Discord = this.Discord + }); + + yield return new(transportMember) + { + Discord = this.Discord, + guild_id = this.Id + }; + } + + TransportMember? lastMember = transportMembers.LastOrDefault(); + last = lastMember?.User.Id ?? 0; + } + } + + /// + /// Requests that Discord send a list of guild members based on the specified arguments. This method will fire the GuildMembersChunked event. + /// If no arguments aside from and are specified, this will request all guild members. + /// + /// Filters the returned members based on what the username starts with. Either this or must not be null. + /// The must also be greater than 0 if this is specified. + /// Total number of members to request. This must be greater than 0 if is specified. + /// Whether to include the associated with the fetched members. + /// Whether to limit the request to the specified user ids. Either this or must not be null. + /// The unique string to identify the response. This must be unique per-guild if multiple requests to the same guild are made. + /// A cancellation token to cancel the iterator with. + /// An asynchronous iterator that will return all members. + public async IAsyncEnumerable EnumerateRequestMembersAsync + ( + string query = "", + int limit = 0, + bool? presences = null, + IEnumerable? userIds = null, + string? nonce = null, + [EnumeratorCancellation] CancellationToken cancellationToken = default + ) + { + if (this.Discord is not DiscordClient client) + { + throw new InvalidOperationException("This operation is only valid for regular Discord clients."); + } + + ChannelReader reader = client.RegisterGuildMemberChunksEnumerator(this.Id, nonce); + + await RequestMembersAsync(query, limit, presences, userIds, nonce); + + await foreach (var evt in reader.ReadAllAsync(cancellationToken)) + { + foreach (DiscordMember member in evt.Members) + { + yield return member; + } + } + } + + /// + /// Requests that Discord send a list of guild members based on the specified arguments. This method will fire the GuildMembersChunked event. + /// If no arguments aside from and are specified, this will request all guild members. + /// + /// Filters the returned members based on what the username starts with. Either this or must not be null. + /// The must also be greater than 0 if this is specified. + /// Total number of members to request. This must be greater than 0 if is specified. + /// Whether to include the associated with the fetched members. + /// Whether to limit the request to the specified user ids. Either this or must not be null. + /// The unique string to identify the response. + public async Task RequestMembersAsync(string query = "", int limit = 0, bool? presences = null, IEnumerable? userIds = null, string? nonce = null) + { + if (this.Discord is not DiscordClient client) + { + throw new InvalidOperationException("This operation is only valid for regular Discord clients."); + } + + if (query == null && userIds == null) + { + throw new ArgumentException("The query and user IDs cannot both be null."); + } + + if (query != null && userIds != null) + { + query = null; + } + + GatewayRequestGuildMembers gatewayRequestGuildMembers = new(this) + { + Query = query, + Limit = limit >= 0 ? limit : 0, + Presences = presences, + UserIds = userIds, + Nonce = nonce + }; + +#pragma warning disable DSP0004 + await client.SendPayloadAsync(GatewayOpCode.RequestGuildMembers, gatewayRequestGuildMembers, this.Id); +#pragma warning restore DSP0004 + } + + /// + /// Gets all the channels this guild has. + /// + /// A collection of this guild's channels. + /// Thrown when Discord is unable to process the request. + public async Task> GetChannelsAsync() + => await this.Discord.ApiClient.GetGuildChannelsAsync(this.Id); + + /// + /// Creates a new role in this guild. + /// + /// Name of the role. + /// Permissions for the role. + /// Color for the role. + /// Whether the role is to be hoisted. + /// Whether the role is to be mentionable. + /// Reason for audit logs. + /// The icon to add to this role + /// The emoji to add to this role. Must be unicode. + /// The newly-created role. + /// Thrown when the client does not have the permission. + /// Thrown when Discord is unable to process the request. + public async Task CreateRoleAsync(string? name = null, DiscordPermissions? permissions = null, DiscordColor? color = null, bool? hoist = null, bool? mentionable = null, string? reason = null, Stream? icon = null, DiscordEmoji? emoji = null) + => await this.Discord.ApiClient.CreateGuildRoleAsync(this.Id, name, permissions, color?.Value, hoist, mentionable, icon, emoji?.ToString(), reason); + + /// + /// Gets a channel from this guild by its ID. + /// + /// ID of the channel to get. + /// Requested channel. + internal DiscordChannel? GetChannel(ulong id) + => this.channels != null && this.channels.TryGetValue(id, out DiscordChannel? channel) ? channel : null; + + /// + /// Gets a channel from this guild by its ID. + /// + /// ID of the channel to get. + /// If set to true this method will skip all caches and always perform a rest api call + /// Requested channel. + /// Thrown when Discord is unable to process the request. + /// Thrown when this channel does not exists + /// Thrown when the channel exists but does not belong to this guild instance. + public async Task GetChannelAsync(ulong id, bool skipCache = false) + { + DiscordChannel? channel; + if (skipCache) + { + channel = await this.Discord.ApiClient.GetChannelAsync(id); + + if (channel.GuildId is null || (channel.GuildId is not null && channel.GuildId.Value != this.Id)) + { + throw new InvalidOperationException("The channel exists but does not belong to this guild."); + } + + return channel; + } + + if (this.channels is not null && this.channels.TryGetValue(id, out channel)) + { + return channel; + } + + if (this.threads.TryGetValue(id, out DiscordThreadChannel? threadChannel)) + { + return threadChannel; + } + + channel = await this.Discord.ApiClient.GetChannelAsync(id); + + if (channel.GuildId is null || (channel.GuildId is not null && channel.GuildId.Value != this.Id)) + { + throw new InvalidOperationException("The channel exists but does not belong to this guild."); + } + + return channel; + } + + /// + /// Gets audit log entries for this guild. + /// + /// Maximum number of entries to fetch. Defaults to 100 + /// Filter by member responsible. + /// Filter by action type. + /// A collection of requested audit log entries. + /// Thrown when the client does not have the permission. + /// Thrown when Discord is unable to process the request. + /// If you set to null, it will fetch all entries. This may take a while as it will result in multiple api calls + public async IAsyncEnumerable GetAuditLogsAsync + ( + int? limit = 100, + DiscordMember? byMember = null, + DiscordAuditLogActionType? actionType = null + ) + { + //Get all entries from api + int entriesAcquiredLastCall = 1, totalEntriesCollected = 0, remainingEntries; + ulong last = 0; + while (entriesAcquiredLastCall > 0) + { + remainingEntries = limit != null ? limit.Value - totalEntriesCollected : 100; + remainingEntries = Math.Min(100, remainingEntries); + if (remainingEntries <= 0) + { + break; + } + + AuditLog guildAuditLog = await this.Discord.ApiClient.GetAuditLogsAsync(this.Id, remainingEntries, null, + last == 0 ? null : last, byMember?.Id, actionType); + entriesAcquiredLastCall = guildAuditLog.Entries.Count(); + totalEntriesCollected += entriesAcquiredLastCall; + if (entriesAcquiredLastCall > 0) + { + last = guildAuditLog.Entries.Last().Id; + IAsyncEnumerable parsedEntries = AuditLogParser.ParseAuditLogToEntriesAsync(this, guildAuditLog); + await foreach (DiscordAuditLogEntry discordAuditLogEntry in parsedEntries) + { + yield return discordAuditLogEntry; + } + } + + if (limit.HasValue) + { + int remaining = limit.Value - totalEntriesCollected; + if (remaining < 1) + { + break; + } + } + else if (entriesAcquiredLastCall < 100) + { + break; + } + } + } + + /// + /// Gets all of this guild's custom emojis. + /// + /// All of this guild's custom emojis. + /// Thrown when Discord is unable to process the request. + public async Task> GetEmojisAsync() + => await this.Discord.ApiClient.GetGuildEmojisAsync(this.Id); + + /// + /// Gets this guild's specified custom emoji. + /// + /// ID of the emoji to get. + /// The requested custom emoji. + /// Thrown when Discord is unable to process the request. + public async Task GetEmojiAsync(ulong id) + => await this.Discord.ApiClient.GetGuildEmojiAsync(this.Id, id); + + /// + /// Creates a new custom emoji for this guild. + /// + /// Name of the new emoji. + /// Image to use as the emoji. + /// Roles for which the emoji will be available. This works only if your application is whitelisted as integration. + /// Reason for audit log. + /// The newly-created emoji. + /// Thrown when the client does not have the permission. + /// Thrown when Discord is unable to process the request. + public async Task CreateEmojiAsync(string name, Stream image, IEnumerable? roles = null, string? reason = null) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentNullException(nameof(name)); + } + + name = name.Trim(); + if (name.Length is < 2 or > 50) + { + throw new ArgumentException("Emoji name needs to be between 2 and 50 characters long."); + } + + ArgumentNullException.ThrowIfNull(image); + + string? image64 = null; + using (InlineMediaTool imgtool = new(image)) + { + image64 = imgtool.GetBase64(); + } + + return await this.Discord.ApiClient.CreateGuildEmojiAsync(this.Id, name, image64, roles?.Select(xr => xr.Id), reason); + } + + /// + /// Modifies a this guild's custom emoji. + /// + /// Emoji to modify. + /// New name for the emoji. + /// Roles for which the emoji will be available. This works only if your application is whitelisted as integration. + /// Reason for audit log. + /// The modified emoji. + /// Thrown when the client does not have the permission. + /// Thrown when Discord is unable to process the request. + public async Task ModifyEmojiAsync(DiscordGuildEmoji emoji, string name, IEnumerable? roles = null, string? reason = null) + { + ArgumentNullException.ThrowIfNull(emoji); + if (emoji.Guild.Id != this.Id) + { + throw new ArgumentException("This emoji does not belong to this guild."); + } + + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentNullException(nameof(name)); + } + + name = name.Trim(); + return name.Length is < 2 or > 50 + ? throw new ArgumentException("Emoji name needs to be between 2 and 50 characters long.") + : await this.Discord.ApiClient.ModifyGuildEmojiAsync(this.Id, emoji.Id, name, roles?.Select(xr => xr.Id), reason); + } + + /// + /// Deletes this guild's custom emoji. + /// + /// Emoji to delete. + /// Reason for audit log. + /// + /// Thrown when the client does not have the permission. + /// Thrown when Discord is unable to process the request. + /// Thrown when the emoji does not exist on this guild + public async Task DeleteEmojiAsync(DiscordGuildEmoji emoji, string? reason = null) + { + ArgumentNullException.ThrowIfNull(emoji); + + if (emoji.Guild.Id != this.Id) + { + throw new ArgumentException("This emoji does not belong to this guild."); + } + else + { + await this.Discord.ApiClient.DeleteGuildEmojiAsync(this.Id, emoji.Id, reason); + } + } + + /// + /// Deletes this guild's custom emoji. + /// + /// Emoji to delete. + /// Reason for audit log. + /// + /// Thrown when the client does not have the permission. + /// Thrown when Discord is unable to process the request. + /// Thrown when the emoji does not exist on this guild + public async Task DeleteEmojiAsync(ulong emojiId, string? reason = null) + => await this.Discord.ApiClient.DeleteGuildEmojiAsync(this.Id, emojiId, reason); + + /// + /// Gets the default channel for this guild. + /// Default channel is the first channel current member can see. + /// + /// This member's default guild. + /// Thrown when Discord is unable to process the request. + public DiscordChannel? GetDefaultChannel() + { + return this.channels?.Values.Where(xc => xc.Type == DiscordChannelType.Text) + .OrderBy(xc => xc.Position) + .FirstOrDefault(xc => xc.PermissionsFor(this.CurrentMember).HasPermission(DiscordPermission.ViewChannel)); + } + + /// + /// Gets the guild's widget + /// + /// The guild's widget + public async Task GetWidgetAsync() + => await this.Discord.ApiClient.GetGuildWidgetAsync(this.Id); + + /// + /// Gets the guild's widget settings + /// + /// The guild's widget settings + public async Task GetWidgetSettingsAsync() + => await this.Discord.ApiClient.GetGuildWidgetSettingsAsync(this.Id); + + /// + /// Modifies the guild's widget settings + /// + /// If the widget is enabled or not + /// Widget channel + /// Reason the widget settings were modified + /// The newly modified widget settings + public async Task ModifyWidgetSettingsAsync(bool? isEnabled = null, DiscordChannel? channel = null, string? reason = null) + => await this.Discord.ApiClient.ModifyGuildWidgetSettingsAsync(this.Id, isEnabled, channel?.Id, reason); + + /// + /// Gets all of this guild's templates. + /// + /// All of the guild's templates. + /// Throws when the client does not have the permission. + /// Thrown when Discord is unable to process the request. + public async Task> GetTemplatesAsync() + => await this.Discord.ApiClient.GetGuildTemplatesAsync(this.Id); + + /// + /// Creates a guild template. + /// + /// Name of the template. + /// Description of the template. + /// The template created. + /// Throws when a template already exists for the guild or a null parameter is provided for the name. + /// Throws when the client does not have the permission. + /// Thrown when Discord is unable to process the request. + public async Task CreateTemplateAsync(string name, string? description = null) + => await this.Discord.ApiClient.CreateGuildTemplateAsync(this.Id, name, description); + + /// + /// Syncs the template to the current guild's state. + /// + /// The code of the template to sync. + /// The template synced. + /// Throws when the template for the code cannot be found + /// Throws when the client does not have the permission. + /// Thrown when Discord is unable to process the request. + public async Task SyncTemplateAsync(string code) + => await this.Discord.ApiClient.SyncGuildTemplateAsync(this.Id, code); + + /// + /// Modifies the template's metadata. + /// + /// The template's code. + /// Name of the template. + /// Description of the template. + /// The template modified. + /// Throws when the template for the code cannot be found + /// Throws when the client does not have the permission. + /// Thrown when Discord is unable to process the request. + public async Task ModifyTemplateAsync(string code, string? name = null, string? description = null) + => await this.Discord.ApiClient.ModifyGuildTemplateAsync(this.Id, code, name, description); + + /// + /// Deletes the template. + /// + /// The code of the template to delete. + /// The deleted template. + /// Throws when the template for the code cannot be found + /// Throws when the client does not have the permission. + /// Thrown when Discord is unable to process the request. + public async Task DeleteTemplateAsync(string code) + => await this.Discord.ApiClient.DeleteGuildTemplateAsync(this.Id, code); + + /// + /// Gets this guild's membership screening form. + /// + /// This guild's membership screening form. + /// Thrown when Discord is unable to process the request. + public async Task GetMembershipScreeningFormAsync() + => await this.Discord.ApiClient.GetGuildMembershipScreeningFormAsync(this.Id); + + /// + /// Modifies this guild's membership screening form. + /// + /// Action to perform + /// The modified screening form. + /// Thrown when the client doesn't have the permission, or community is not enabled on this guild. + /// Thrown when Discord is unable to process the request. + public async Task ModifyMembershipScreeningFormAsync(Action action) + { + MembershipScreeningEditModel editModel = new(); + action(editModel); + return await this.Discord.ApiClient.ModifyGuildMembershipScreeningFormAsync(this.Id, editModel.Enabled, editModel.Fields, editModel.Description); + } + + /// + /// Gets a list of stickers from this guild. + /// + /// + public async Task> GetStickersAsync() + => await this.Discord.ApiClient.GetGuildStickersAsync(this.Id); + + /// + /// Gets a sticker from this guild. + /// + /// The id of the sticker. + /// + public async Task GetStickerAsync(ulong stickerId) + => await this.Discord.ApiClient.GetGuildStickerAsync(this.Id, stickerId); + + /// + /// Creates a sticker in this guild. Lottie stickers can only be created on verified and/or partnered servers. + /// + /// The name of the sticker. + /// The description of the sticker. + /// The tags of the sticker. This must be a unicode emoji. + /// The image content of the sticker. + /// The image format of the sticker. + /// The reason this sticker is being created. + + public async Task CreateStickerAsync(string name, string description, string tags, Stream imageContents, DiscordStickerFormat format, string? reason = null) + { + string contentType, extension; + if (format is DiscordStickerFormat.PNG or DiscordStickerFormat.APNG) + { + contentType = "image/png"; + extension = "png"; + } + else + { + if (!this.Features.Contains("PARTNERED") && !this.Features.Contains("VERIFIED")) + { + throw new InvalidOperationException("Lottie stickers can only be created on partnered or verified guilds."); + } + + contentType = "application/json"; + extension = "json"; + } + + return await this.Discord.ApiClient.CreateGuildStickerAsync(this.Id, name, description ?? string.Empty, tags, new DiscordMessageFile(null, imageContents, null, extension, contentType), reason); + } + + /// + /// Modifies a sticker in this guild. + /// + /// The id of the sticker. + /// Action to perform. + /// Reason for audit log. + public async Task ModifyStickerAsync(ulong stickerId, Action action, string? reason = null) + { + StickerEditModel editModel = new(); + action(editModel); + return await this.Discord.ApiClient.ModifyStickerAsync(this.Id, stickerId, editModel.Name, editModel.Description, editModel.Tags, reason ?? editModel.AuditLogReason); + } + + /// + /// Modifies a sticker in this guild. + /// + /// Sticker to modify. + /// Action to perform. + /// Reason for audit log. + public async Task ModifyStickerAsync(DiscordMessageSticker sticker, Action action, string? reason = null) + { + StickerEditModel editModel = new(); + action(editModel); + return await this.Discord.ApiClient.ModifyStickerAsync(this.Id, sticker.Id, editModel.Name, editModel.Description, editModel.Tags, reason ?? editModel.AuditLogReason); + } + + /// + /// Deletes a sticker in this guild. + /// + /// The id of the sticker. + /// Reason for audit log. + public async Task DeleteStickerAsync(ulong stickerId, string? reason = null) + => await this.Discord.ApiClient.DeleteStickerAsync(this.Id, stickerId, reason); + + /// + /// Deletes a sticker in this guild. + /// + /// Sticker to delete. + /// Reason for audit log. + public async Task DeleteStickerAsync(DiscordMessageSticker sticker, string? reason = null) + => await this.Discord.ApiClient.DeleteStickerAsync(this.Id, sticker.Id, reason); + + /// + /// Gets all the application commands in this guild. + /// + /// Whether to include localizations in the response. + /// A list of application commands in this guild. + public async Task> GetApplicationCommandsAsync(bool withLocalizations = false) => + await this.Discord.ApiClient.GetGuildApplicationCommandsAsync(this.Discord.CurrentApplication.Id, this.Id, withLocalizations); + + /// + /// Overwrites the existing application commands in this guild. New commands are automatically created and missing commands are automatically delete + /// + /// The list of commands to overwrite with. + /// The list of guild commands + public async Task> BulkOverwriteApplicationCommandsAsync(IEnumerable commands) => + await this.Discord.ApiClient.BulkOverwriteGuildApplicationCommandsAsync(this.Discord.CurrentApplication.Id, this.Id, commands); + + /// + /// Creates or overwrites a application command in this guild. + /// + /// The command to create. + /// The created command. + public async Task CreateApplicationCommandAsync(DiscordApplicationCommand command) => + await this.Discord.ApiClient.CreateGuildApplicationCommandAsync(this.Discord.CurrentApplication.Id, this.Id, command); + + /// + /// Edits a application command in this guild. + /// + /// The id of the command to edit. + /// Action to perform. + /// The edit command. + public async Task EditApplicationCommandAsync(ulong commandId, Action action) + { + ApplicationCommandEditModel editModel = new(); + action(editModel); + return await this.Discord.ApiClient.EditGuildApplicationCommandAsync(this.Discord.CurrentApplication.Id, this.Id, commandId, editModel.Name, editModel.Description, editModel.Options, editModel.DefaultPermission, editModel.NSFW, default, default, editModel.AllowDMUsage, editModel.DefaultMemberPermissions); + } + + /// + /// Gets a application command in this guild by its id. + /// + /// The ID of the command to get. + /// The command with the ID. + public async Task GetApplicationCommandAsync(ulong commandId) => + await this.Discord.ApiClient.GetGlobalApplicationCommandAsync(this.Discord.CurrentApplication.Id, commandId); + + /// + /// Gets a application command in this guild by its name. + /// + /// The name of the command to get. + /// Whether to include localizations in the response. + /// The command with the name. This is null when the command is not found + public async Task GetApplicationCommandAsync(string commandName, bool withLocalizations = false) + { + foreach (DiscordApplicationCommand command in await this.Discord.ApiClient.GetGlobalApplicationCommandsAsync(this.Discord.CurrentApplication.Id, withLocalizations)) + { + if (command.Name == commandName) + { + return command; + } + } + + return null; + } + + /// + /// Gets this guild's welcome screen. + /// + /// This guild's welcome screen object. + /// Thrown when Discord is unable to process the request. + public async Task GetWelcomeScreenAsync() => + await this.Discord.ApiClient.GetGuildWelcomeScreenAsync(this.Id); + + /// + /// Modifies this guild's welcome screen. + /// + /// Action to perform. + /// Reason for audit log. + /// The modified welcome screen. + /// Thrown when the client doesn't have the permission, or community is not enabled on this guild. + /// Thrown when Discord is unable to process the request. + public async Task ModifyWelcomeScreenAsync(Action action, string? reason = null) + { + WelcomeScreenEditModel editModel = new(); + action(editModel); + return await this.Discord.ApiClient.ModifyGuildWelcomeScreenAsync(this.Id, editModel.Enabled, editModel.WelcomeChannels, editModel.Description, reason); + } + + /// + /// Gets all application command permissions in this guild. + /// + /// A list of permissions. + public async Task> GetApplicationCommandsPermissionsAsync() + => await this.Discord.ApiClient.GetGuildApplicationCommandPermissionsAsync(this.Discord.CurrentApplication.Id, this.Id); + + /// + /// Gets permissions for a application command in this guild. + /// + /// The command to get them for. + /// The permissions. + public async Task GetApplicationCommandPermissionsAsync(DiscordApplicationCommand command) + => await this.Discord.ApiClient.GetApplicationCommandPermissionsAsync(this.Discord.CurrentApplication.Id, this.Id, command.Id); + + /// + /// Edits permissions for a application command in this guild. + /// + /// The command to edit permissions for. + /// The list of permissions to use. + /// The edited permissions. + public async Task EditApplicationCommandPermissionsAsync(DiscordApplicationCommand command, IEnumerable permissions) + => await this.Discord.ApiClient.EditApplicationCommandPermissionsAsync(this.Discord.CurrentApplication.Id, this.Id, command.Id, permissions); + + /// + /// Batch edits permissions for a application command in this guild. + /// + /// The list of permissions to use. + /// A list of edited permissions. + public async Task> BatchEditApplicationCommandPermissionsAsync(IEnumerable permissions) + => await this.Discord.ApiClient.BatchEditApplicationCommandPermissionsAsync(this.Discord.CurrentApplication.Id, this.Id, permissions); + + /// + /// Creates an auto-moderation rule in the guild. + /// + /// The rule name. + /// The event in which the rule should be triggered. + /// The type of content which can trigger the rule. + /// Metadata used to determine whether a rule should be triggered. This argument can be skipped depending eventType value. + /// Actions that will execute after the trigger of the rule. + /// Whether the rule is enabled or not. + /// Roles that will not trigger the rule. + /// Channels which will not trigger the rule. + /// Reason for audit logs. + /// The created rule. + public async Task CreateAutoModerationRuleAsync + ( + string name, + DiscordRuleEventType eventType, + DiscordRuleTriggerType triggerType, + DiscordRuleTriggerMetadata triggerMetadata, + IReadOnlyList actions, + Optional enabled = default, + Optional> exemptRoles = default, + Optional> exemptChannels = default, + string? reason = null + ) + { + return await this.Discord.ApiClient.CreateGuildAutoModerationRuleAsync + ( + this.Id, + name, + eventType, + triggerType, + triggerMetadata, + actions, + enabled, + exemptRoles, + exemptChannels, + reason + ); + } + + /// + /// Gets an auto-moderation rule by an id. + /// + /// The rule id. + /// The found rule. + public async Task GetAutoModerationRuleAsync(ulong ruleId) + => await this.Discord.ApiClient.GetGuildAutoModerationRuleAsync(this.Id, ruleId); + + /// + /// Gets all auto-moderation rules in the guild. + /// + /// All rules available in the guild. + public async Task> GetAutoModerationRulesAsync() + => await this.Discord.ApiClient.GetGuildAutoModerationRulesAsync(this.Id); + + /// + /// Modify an auto-moderation rule in the guild. + /// + /// The id of the rule that will be edited. + /// Action to perform on this rule. + /// The modified rule. + /// All arguments are optionals. + public async Task ModifyAutoModerationRuleAsync(ulong ruleId, Action action) + { + AutoModerationRuleEditModel model = new(); + + action(model); + + return await this.Discord.ApiClient.ModifyGuildAutoModerationRuleAsync + ( + this.Id, + ruleId, + model.Name, + model.EventType, + model.TriggerMetadata, + model.Actions, + model.Enable, + model.ExemptRoles, + model.ExemptChannels, + model.AuditLogReason + ); + } + + /// + /// Deletes a auto-moderation rule by an id. + /// + /// The rule id. + /// Reason for audit logs. + /// + public async Task DeleteAutoModerationRuleAsync(ulong ruleId, string? reason = null) + => await this.Discord.ApiClient.DeleteGuildAutoModerationRuleAsync(this.Id, ruleId, reason); + + /// + /// Gets the current user's voice state in this guild. + /// + /// Whether to skip the cache or not. + /// Returns the users voicestate. This is null if the user is in no voice channel + public async Task GetCurrentUserVoiceStateAsync(bool skipCache = false) + { + if (!skipCache && this.VoiceStates.TryGetValue(this.Discord.CurrentUser.Id, out DiscordVoiceState? voiceState)) + { + return voiceState; + } + + try + { + return await this.Discord.ApiClient.GetCurrentUserVoiceStateAsync(this.Id); + } + catch (NotFoundException) + { + return null; + } + } + + /// + /// Gets user's voice state in this guild. + /// + /// The member to get the voice state for. + /// Whether to skip the cache or not. + /// Returns the users voicestate. This is null if the user is in no voice channel + public Task GetMemberVoiceStateAsync(DiscordUser member, bool skipCache = false) + => GetMemberVoiceStateAsync(member.Id, skipCache); + + /// + /// Gets user's voice state in this guild. + /// + /// The member ID to get the voice state for. + /// Whether to skip the cache or not. + /// Returns the users voicestate. This is null if the user is in no voice channel + public async Task GetMemberVoiceStateAsync(ulong memberId, bool skipCache = false) + { + if (!skipCache && this.VoiceStates.TryGetValue(memberId, out DiscordVoiceState? voiceState)) + { + return voiceState; + } + + try + { + return await this.Discord.ApiClient.GetUserVoiceStateAsync(this.Id, memberId); + } + catch (NotFoundException) + { + return null; + } + } + + #endregion + + /// + /// Returns a string representation of this guild. + /// + /// String representation of this guild. + public override string ToString() => $"Guild {this.Id}; {this.Name}"; + + /// + /// Checks whether this is equal to another object. + /// + /// Object to compare to. + /// Whether the object is equal to this . + public override bool Equals(object obj) => Equals(obj as DiscordGuild); + + /// + /// Checks whether this is equal to another . + /// + /// to compare to. + /// Whether the is equal to this . + public bool Equals(DiscordGuild e) => e is not null && (ReferenceEquals(this, e) || this.Id == e.Id); + + /// + /// Gets the hash code for this . + /// + /// The hash code for this . + public override int GetHashCode() => this.Id.GetHashCode(); + + /// + /// Gets whether the two objects are equal. + /// + /// First member to compare. + /// Second member to compare. + /// Whether the two members are equal. + public static bool operator ==(DiscordGuild e1, DiscordGuild e2) + { + object? o1 = e1; + object? o2 = e2; + + 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 member to compare. + /// Second member to compare. + /// Whether the two members are not equal. + public static bool operator !=(DiscordGuild e1, DiscordGuild e2) + => !(e1 == e2); +} + +/// +/// Represents guild verification level. +/// +public enum DiscordVerificationLevel : int +{ + /// + /// No verification. Anyone can join and chat right away. + /// + None = 0, + + /// + /// Low verification level. Users are required to have a verified email attached to their account in order to be able to chat. + /// + Low = 1, + + /// + /// Medium verification level. Users are required to have a verified email attached to their account, and account age need to be at least 5 minutes in order to be able to chat. + /// + Medium = 2, + + /// + /// (╯°□°)╯︵ ┻━┻ verification level. Users are required to have a verified email attached to their account, account age need to be at least 5 minutes, and they need to be in the server for at least 10 minutes in order to be able to chat. + /// + High = 3, + + /// + /// ┻━┻ ミヽ(ಠ益ಠ)ノ彡┻━┻ verification level. Users are required to have a verified phone number attached to their account. + /// + Highest = 4 +} + +/// +/// Represents default notification level for a guild. +/// +public enum DiscordDefaultMessageNotifications : int +{ + /// + /// All messages will trigger push notifications. + /// + AllMessages = 0, + + /// + /// Only messages that mention the user (or a role he's in) will trigger push notifications. + /// + MentionsOnly = 1 +} + +/// +/// Represents multi-factor authentication level required by a guild to use administrator functionality. +/// +public enum DiscordMfaLevel : int +{ + /// + /// Multi-factor authentication is not required to use administrator functionality. + /// + Disabled = 0, + + /// + /// Multi-factor authentication is required to use administrator functionality. + /// + Enabled = 1 +} + +/// +/// Represents the value of explicit content filter in a guild. +/// +public enum DiscordExplicitContentFilter : int +{ + /// + /// Explicit content filter is disabled. + /// + Disabled = 0, + + /// + /// Only messages from members without any roles are scanned. + /// + MembersWithoutRoles = 1, + + /// + /// Messages from all members are scanned. + /// + AllMembers = 2 +} + +/// +/// Represents the formats for a guild widget. +/// +public enum DiscordWidgetType : int +{ + /// + /// The widget is represented in shield format. + /// This is the default widget type. + /// + Shield = 0, + + /// + /// The widget is represented as the first banner type. + /// + Banner1 = 1, + + /// + /// The widget is represented as the second banner type. + /// + Banner2 = 2, + + /// + /// The widget is represented as the third banner type. + /// + Banner3 = 3, + + /// + /// The widget is represented in the fourth banner type. + /// + Banner4 = 4 +} diff --git a/DSharpPlus/Entities/Guild/DiscordGuildEmbed.cs b/DSharpPlus/Entities/Guild/DiscordGuildEmbed.cs index ddaaa32504..915cb2e58c 100644 --- a/DSharpPlus/Entities/Guild/DiscordGuildEmbed.cs +++ b/DSharpPlus/Entities/Guild/DiscordGuildEmbed.cs @@ -1,21 +1,21 @@ -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents a Discord guild widget. -/// -public class DiscordGuildEmbed -{ - /// - /// Gets whether the embed is enabled. - /// - [JsonProperty("enabled", NullValueHandling = NullValueHandling.Ignore)] - public bool IsEnabled { get; set; } - - /// - /// Gets the ID of the widget channel. - /// - [JsonProperty("channel_id", NullValueHandling = NullValueHandling.Ignore)] - public ulong ChannelId { get; set; } -} +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Represents a Discord guild widget. +/// +public class DiscordGuildEmbed +{ + /// + /// Gets whether the embed is enabled. + /// + [JsonProperty("enabled", NullValueHandling = NullValueHandling.Ignore)] + public bool IsEnabled { get; set; } + + /// + /// Gets the ID of the widget channel. + /// + [JsonProperty("channel_id", NullValueHandling = NullValueHandling.Ignore)] + public ulong ChannelId { get; set; } +} diff --git a/DSharpPlus/Entities/Guild/DiscordGuildEmoji.cs b/DSharpPlus/Entities/Guild/DiscordGuildEmoji.cs index a88254649e..30badec705 100644 --- a/DSharpPlus/Entities/Guild/DiscordGuildEmoji.cs +++ b/DSharpPlus/Entities/Guild/DiscordGuildEmoji.cs @@ -1,48 +1,48 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -public sealed class DiscordGuildEmoji : DiscordEmoji -{ - /// - /// Gets the user that created this emoji. - /// - [JsonIgnore] - public new DiscordUser User { get; internal set; } - - /// - /// Gets the guild to which this emoji belongs. - /// - [JsonIgnore] - public DiscordGuild Guild { get; internal set; } - - internal DiscordGuildEmoji() { } - - /// - /// Modifies this emoji. - /// - /// New name for this emoji. - /// Roles for which this emoji will be available. This works only if your application is whitelisted as integration. - /// Reason for audit log. - /// The modified emoji. - /// Thrown when the client does not have the permission. - /// Thrown when the emoji does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public Task ModifyAsync(string name, IEnumerable roles = null, string reason = null) - => this.Guild.ModifyEmojiAsync(this, name, roles, reason); - - /// - /// Deletes this emoji. - /// - /// Reason for audit log. - /// - /// 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 DeleteAsync(string reason = null) - => this.Guild.DeleteEmojiAsync(this, reason); -} +using System.Collections.Generic; +using System.Threading.Tasks; +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +public sealed class DiscordGuildEmoji : DiscordEmoji +{ + /// + /// Gets the user that created this emoji. + /// + [JsonIgnore] + public new DiscordUser User { get; internal set; } + + /// + /// Gets the guild to which this emoji belongs. + /// + [JsonIgnore] + public DiscordGuild Guild { get; internal set; } + + internal DiscordGuildEmoji() { } + + /// + /// Modifies this emoji. + /// + /// New name for this emoji. + /// Roles for which this emoji will be available. This works only if your application is whitelisted as integration. + /// Reason for audit log. + /// The modified emoji. + /// Thrown when the client does not have the permission. + /// Thrown when the emoji does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public Task ModifyAsync(string name, IEnumerable roles = null, string reason = null) + => this.Guild.ModifyEmojiAsync(this, name, roles, reason); + + /// + /// Deletes this emoji. + /// + /// Reason for audit log. + /// + /// 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 DeleteAsync(string reason = null) + => this.Guild.DeleteEmojiAsync(this, reason); +} diff --git a/DSharpPlus/Entities/Guild/DiscordGuildMembershipScreening.cs b/DSharpPlus/Entities/Guild/DiscordGuildMembershipScreening.cs index 18bd0ab326..9427d02eb2 100644 --- a/DSharpPlus/Entities/Guild/DiscordGuildMembershipScreening.cs +++ b/DSharpPlus/Entities/Guild/DiscordGuildMembershipScreening.cs @@ -1,29 +1,29 @@ -using System; -using System.Collections.Generic; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents a guild's membership screening form. -/// -public class DiscordGuildMembershipScreening -{ - /// - /// Gets when the fields were last updated. - /// - [JsonProperty("version")] - public DateTimeOffset Version { get; internal set; } - - /// - /// Gets the steps in the screening form. - /// - [JsonProperty("form_fields")] - public IReadOnlyList Fields { get; internal set; } - - /// - /// Gets the server description shown in the screening form. - /// - [JsonProperty("description")] - public string Description { get; internal set; } -} +using System; +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Represents a guild's membership screening form. +/// +public class DiscordGuildMembershipScreening +{ + /// + /// Gets when the fields were last updated. + /// + [JsonProperty("version")] + public DateTimeOffset Version { get; internal set; } + + /// + /// Gets the steps in the screening form. + /// + [JsonProperty("form_fields")] + public IReadOnlyList Fields { get; internal set; } + + /// + /// Gets the server description shown in the screening form. + /// + [JsonProperty("description")] + public string Description { get; internal set; } +} diff --git a/DSharpPlus/Entities/Guild/DiscordGuildMembershipScreeningField.cs b/DSharpPlus/Entities/Guild/DiscordGuildMembershipScreeningField.cs index 431c87292d..d1ba3e2689 100644 --- a/DSharpPlus/Entities/Guild/DiscordGuildMembershipScreeningField.cs +++ b/DSharpPlus/Entities/Guild/DiscordGuildMembershipScreeningField.cs @@ -1,45 +1,45 @@ -using System.Collections.Generic; -using System.Linq; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents a field in a guild's membership screening form -/// -public class DiscordGuildMembershipScreeningField -{ - /// - /// Gets the type of the field. - /// - [JsonProperty("field_type", NullValueHandling = NullValueHandling.Ignore)] - public DiscordMembershipScreeningFieldType Type { get; internal set; } - - /// - /// Gets the title of the field. - /// - [JsonProperty("label", NullValueHandling = NullValueHandling.Ignore)] - public string Label { get; internal set; } - - /// - /// Gets the list of rules - /// - [JsonProperty("values", NullValueHandling = NullValueHandling.Ignore)] - public IReadOnlyList Values { get; internal set; } - - /// - /// Gets whether the user has to fill out this field - /// - [JsonProperty("required", NullValueHandling = NullValueHandling.Ignore)] - public bool IsRequired { get; internal set; } - - public DiscordGuildMembershipScreeningField(DiscordMembershipScreeningFieldType type, string label, IEnumerable values, bool required = true) - { - this.Type = type; - this.Label = label; - this.Values = values.ToList(); - this.IsRequired = required; - } - - internal DiscordGuildMembershipScreeningField() { } -} +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Represents a field in a guild's membership screening form +/// +public class DiscordGuildMembershipScreeningField +{ + /// + /// Gets the type of the field. + /// + [JsonProperty("field_type", NullValueHandling = NullValueHandling.Ignore)] + public DiscordMembershipScreeningFieldType Type { get; internal set; } + + /// + /// Gets the title of the field. + /// + [JsonProperty("label", NullValueHandling = NullValueHandling.Ignore)] + public string Label { get; internal set; } + + /// + /// Gets the list of rules + /// + [JsonProperty("values", NullValueHandling = NullValueHandling.Ignore)] + public IReadOnlyList Values { get; internal set; } + + /// + /// Gets whether the user has to fill out this field + /// + [JsonProperty("required", NullValueHandling = NullValueHandling.Ignore)] + public bool IsRequired { get; internal set; } + + public DiscordGuildMembershipScreeningField(DiscordMembershipScreeningFieldType type, string label, IEnumerable values, bool required = true) + { + this.Type = type; + this.Label = label; + this.Values = values.ToList(); + this.IsRequired = required; + } + + internal DiscordGuildMembershipScreeningField() { } +} diff --git a/DSharpPlus/Entities/Guild/DiscordGuildPreview.cs b/DSharpPlus/Entities/Guild/DiscordGuildPreview.cs index 4c82dc3074..b399586b72 100644 --- a/DSharpPlus/Entities/Guild/DiscordGuildPreview.cs +++ b/DSharpPlus/Entities/Guild/DiscordGuildPreview.cs @@ -1,72 +1,72 @@ -using System.Collections.Concurrent; -using System.Collections.Generic; -using DSharpPlus.Net.Serialization; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents a guild preview. -/// -public class DiscordGuildPreview : SnowflakeObject -{ - /// - /// Gets the guild's name. - /// - [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] - public string Name { get; internal set; } - - /// - /// Gets the guild's icon. - /// - [JsonProperty("icon", NullValueHandling = NullValueHandling.Ignore)] - public string Icon { get; internal set; } - - /// - /// Gets the guild's splash. - /// - [JsonProperty("splash", NullValueHandling = NullValueHandling.Ignore)] - public string Splash { get; internal set; } - - /// - /// Gets the guild's discovery splash. - /// - [JsonProperty("discovery_splash", NullValueHandling = NullValueHandling.Ignore)] - public string DiscoverySplash { get; internal set; } - - /// - /// Gets a collection of this guild's emojis. - /// - [JsonIgnore] - public IReadOnlyDictionary Emojis => new ReadOnlyConcurrentDictionary(this.emojis); - - [JsonProperty("emojis", NullValueHandling = NullValueHandling.Ignore)] - [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] - internal ConcurrentDictionary emojis; - - /// - /// Gets a collection of this guild's features. - /// - [JsonProperty("features", NullValueHandling = NullValueHandling.Ignore)] - public IReadOnlyList Features { get; internal set; } - - /// - /// Gets the approximate member count. - /// - [JsonProperty("approximate_member_count")] - public int ApproximateMemberCount { get; internal set; } - - /// - /// Gets the approximate presence count. - /// - [JsonProperty("approximate_presence_count")] - public int ApproximatePresenceCount { get; internal set; } - - /// - /// Gets the description for the guild, if the guild is discoverable. - /// - [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] - public string Description { get; internal set; } - - internal DiscordGuildPreview() { } -} +using System.Collections.Concurrent; +using System.Collections.Generic; +using DSharpPlus.Net.Serialization; +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Represents a guild preview. +/// +public class DiscordGuildPreview : SnowflakeObject +{ + /// + /// Gets the guild's name. + /// + [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] + public string Name { get; internal set; } + + /// + /// Gets the guild's icon. + /// + [JsonProperty("icon", NullValueHandling = NullValueHandling.Ignore)] + public string Icon { get; internal set; } + + /// + /// Gets the guild's splash. + /// + [JsonProperty("splash", NullValueHandling = NullValueHandling.Ignore)] + public string Splash { get; internal set; } + + /// + /// Gets the guild's discovery splash. + /// + [JsonProperty("discovery_splash", NullValueHandling = NullValueHandling.Ignore)] + public string DiscoverySplash { get; internal set; } + + /// + /// Gets a collection of this guild's emojis. + /// + [JsonIgnore] + public IReadOnlyDictionary Emojis => new ReadOnlyConcurrentDictionary(this.emojis); + + [JsonProperty("emojis", NullValueHandling = NullValueHandling.Ignore)] + [JsonConverter(typeof(SnowflakeArrayAsDictionaryJsonConverter))] + internal ConcurrentDictionary emojis; + + /// + /// Gets a collection of this guild's features. + /// + [JsonProperty("features", NullValueHandling = NullValueHandling.Ignore)] + public IReadOnlyList Features { get; internal set; } + + /// + /// Gets the approximate member count. + /// + [JsonProperty("approximate_member_count")] + public int ApproximateMemberCount { get; internal set; } + + /// + /// Gets the approximate presence count. + /// + [JsonProperty("approximate_presence_count")] + public int ApproximatePresenceCount { get; internal set; } + + /// + /// Gets the description for the guild, if the guild is discoverable. + /// + [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] + public string Description { get; internal set; } + + internal DiscordGuildPreview() { } +} diff --git a/DSharpPlus/Entities/Guild/DiscordGuildTemplate.cs b/DSharpPlus/Entities/Guild/DiscordGuildTemplate.cs index 83b8565ca6..f28e3923ac 100644 --- a/DSharpPlus/Entities/Guild/DiscordGuildTemplate.cs +++ b/DSharpPlus/Entities/Guild/DiscordGuildTemplate.cs @@ -1,73 +1,73 @@ -using System; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -public class DiscordGuildTemplate -{ - /// - /// Gets the template code. - /// - [JsonProperty("code", NullValueHandling = NullValueHandling.Ignore)] - public string Code { get; internal set; } - - /// - /// Gets the name of the template. - /// - [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] - public string Name { get; internal set; } - - /// - /// Gets the description of the template. - /// - [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] - public string Description { get; internal set; } - - /// - /// Gets the number of times the template has been used. - /// - [JsonProperty("usage_count", NullValueHandling = NullValueHandling.Ignore)] - public int UsageCount { get; internal set; } - - /// - /// Gets the ID of the creator of the template. - /// - [JsonProperty("creator_id", NullValueHandling = NullValueHandling.Ignore)] - public ulong CreatorId { get; internal set; } - - /// - /// Gets the creator of the template. - /// - [JsonProperty("creator", NullValueHandling = NullValueHandling.Ignore)] - public DiscordUser Creator { get; internal set; } - - /// - /// Date the template was created. - /// - [JsonProperty("created_at", NullValueHandling = NullValueHandling.Ignore)] - public DateTimeOffset CreatedAt { get; internal set; } - - /// - /// Date the template was updated. - /// - [JsonProperty("updated_at", NullValueHandling = NullValueHandling.Ignore)] - public DateTimeOffset UpdatedAt { get; internal set; } - - /// - /// Gets the ID of the source guild. - /// - [JsonProperty("source_guild_id", NullValueHandling = NullValueHandling.Ignore)] - public ulong SourceGuildId { get; internal set; } - - /// - /// Gets the source guild. - /// - [JsonProperty("serialized_source_guild", NullValueHandling = NullValueHandling.Ignore)] - public DiscordGuild SourceGuild { get; internal set; } - - /// - /// Gets whether the template has not synced changes. - /// - [JsonProperty("is_dirty", NullValueHandling = NullValueHandling.Ignore)] - public bool? IsDirty { get; internal set; } -} +using System; +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +public class DiscordGuildTemplate +{ + /// + /// Gets the template code. + /// + [JsonProperty("code", NullValueHandling = NullValueHandling.Ignore)] + public string Code { get; internal set; } + + /// + /// Gets the name of the template. + /// + [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] + public string Name { get; internal set; } + + /// + /// Gets the description of the template. + /// + [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] + public string Description { get; internal set; } + + /// + /// Gets the number of times the template has been used. + /// + [JsonProperty("usage_count", NullValueHandling = NullValueHandling.Ignore)] + public int UsageCount { get; internal set; } + + /// + /// Gets the ID of the creator of the template. + /// + [JsonProperty("creator_id", NullValueHandling = NullValueHandling.Ignore)] + public ulong CreatorId { get; internal set; } + + /// + /// Gets the creator of the template. + /// + [JsonProperty("creator", NullValueHandling = NullValueHandling.Ignore)] + public DiscordUser Creator { get; internal set; } + + /// + /// Date the template was created. + /// + [JsonProperty("created_at", NullValueHandling = NullValueHandling.Ignore)] + public DateTimeOffset CreatedAt { get; internal set; } + + /// + /// Date the template was updated. + /// + [JsonProperty("updated_at", NullValueHandling = NullValueHandling.Ignore)] + public DateTimeOffset UpdatedAt { get; internal set; } + + /// + /// Gets the ID of the source guild. + /// + [JsonProperty("source_guild_id", NullValueHandling = NullValueHandling.Ignore)] + public ulong SourceGuildId { get; internal set; } + + /// + /// Gets the source guild. + /// + [JsonProperty("serialized_source_guild", NullValueHandling = NullValueHandling.Ignore)] + public DiscordGuild SourceGuild { get; internal set; } + + /// + /// Gets whether the template has not synced changes. + /// + [JsonProperty("is_dirty", NullValueHandling = NullValueHandling.Ignore)] + public bool? IsDirty { get; internal set; } +} diff --git a/DSharpPlus/Entities/Guild/DiscordGuildWelcomeScreen.cs b/DSharpPlus/Entities/Guild/DiscordGuildWelcomeScreen.cs index fdace0e6e7..c83340dcc0 100644 --- a/DSharpPlus/Entities/Guild/DiscordGuildWelcomeScreen.cs +++ b/DSharpPlus/Entities/Guild/DiscordGuildWelcomeScreen.cs @@ -1,22 +1,22 @@ -using System.Collections.Generic; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents a discord welcome screen object. -/// -public class DiscordGuildWelcomeScreen -{ - /// - /// Gets the server description shown in the welcome screen. - /// - [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] - public string Description { get; internal set; } - - /// - /// Gets the channels shown in the welcome screen. - /// - [JsonProperty("welcome_channels", NullValueHandling = NullValueHandling.Ignore)] - public IReadOnlyList WelcomeChannels { get; internal set; } -} +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Represents a discord welcome screen object. +/// +public class DiscordGuildWelcomeScreen +{ + /// + /// Gets the server description shown in the welcome screen. + /// + [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] + public string Description { get; internal set; } + + /// + /// Gets the channels shown in the welcome screen. + /// + [JsonProperty("welcome_channels", NullValueHandling = NullValueHandling.Ignore)] + public IReadOnlyList WelcomeChannels { get; internal set; } +} diff --git a/DSharpPlus/Entities/Guild/DiscordGuildWelcomeScreenChannel.cs b/DSharpPlus/Entities/Guild/DiscordGuildWelcomeScreenChannel.cs index 6ecfce1c53..336f632b6d 100644 --- a/DSharpPlus/Entities/Guild/DiscordGuildWelcomeScreenChannel.cs +++ b/DSharpPlus/Entities/Guild/DiscordGuildWelcomeScreenChannel.cs @@ -1,50 +1,50 @@ -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents a channel in a welcome screen -/// -public class DiscordGuildWelcomeScreenChannel -{ - /// - /// Gets the id of the channel. - /// - [JsonProperty("channel_id", NullValueHandling = NullValueHandling.Ignore)] - public ulong ChannelId { get; internal set; } - - /// - /// Gets the description shown for the channel. - /// - [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] - public string Description { get; internal set; } - - /// - /// Gets the emoji id if the emoji is custom, when applicable. - /// - [JsonProperty("emoji_id", NullValueHandling = NullValueHandling.Ignore)] - public ulong? EmojiId { get; internal set; } - - /// - /// Gets the name of the emoji if custom or the unicode character if standard, when applicable. - /// - [JsonProperty("emoji_name", NullValueHandling = NullValueHandling.Ignore)] - public string EmojiName { get; internal set; } - - public DiscordGuildWelcomeScreenChannel(ulong channelId, string description, DiscordEmoji emoji = null) - { - this.ChannelId = channelId; - this.Description = description; - if (emoji != null) - { - if (emoji.Id == 0) - { - this.EmojiName = emoji.Name; - } - else - { - this.EmojiId = emoji.Id; - } - } - } -} +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Represents a channel in a welcome screen +/// +public class DiscordGuildWelcomeScreenChannel +{ + /// + /// Gets the id of the channel. + /// + [JsonProperty("channel_id", NullValueHandling = NullValueHandling.Ignore)] + public ulong ChannelId { get; internal set; } + + /// + /// Gets the description shown for the channel. + /// + [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] + public string Description { get; internal set; } + + /// + /// Gets the emoji id if the emoji is custom, when applicable. + /// + [JsonProperty("emoji_id", NullValueHandling = NullValueHandling.Ignore)] + public ulong? EmojiId { get; internal set; } + + /// + /// Gets the name of the emoji if custom or the unicode character if standard, when applicable. + /// + [JsonProperty("emoji_name", NullValueHandling = NullValueHandling.Ignore)] + public string EmojiName { get; internal set; } + + public DiscordGuildWelcomeScreenChannel(ulong channelId, string description, DiscordEmoji emoji = null) + { + this.ChannelId = channelId; + this.Description = description; + if (emoji != null) + { + if (emoji.Id == 0) + { + this.EmojiName = emoji.Name; + } + else + { + this.EmojiId = emoji.Id; + } + } + } +} diff --git a/DSharpPlus/Entities/Guild/DiscordMember.cs b/DSharpPlus/Entities/Guild/DiscordMember.cs index d5d435a0dc..be66f0b7a7 100644 --- a/DSharpPlus/Entities/Guild/DiscordMember.cs +++ b/DSharpPlus/Entities/Guild/DiscordMember.cs @@ -1,639 +1,639 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Threading.Tasks; -using DSharpPlus.Net; -using DSharpPlus.Net.Abstractions; -using DSharpPlus.Net.Models; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents a Discord guild member. -/// -public class DiscordMember : DiscordUser, IEquatable -{ - internal DiscordMember() { } - - internal DiscordMember(DiscordUser user) - { - this.Discord = user.Discord; - - this.Id = user.Id; - - this.role_ids = []; - } - - internal DiscordMember(TransportMember member) - { - this.Id = member.User.Id; - this.IsDeafened = member.IsDeafened; - this.IsMuted = member.IsMuted; - this.JoinedAt = member.JoinedAt; - this.Nickname = member.Nickname; - this.PremiumSince = member.PremiumSince; - this.IsPending = member.IsPending; - this.avatarHash = member.AvatarHash; - this.role_ids = member.Roles ?? []; - this.CommunicationDisabledUntil = member.CommunicationDisabledUntil; - this.MemberFlags = member.Flags; - } - - /// - /// Gets the member's avatar for the current guild. - /// - [JsonIgnore] - public string? GuildAvatarHash => this.avatarHash; - - /// - /// Gets the members avatar url for the current guild. - /// - [JsonIgnore] - public string? GuildAvatarUrl => string.IsNullOrWhiteSpace(this.GuildAvatarHash) ? null : $"https://cdn.discordapp.com/{Endpoints.GUILDS}/{this.guild_id}/{Endpoints.USERS}/{this.Id}/{Endpoints.AVATARS}/{this.GuildAvatarHash}.{(this.GuildAvatarHash.StartsWith("a_") ? "gif" : "png")}?size=1024"; - - [JsonIgnore] - internal string? avatarHash; - - /// - /// Gets the member's avatar hash as displayed in the current guild. - /// - [JsonIgnore] - public string DisplayAvatarHash => this.GuildAvatarHash ?? this.User.AvatarHash; - - /// - /// Gets the member's avatar url as displayed in the current guild. - /// - [JsonIgnore] - public string DisplayAvatarUrl => this.GuildAvatarUrl ?? this.User.AvatarUrl; - - /// - /// Gets this member's nickname. - /// - [JsonProperty("nick", NullValueHandling = NullValueHandling.Ignore)] - public string Nickname { get; internal set; } - - /// - /// Gets this member's display name. - /// - [JsonIgnore] - public string DisplayName => this.Nickname ?? this.GlobalName ?? this.Username; - - /// - /// How long this member's communication will be suppressed for. - /// - [JsonProperty("communication_disabled_until", NullValueHandling = NullValueHandling.Include)] - public DateTimeOffset? CommunicationDisabledUntil { get; internal set; } - - /// - /// List of role IDs - /// - [JsonIgnore] - internal IReadOnlyList RoleIds => this.role_ids; - - [JsonProperty("roles", NullValueHandling = NullValueHandling.Ignore)] - internal List role_ids; - - /// - /// Gets the list of roles associated with this member. - /// - [JsonIgnore] - public IEnumerable Roles - => this.RoleIds.Select(id => this.Guild.Roles.GetValueOrDefault(id)).Where(x => x != null); - - /// - /// Gets the color associated with this user's top color-giving role, otherwise 0 (no color). - /// - [JsonIgnore] - public DiscordColor Color - { - get - { - DiscordRole? role = this.Roles.OrderByDescending(xr => xr.Position).FirstOrDefault(xr => xr.Color.Value != 0); - return role != null ? role.Color : new DiscordColor(); - } - } - - /// - /// Date the user joined the guild - /// - [JsonProperty("joined_at", NullValueHandling = NullValueHandling.Ignore)] - public DateTimeOffset JoinedAt { get; internal set; } - - /// - /// Date the user started boosting this server - /// - [JsonProperty("premium_since", NullValueHandling = NullValueHandling.Ignore)] - public DateTimeOffset? PremiumSince { get; internal set; } - - /// - /// If the user is deafened - /// - [JsonProperty("is_deafened", NullValueHandling = NullValueHandling.Ignore)] - public bool IsDeafened { get; internal set; } - - /// - /// If the user is muted - /// - [JsonProperty("is_muted", NullValueHandling = NullValueHandling.Ignore)] - public bool IsMuted { get; internal set; } - - /// - /// If the user has passed the guild's Membership Screening requirements - /// - [JsonProperty("pending", NullValueHandling = NullValueHandling.Ignore)] - public bool? IsPending { get; internal set; } - - /// - /// Gets whether or not the member is timed out. - /// - [JsonIgnore] - public bool IsTimedOut => this.CommunicationDisabledUntil.HasValue && this.CommunicationDisabledUntil.Value > DateTimeOffset.UtcNow; - - /// - /// Gets this member's voice state. - /// - [JsonIgnore] - public DiscordVoiceState VoiceState - => this.Discord.Guilds[this.guild_id].VoiceStates.TryGetValue(this.Id, out DiscordVoiceState? voiceState) ? voiceState : null; - - [JsonIgnore] - internal ulong guild_id = 0; - - /// - /// Gets the guild of which this member is a part of. - /// - [JsonIgnore] - public DiscordGuild Guild - => this.Discord.Guilds[this.guild_id]; - - /// - /// Gets whether this member is the Guild owner. - /// - [JsonIgnore] - public bool IsOwner - => this.Id == this.Guild.OwnerId; - - /// - /// Gets the member's position in the role hierarchy, which is the member's highest role's position. Returns for the guild's owner. - /// - [JsonIgnore] - public int Hierarchy - => this.IsOwner ? int.MaxValue : this.RoleIds.Count == 0 ? 0 : this.Roles.Max(x => x.Position); - - /// - /// Gets the permissions for the current member. - /// - [JsonIgnore] - public DiscordPermissions Permissions => GetPermissions(); - - /// - /// Gets the member's guild flags. - /// - [JsonProperty("flags", NullValueHandling = NullValueHandling.Ignore)] - public DiscordMemberFlags? MemberFlags { get; internal set; } - - - #region Overridden user properties - [JsonIgnore] - internal DiscordUser User - => this.Discord.UserCache[this.Id]; - - /// - /// Gets this member's username. - /// - [JsonIgnore] - public override string Username - { - get => this.User.Username; - internal set => this.User.Username = value; - } - - /// - /// Gets the member's 4-digit discriminator. - /// - [JsonIgnore] - public override string Discriminator - { - get => this.User.Discriminator; - internal set => this.User.Discriminator = value; - } - - /// - /// Gets the member's banner hash. - /// - [JsonIgnore] - public override string BannerHash - { - get => this.User.BannerHash; - internal set => this.User.BannerHash = value; - } - - /// - /// The color of this member's banner. Mutually exclusive with . - /// - [JsonIgnore] - public override DiscordColor? BannerColor => this.User.BannerColor; - - /// - /// Gets the member's avatar hash. - /// - [JsonIgnore] - public override string AvatarHash - { - get => this.User.AvatarHash; - internal set => this.User.AvatarHash = value; - } - - /// - /// Gets whether the member is a bot. - /// - [JsonIgnore] - public override bool IsBot - { - get => this.User.IsBot; - internal set => this.User.IsBot = value; - } - - /// - /// Gets the member's email address. - /// This is only present in OAuth. - /// - [JsonIgnore] - public override string Email - { - get => this.User.Email; - internal set => this.User.Email = value; - } - - /// - /// Gets whether the member has multi-factor authentication enabled. - /// - [JsonIgnore] - public override bool? MfaEnabled - { - get => this.User.MfaEnabled; - internal set => this.User.MfaEnabled = value; - } - - /// - /// Gets whether the member is verified. - /// This is only present in OAuth. - /// - [JsonIgnore] - public override bool? Verified - { - get => this.User.Verified; - internal set => this.User.Verified = value; - } - - /// - /// Gets the member's chosen language - /// - [JsonIgnore] - public override string Locale - { - get => this.User.Locale; - internal set => this.User.Locale = value; - } - - /// - /// Gets the user's flags. - /// - [JsonIgnore] - public override DiscordUserFlags? OAuthFlags - { - get => this.User.OAuthFlags; - internal set => this.User.OAuthFlags = value; - } - - /// - /// Gets the member's flags for OAuth. - /// - [JsonIgnore] - public override DiscordUserFlags? Flags - { - get => this.User.Flags; - internal set => this.User.Flags = value; - } - - /// - /// Gets the member's global display name. - /// - [JsonIgnore] - public override string? GlobalName - { - get => this.User.GlobalName; - internal set => this.User.GlobalName = value; - } - #endregion - - /// - /// Times-out a member and restricts their ability to send messages, add reactions, speak in threads, and join voice channels. - /// - /// How long the timeout should last. Set to or a time in the past to remove the timeout. - /// Why this member is being restricted. - public async Task TimeoutAsync(DateTimeOffset? until, string reason = default) - => await this.Discord.ApiClient.ModifyGuildMemberAsync(this.guild_id, this.Id, communicationDisabledUntil: until, reason: reason); - - /// - /// Sets this member's voice mute status. - /// - /// Whether the member is to be muted. - /// Reason for audit logs. - /// - /// Thrown when the client does not have the permission. - /// Thrown when the member does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public async Task SetMuteAsync(bool mute, string reason = null) - => await this.Discord.ApiClient.ModifyGuildMemberAsync(this.guild_id, this.Id, mute: mute, reason: reason); - - /// - /// Sets this member's voice deaf status. - /// - /// Whether the member is to be deafened. - /// Reason for audit logs. - /// - /// Thrown when the client does not have the permission. - /// Thrown when the member does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public async Task SetDeafAsync(bool deaf, string reason = null) - => await this.Discord.ApiClient.ModifyGuildMemberAsync(this.guild_id, this.Id, deaf: deaf, reason: reason); - - /// - /// Modifies this member. - /// - /// Action to perform on this member. - /// - /// 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 async Task ModifyAsync(Action action) - { - MemberEditModel mdl = new(); - action(mdl); - - if (mdl.VoiceChannel.HasValue && mdl.VoiceChannel.Value != null && mdl.VoiceChannel.Value.Type != DiscordChannelType.Voice && mdl.VoiceChannel.Value.Type != DiscordChannelType.Stage) - { - throw new ArgumentException($"{nameof(MemberEditModel)}.{nameof(mdl.VoiceChannel)} must be a voice or stage channel.", nameof(action)); - } - - if (mdl.Nickname.HasValue && this.Discord.CurrentUser.Id == this.Id) - { - await this.Discord.ApiClient.ModifyCurrentMemberAsync(this.Guild.Id, mdl.Nickname.Value, - mdl.AuditLogReason); - - await this.Discord.ApiClient.ModifyGuildMemberAsync(this.Guild.Id, this.Id, Optional.FromNoValue(), - mdl.Roles.IfPresent(e => e.Select(xr => xr.Id)), mdl.Muted, mdl.Deafened, - mdl.VoiceChannel.IfPresent(e => e?.Id), default, mdl.MemberFlags, mdl.AuditLogReason); - } - else - { - await this.Discord.ApiClient.ModifyGuildMemberAsync(this.Guild.Id, this.Id, mdl.Nickname, - mdl.Roles.IfPresent(e => e.Select(xr => xr.Id)), mdl.Muted, mdl.Deafened, - mdl.VoiceChannel.IfPresent(e => e?.Id), mdl.CommunicationDisabledUntil, mdl.MemberFlags, mdl.AuditLogReason); - } - } - - /// - /// Grants a role to the member. - /// - /// Role to grant. - /// Reason for audit logs. - /// - /// Thrown when the client does not have the permission. - /// Thrown when the member does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public async Task GrantRoleAsync(DiscordRole role, string reason = null) - => await this.Discord.ApiClient.AddGuildMemberRoleAsync(this.Guild.Id, this.Id, role.Id, reason); - - /// - /// Revokes a role from a member. - /// - /// Role to revoke. - /// Reason for audit logs. - /// - /// Thrown when the client does not have the permission. - /// Thrown when the member does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public async Task RevokeRoleAsync(DiscordRole role, string reason = null) - => await this.Discord.ApiClient.RemoveGuildMemberRoleAsync(this.Guild.Id, this.Id, role.Id, reason); - - /// - /// Sets the member's roles to ones specified. - /// - /// Roles to set. - /// Reason for audit logs. - /// - /// Thrown when the client does not have the permission. - /// Thrown when the member does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - /// Thrown when attempting to add a managed role. - public async Task ReplaceRolesAsync(IEnumerable roles, string reason = null) - { - if (roles.Where(x => x.IsManaged).Any()) - { - throw new InvalidOperationException("Cannot assign managed roles."); - } - IEnumerable managedRoles = this.Roles.Where(x => x.IsManaged); - - IEnumerable newRoles = managedRoles.Concat(roles); - - await this.Discord.ApiClient.ModifyGuildMemberAsync(this.Guild.Id, this.Id, default, - new Optional>(newRoles.Select(xr => xr.Id)), reason: reason); - } - - /// - /// Bans a this member from their guild. - /// - /// The duration in which discord should delete messages from the banned user. - /// Reason for audit logs. - /// - /// Thrown when the client does not have the permission. - /// Thrown when the member does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public Task BanAsync(TimeSpan deleteMessageDuration = default, string reason = null) - => this.Guild.BanMemberAsync(this, deleteMessageDuration, reason); - - /// 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 UnbanAsync(string reason = null) => this.Guild.UnbanMemberAsync(this, reason); - - /// - /// Kicks this member from their guild. - /// - /// Reason for audit logs. - /// - /// [alias="KickAsync"] - /// 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 async Task RemoveAsync(string reason = null) - => await this.Discord.ApiClient.RemoveGuildMemberAsync(this.guild_id, this.Id, reason); - - /// - /// Moves this member to the specified voice 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 PlaceInAsync(DiscordChannel channel) - => channel.PlaceMemberAsync(this); - - /// - /// Updates the member's suppress state in a stage channel. - /// - /// The channel the member is currently in. - /// Toggles the member's suppress state. - /// Thrown when the channel in not a voice channel. - public async Task UpdateVoiceStateAsync(DiscordChannel channel, bool? suppress) - { - if (channel.Type != DiscordChannelType.Stage) - { - throw new ArgumentException("Voice state can only be updated in a stage channel."); - } - - await this.Discord.ApiClient.UpdateUserVoiceStateAsync(this.Guild.Id, this.Id, channel.Id, suppress); - } - - /// - /// Calculates permissions in a given channel for this member. - /// - /// Channel to calculate permissions for. - /// Calculated permissions for this member in the channel. - public DiscordPermissions PermissionsIn(DiscordChannel channel) - => channel.PermissionsFor(this); - - /// - /// Constructs the url for a guild member's avatar, defaulting to the user's avatar if none is set. - /// - /// The image format of the avatar to get. - /// The maximum size of the avatar. Must be a power of two, minimum 16, maximum 4096. - /// The URL of the user's avatar. - public string GetGuildAvatarUrl(MediaFormat imageFormat, ushort imageSize = 1024) - { - // Run this if statement before any others to prevent running the if statements twice. - if (string.IsNullOrWhiteSpace(this.GuildAvatarHash)) - { - return GetAvatarUrl(imageFormat, imageSize); - } - - if (imageFormat == MediaFormat.Unknown) - { - throw new ArgumentException("You must specify valid image format.", nameof(imageFormat)); - } - - // Makes sure the image size is in between Discord's allowed range. - if (imageSize is < 16 or > 4096) - { - throw new ArgumentOutOfRangeException(nameof(imageSize), "Image Size is not in between 16 and 4096: "); - } - - // Checks to see if the image size is not a power of two. - if (!(imageSize is not 0 && (imageSize & (imageSize - 1)) is 0)) - { - throw new ArgumentOutOfRangeException(nameof(imageSize), "Image size is not a power of two: "); - } - - // Get the string variants of the method parameters to use in the urls. - string stringImageFormat = imageFormat switch - { - MediaFormat.Gif => "gif", - MediaFormat.Jpeg => "jpg", - MediaFormat.Png => "png", - MediaFormat.WebP => "webp", - MediaFormat.Auto => !string.IsNullOrWhiteSpace(this.GuildAvatarHash) ? (this.GuildAvatarHash.StartsWith("a_") ? "gif" : "png") : "png", - _ => throw new ArgumentOutOfRangeException(nameof(imageFormat)), - }; - string stringImageSize = imageSize.ToString(CultureInfo.InvariantCulture); - - return $"https://cdn.discordapp.com/{Endpoints.GUILDS}/{this.guild_id}/{Endpoints.USERS}/{this.Id}/{Endpoints.AVATARS}/{this.GuildAvatarHash}.{stringImageFormat}?size={stringImageSize}"; - } - - /// - /// Returns a string representation of this member. - /// - /// String representation of this member. - public override string ToString() => $"Member {this.Id}; {this.Username}#{this.Discriminator} ({this.DisplayName})"; - - /// - /// Gets the hash code for this . - /// - /// The hash code for this . - public override int GetHashCode() - { - int hash = 13; - - hash = (hash * 7) + this.Id.GetHashCode(); - hash = (hash * 7) + this.guild_id.GetHashCode(); - - return hash; - } - - /// - /// 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) => Equals(obj as DiscordMember); - - /// - /// Checks whether this is equal to another . - /// - /// to compare to. - /// Whether the is equal to this . - public bool Equals(DiscordMember? other) => base.Equals(other) && this.guild_id == other?.guild_id; - - /// - /// Gets whether the two objects are equal. - /// - /// First member to compare. - /// Second member to compare. - /// Whether the two members are equal. - public static bool operator ==(DiscordMember obj, DiscordMember other) => obj?.Equals(other) ?? other is null; - - /// - /// Gets whether the two objects are not equal. - /// - /// First member to compare. - /// Second member to compare. - /// Whether the two members are not equal. - public static bool operator !=(DiscordMember obj, DiscordMember other) => !(obj == other); - - /// - /// Get's the current member's roles based on the sum of the permissions of their given roles. - /// - private DiscordPermissions GetPermissions() - { - if (this.Guild.OwnerId == this.Id) - { - return DiscordPermissions.All; - } - - DiscordPermissions perms; - - // assign @everyone permissions - DiscordRole everyoneRole = this.Guild.EveryoneRole; - perms = everyoneRole.Permissions; - - // assign permissions from member's roles (in order) - perms |= this.Roles.Aggregate(DiscordPermissions.None, (c, role) => c | role.Permissions); - - // Administrator grants all permissions and cannot be overridden - return perms.HasPermission(DiscordPermission.Administrator) ? DiscordPermissions.All : perms; - } -} +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; +using DSharpPlus.Net; +using DSharpPlus.Net.Abstractions; +using DSharpPlus.Net.Models; +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Represents a Discord guild member. +/// +public class DiscordMember : DiscordUser, IEquatable +{ + internal DiscordMember() { } + + internal DiscordMember(DiscordUser user) + { + this.Discord = user.Discord; + + this.Id = user.Id; + + this.role_ids = []; + } + + internal DiscordMember(TransportMember member) + { + this.Id = member.User.Id; + this.IsDeafened = member.IsDeafened; + this.IsMuted = member.IsMuted; + this.JoinedAt = member.JoinedAt; + this.Nickname = member.Nickname; + this.PremiumSince = member.PremiumSince; + this.IsPending = member.IsPending; + this.avatarHash = member.AvatarHash; + this.role_ids = member.Roles ?? []; + this.CommunicationDisabledUntil = member.CommunicationDisabledUntil; + this.MemberFlags = member.Flags; + } + + /// + /// Gets the member's avatar for the current guild. + /// + [JsonIgnore] + public string? GuildAvatarHash => this.avatarHash; + + /// + /// Gets the members avatar url for the current guild. + /// + [JsonIgnore] + public string? GuildAvatarUrl => string.IsNullOrWhiteSpace(this.GuildAvatarHash) ? null : $"https://cdn.discordapp.com/{Endpoints.GUILDS}/{this.guild_id}/{Endpoints.USERS}/{this.Id}/{Endpoints.AVATARS}/{this.GuildAvatarHash}.{(this.GuildAvatarHash.StartsWith("a_") ? "gif" : "png")}?size=1024"; + + [JsonIgnore] + internal string? avatarHash; + + /// + /// Gets the member's avatar hash as displayed in the current guild. + /// + [JsonIgnore] + public string DisplayAvatarHash => this.GuildAvatarHash ?? this.User.AvatarHash; + + /// + /// Gets the member's avatar url as displayed in the current guild. + /// + [JsonIgnore] + public string DisplayAvatarUrl => this.GuildAvatarUrl ?? this.User.AvatarUrl; + + /// + /// Gets this member's nickname. + /// + [JsonProperty("nick", NullValueHandling = NullValueHandling.Ignore)] + public string Nickname { get; internal set; } + + /// + /// Gets this member's display name. + /// + [JsonIgnore] + public string DisplayName => this.Nickname ?? this.GlobalName ?? this.Username; + + /// + /// How long this member's communication will be suppressed for. + /// + [JsonProperty("communication_disabled_until", NullValueHandling = NullValueHandling.Include)] + public DateTimeOffset? CommunicationDisabledUntil { get; internal set; } + + /// + /// List of role IDs + /// + [JsonIgnore] + internal IReadOnlyList RoleIds => this.role_ids; + + [JsonProperty("roles", NullValueHandling = NullValueHandling.Ignore)] + internal List role_ids; + + /// + /// Gets the list of roles associated with this member. + /// + [JsonIgnore] + public IEnumerable Roles + => this.RoleIds.Select(id => this.Guild.Roles.GetValueOrDefault(id)).Where(x => x != null); + + /// + /// Gets the color associated with this user's top color-giving role, otherwise 0 (no color). + /// + [JsonIgnore] + public DiscordColor Color + { + get + { + DiscordRole? role = this.Roles.OrderByDescending(xr => xr.Position).FirstOrDefault(xr => xr.Color.Value != 0); + return role != null ? role.Color : new DiscordColor(); + } + } + + /// + /// Date the user joined the guild + /// + [JsonProperty("joined_at", NullValueHandling = NullValueHandling.Ignore)] + public DateTimeOffset JoinedAt { get; internal set; } + + /// + /// Date the user started boosting this server + /// + [JsonProperty("premium_since", NullValueHandling = NullValueHandling.Ignore)] + public DateTimeOffset? PremiumSince { get; internal set; } + + /// + /// If the user is deafened + /// + [JsonProperty("is_deafened", NullValueHandling = NullValueHandling.Ignore)] + public bool IsDeafened { get; internal set; } + + /// + /// If the user is muted + /// + [JsonProperty("is_muted", NullValueHandling = NullValueHandling.Ignore)] + public bool IsMuted { get; internal set; } + + /// + /// If the user has passed the guild's Membership Screening requirements + /// + [JsonProperty("pending", NullValueHandling = NullValueHandling.Ignore)] + public bool? IsPending { get; internal set; } + + /// + /// Gets whether or not the member is timed out. + /// + [JsonIgnore] + public bool IsTimedOut => this.CommunicationDisabledUntil.HasValue && this.CommunicationDisabledUntil.Value > DateTimeOffset.UtcNow; + + /// + /// Gets this member's voice state. + /// + [JsonIgnore] + public DiscordVoiceState VoiceState + => this.Discord.Guilds[this.guild_id].VoiceStates.TryGetValue(this.Id, out DiscordVoiceState? voiceState) ? voiceState : null; + + [JsonIgnore] + internal ulong guild_id = 0; + + /// + /// Gets the guild of which this member is a part of. + /// + [JsonIgnore] + public DiscordGuild Guild + => this.Discord.Guilds[this.guild_id]; + + /// + /// Gets whether this member is the Guild owner. + /// + [JsonIgnore] + public bool IsOwner + => this.Id == this.Guild.OwnerId; + + /// + /// Gets the member's position in the role hierarchy, which is the member's highest role's position. Returns for the guild's owner. + /// + [JsonIgnore] + public int Hierarchy + => this.IsOwner ? int.MaxValue : this.RoleIds.Count == 0 ? 0 : this.Roles.Max(x => x.Position); + + /// + /// Gets the permissions for the current member. + /// + [JsonIgnore] + public DiscordPermissions Permissions => GetPermissions(); + + /// + /// Gets the member's guild flags. + /// + [JsonProperty("flags", NullValueHandling = NullValueHandling.Ignore)] + public DiscordMemberFlags? MemberFlags { get; internal set; } + + + #region Overridden user properties + [JsonIgnore] + internal DiscordUser User + => this.Discord.UserCache[this.Id]; + + /// + /// Gets this member's username. + /// + [JsonIgnore] + public override string Username + { + get => this.User.Username; + internal set => this.User.Username = value; + } + + /// + /// Gets the member's 4-digit discriminator. + /// + [JsonIgnore] + public override string Discriminator + { + get => this.User.Discriminator; + internal set => this.User.Discriminator = value; + } + + /// + /// Gets the member's banner hash. + /// + [JsonIgnore] + public override string BannerHash + { + get => this.User.BannerHash; + internal set => this.User.BannerHash = value; + } + + /// + /// The color of this member's banner. Mutually exclusive with . + /// + [JsonIgnore] + public override DiscordColor? BannerColor => this.User.BannerColor; + + /// + /// Gets the member's avatar hash. + /// + [JsonIgnore] + public override string AvatarHash + { + get => this.User.AvatarHash; + internal set => this.User.AvatarHash = value; + } + + /// + /// Gets whether the member is a bot. + /// + [JsonIgnore] + public override bool IsBot + { + get => this.User.IsBot; + internal set => this.User.IsBot = value; + } + + /// + /// Gets the member's email address. + /// This is only present in OAuth. + /// + [JsonIgnore] + public override string Email + { + get => this.User.Email; + internal set => this.User.Email = value; + } + + /// + /// Gets whether the member has multi-factor authentication enabled. + /// + [JsonIgnore] + public override bool? MfaEnabled + { + get => this.User.MfaEnabled; + internal set => this.User.MfaEnabled = value; + } + + /// + /// Gets whether the member is verified. + /// This is only present in OAuth. + /// + [JsonIgnore] + public override bool? Verified + { + get => this.User.Verified; + internal set => this.User.Verified = value; + } + + /// + /// Gets the member's chosen language + /// + [JsonIgnore] + public override string Locale + { + get => this.User.Locale; + internal set => this.User.Locale = value; + } + + /// + /// Gets the user's flags. + /// + [JsonIgnore] + public override DiscordUserFlags? OAuthFlags + { + get => this.User.OAuthFlags; + internal set => this.User.OAuthFlags = value; + } + + /// + /// Gets the member's flags for OAuth. + /// + [JsonIgnore] + public override DiscordUserFlags? Flags + { + get => this.User.Flags; + internal set => this.User.Flags = value; + } + + /// + /// Gets the member's global display name. + /// + [JsonIgnore] + public override string? GlobalName + { + get => this.User.GlobalName; + internal set => this.User.GlobalName = value; + } + #endregion + + /// + /// Times-out a member and restricts their ability to send messages, add reactions, speak in threads, and join voice channels. + /// + /// How long the timeout should last. Set to or a time in the past to remove the timeout. + /// Why this member is being restricted. + public async Task TimeoutAsync(DateTimeOffset? until, string reason = default) + => await this.Discord.ApiClient.ModifyGuildMemberAsync(this.guild_id, this.Id, communicationDisabledUntil: until, reason: reason); + + /// + /// Sets this member's voice mute status. + /// + /// Whether the member is to be muted. + /// Reason for audit logs. + /// + /// Thrown when the client does not have the permission. + /// Thrown when the member does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task SetMuteAsync(bool mute, string reason = null) + => await this.Discord.ApiClient.ModifyGuildMemberAsync(this.guild_id, this.Id, mute: mute, reason: reason); + + /// + /// Sets this member's voice deaf status. + /// + /// Whether the member is to be deafened. + /// Reason for audit logs. + /// + /// Thrown when the client does not have the permission. + /// Thrown when the member does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task SetDeafAsync(bool deaf, string reason = null) + => await this.Discord.ApiClient.ModifyGuildMemberAsync(this.guild_id, this.Id, deaf: deaf, reason: reason); + + /// + /// Modifies this member. + /// + /// Action to perform on this member. + /// + /// 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 async Task ModifyAsync(Action action) + { + MemberEditModel mdl = new(); + action(mdl); + + if (mdl.VoiceChannel.HasValue && mdl.VoiceChannel.Value != null && mdl.VoiceChannel.Value.Type != DiscordChannelType.Voice && mdl.VoiceChannel.Value.Type != DiscordChannelType.Stage) + { + throw new ArgumentException($"{nameof(MemberEditModel)}.{nameof(mdl.VoiceChannel)} must be a voice or stage channel.", nameof(action)); + } + + if (mdl.Nickname.HasValue && this.Discord.CurrentUser.Id == this.Id) + { + await this.Discord.ApiClient.ModifyCurrentMemberAsync(this.Guild.Id, mdl.Nickname.Value, + mdl.AuditLogReason); + + await this.Discord.ApiClient.ModifyGuildMemberAsync(this.Guild.Id, this.Id, Optional.FromNoValue(), + mdl.Roles.IfPresent(e => e.Select(xr => xr.Id)), mdl.Muted, mdl.Deafened, + mdl.VoiceChannel.IfPresent(e => e?.Id), default, mdl.MemberFlags, mdl.AuditLogReason); + } + else + { + await this.Discord.ApiClient.ModifyGuildMemberAsync(this.Guild.Id, this.Id, mdl.Nickname, + mdl.Roles.IfPresent(e => e.Select(xr => xr.Id)), mdl.Muted, mdl.Deafened, + mdl.VoiceChannel.IfPresent(e => e?.Id), mdl.CommunicationDisabledUntil, mdl.MemberFlags, mdl.AuditLogReason); + } + } + + /// + /// Grants a role to the member. + /// + /// Role to grant. + /// Reason for audit logs. + /// + /// Thrown when the client does not have the permission. + /// Thrown when the member does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task GrantRoleAsync(DiscordRole role, string reason = null) + => await this.Discord.ApiClient.AddGuildMemberRoleAsync(this.Guild.Id, this.Id, role.Id, reason); + + /// + /// Revokes a role from a member. + /// + /// Role to revoke. + /// Reason for audit logs. + /// + /// Thrown when the client does not have the permission. + /// Thrown when the member does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task RevokeRoleAsync(DiscordRole role, string reason = null) + => await this.Discord.ApiClient.RemoveGuildMemberRoleAsync(this.Guild.Id, this.Id, role.Id, reason); + + /// + /// Sets the member's roles to ones specified. + /// + /// Roles to set. + /// Reason for audit logs. + /// + /// Thrown when the client does not have the permission. + /// Thrown when the member does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + /// Thrown when attempting to add a managed role. + public async Task ReplaceRolesAsync(IEnumerable roles, string reason = null) + { + if (roles.Where(x => x.IsManaged).Any()) + { + throw new InvalidOperationException("Cannot assign managed roles."); + } + IEnumerable managedRoles = this.Roles.Where(x => x.IsManaged); + + IEnumerable newRoles = managedRoles.Concat(roles); + + await this.Discord.ApiClient.ModifyGuildMemberAsync(this.Guild.Id, this.Id, default, + new Optional>(newRoles.Select(xr => xr.Id)), reason: reason); + } + + /// + /// Bans a this member from their guild. + /// + /// The duration in which discord should delete messages from the banned user. + /// Reason for audit logs. + /// + /// Thrown when the client does not have the permission. + /// Thrown when the member does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public Task BanAsync(TimeSpan deleteMessageDuration = default, string reason = null) + => this.Guild.BanMemberAsync(this, deleteMessageDuration, reason); + + /// 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 UnbanAsync(string reason = null) => this.Guild.UnbanMemberAsync(this, reason); + + /// + /// Kicks this member from their guild. + /// + /// Reason for audit logs. + /// + /// [alias="KickAsync"] + /// 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 async Task RemoveAsync(string reason = null) + => await this.Discord.ApiClient.RemoveGuildMemberAsync(this.guild_id, this.Id, reason); + + /// + /// Moves this member to the specified voice 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 PlaceInAsync(DiscordChannel channel) + => channel.PlaceMemberAsync(this); + + /// + /// Updates the member's suppress state in a stage channel. + /// + /// The channel the member is currently in. + /// Toggles the member's suppress state. + /// Thrown when the channel in not a voice channel. + public async Task UpdateVoiceStateAsync(DiscordChannel channel, bool? suppress) + { + if (channel.Type != DiscordChannelType.Stage) + { + throw new ArgumentException("Voice state can only be updated in a stage channel."); + } + + await this.Discord.ApiClient.UpdateUserVoiceStateAsync(this.Guild.Id, this.Id, channel.Id, suppress); + } + + /// + /// Calculates permissions in a given channel for this member. + /// + /// Channel to calculate permissions for. + /// Calculated permissions for this member in the channel. + public DiscordPermissions PermissionsIn(DiscordChannel channel) + => channel.PermissionsFor(this); + + /// + /// Constructs the url for a guild member's avatar, defaulting to the user's avatar if none is set. + /// + /// The image format of the avatar to get. + /// The maximum size of the avatar. Must be a power of two, minimum 16, maximum 4096. + /// The URL of the user's avatar. + public string GetGuildAvatarUrl(MediaFormat imageFormat, ushort imageSize = 1024) + { + // Run this if statement before any others to prevent running the if statements twice. + if (string.IsNullOrWhiteSpace(this.GuildAvatarHash)) + { + return GetAvatarUrl(imageFormat, imageSize); + } + + if (imageFormat == MediaFormat.Unknown) + { + throw new ArgumentException("You must specify valid image format.", nameof(imageFormat)); + } + + // Makes sure the image size is in between Discord's allowed range. + if (imageSize is < 16 or > 4096) + { + throw new ArgumentOutOfRangeException(nameof(imageSize), "Image Size is not in between 16 and 4096: "); + } + + // Checks to see if the image size is not a power of two. + if (!(imageSize is not 0 && (imageSize & (imageSize - 1)) is 0)) + { + throw new ArgumentOutOfRangeException(nameof(imageSize), "Image size is not a power of two: "); + } + + // Get the string variants of the method parameters to use in the urls. + string stringImageFormat = imageFormat switch + { + MediaFormat.Gif => "gif", + MediaFormat.Jpeg => "jpg", + MediaFormat.Png => "png", + MediaFormat.WebP => "webp", + MediaFormat.Auto => !string.IsNullOrWhiteSpace(this.GuildAvatarHash) ? (this.GuildAvatarHash.StartsWith("a_") ? "gif" : "png") : "png", + _ => throw new ArgumentOutOfRangeException(nameof(imageFormat)), + }; + string stringImageSize = imageSize.ToString(CultureInfo.InvariantCulture); + + return $"https://cdn.discordapp.com/{Endpoints.GUILDS}/{this.guild_id}/{Endpoints.USERS}/{this.Id}/{Endpoints.AVATARS}/{this.GuildAvatarHash}.{stringImageFormat}?size={stringImageSize}"; + } + + /// + /// Returns a string representation of this member. + /// + /// String representation of this member. + public override string ToString() => $"Member {this.Id}; {this.Username}#{this.Discriminator} ({this.DisplayName})"; + + /// + /// Gets the hash code for this . + /// + /// The hash code for this . + public override int GetHashCode() + { + int hash = 13; + + hash = (hash * 7) + this.Id.GetHashCode(); + hash = (hash * 7) + this.guild_id.GetHashCode(); + + return hash; + } + + /// + /// 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) => Equals(obj as DiscordMember); + + /// + /// Checks whether this is equal to another . + /// + /// to compare to. + /// Whether the is equal to this . + public bool Equals(DiscordMember? other) => base.Equals(other) && this.guild_id == other?.guild_id; + + /// + /// Gets whether the two objects are equal. + /// + /// First member to compare. + /// Second member to compare. + /// Whether the two members are equal. + public static bool operator ==(DiscordMember obj, DiscordMember other) => obj?.Equals(other) ?? other is null; + + /// + /// Gets whether the two objects are not equal. + /// + /// First member to compare. + /// Second member to compare. + /// Whether the two members are not equal. + public static bool operator !=(DiscordMember obj, DiscordMember other) => !(obj == other); + + /// + /// Get's the current member's roles based on the sum of the permissions of their given roles. + /// + private DiscordPermissions GetPermissions() + { + if (this.Guild.OwnerId == this.Id) + { + return DiscordPermissions.All; + } + + DiscordPermissions perms; + + // assign @everyone permissions + DiscordRole everyoneRole = this.Guild.EveryoneRole; + perms = everyoneRole.Permissions; + + // assign permissions from member's roles (in order) + perms |= this.Roles.Aggregate(DiscordPermissions.None, (c, role) => c | role.Permissions); + + // Administrator grants all permissions and cannot be overridden + return perms.HasPermission(DiscordPermission.Administrator) ? DiscordPermissions.All : perms; + } +} diff --git a/DSharpPlus/Entities/Guild/DiscordMembershipScreeningFieldType.cs b/DSharpPlus/Entities/Guild/DiscordMembershipScreeningFieldType.cs index 3927b0bdb7..c330b6a058 100644 --- a/DSharpPlus/Entities/Guild/DiscordMembershipScreeningFieldType.cs +++ b/DSharpPlus/Entities/Guild/DiscordMembershipScreeningFieldType.cs @@ -1,18 +1,18 @@ -using System.Runtime.Serialization; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; - -namespace DSharpPlus.Entities; - -/// -/// Represents a membership screening field type -/// -[JsonConverter(typeof(StringEnumConverter))] -public enum DiscordMembershipScreeningFieldType -{ - /// - /// Specifies the server rules - /// - [EnumMember(Value = "TERMS")] - Terms -} +using System.Runtime.Serialization; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +namespace DSharpPlus.Entities; + +/// +/// Represents a membership screening field type +/// +[JsonConverter(typeof(StringEnumConverter))] +public enum DiscordMembershipScreeningFieldType +{ + /// + /// Specifies the server rules + /// + [EnumMember(Value = "TERMS")] + Terms +} diff --git a/DSharpPlus/Entities/Guild/DiscordNsfwLevel.cs b/DSharpPlus/Entities/Guild/DiscordNsfwLevel.cs index 49a4134b42..80e5cdc5f2 100644 --- a/DSharpPlus/Entities/Guild/DiscordNsfwLevel.cs +++ b/DSharpPlus/Entities/Guild/DiscordNsfwLevel.cs @@ -1,29 +1,29 @@ -namespace DSharpPlus.Entities; - - -/// -/// Represents a server's content level. -/// -public enum DiscordNsfwLevel -{ - //I'm going off a hunch; default = safe(?) who knows. // - /// - /// Indicates a server's nsfw level is the default. - /// - Default = 0, - - /// - /// Indicates a server's content contains explicit material. - /// - Explicit = 1, - - /// - /// Indicates a server's content is safe for work (SFW). - /// - Safe = 2, - - /// - /// Indicates a server's content is age-gated. - /// - AgeRestricted = 3 -} +namespace DSharpPlus.Entities; + + +/// +/// Represents a server's content level. +/// +public enum DiscordNsfwLevel +{ + //I'm going off a hunch; default = safe(?) who knows. // + /// + /// Indicates a server's nsfw level is the default. + /// + Default = 0, + + /// + /// Indicates a server's content contains explicit material. + /// + Explicit = 1, + + /// + /// Indicates a server's content is safe for work (SFW). + /// + Safe = 2, + + /// + /// Indicates a server's content is age-gated. + /// + AgeRestricted = 3 +} diff --git a/DSharpPlus/Entities/Guild/DiscordPremiumTier.cs b/DSharpPlus/Entities/Guild/DiscordPremiumTier.cs index 43c038b317..45f4e947f0 100644 --- a/DSharpPlus/Entities/Guild/DiscordPremiumTier.cs +++ b/DSharpPlus/Entities/Guild/DiscordPremiumTier.cs @@ -1,33 +1,33 @@ -namespace DSharpPlus.Entities; - - -/// -/// Represents a server's premium tier. -/// -public enum DiscordPremiumTier : int -{ - /// - /// Indicates that this server was not boosted. - /// - None = 0, - - /// - /// Indicates that this server was boosted two times. - /// - Tier_1 = 1, - - /// - /// Indicates that this server was boosted seven times. - /// - Tier_2 = 2, - - /// - /// Indicates that this server was boosted fourteen times. - /// - Tier_3 = 3, - - /// - /// Indicates an unknown premium tier. - /// - Unknown = int.MaxValue -} +namespace DSharpPlus.Entities; + + +/// +/// Represents a server's premium tier. +/// +public enum DiscordPremiumTier : int +{ + /// + /// Indicates that this server was not boosted. + /// + None = 0, + + /// + /// Indicates that this server was boosted two times. + /// + Tier_1 = 1, + + /// + /// Indicates that this server was boosted seven times. + /// + Tier_2 = 2, + + /// + /// Indicates that this server was boosted fourteen times. + /// + Tier_3 = 3, + + /// + /// Indicates an unknown premium tier. + /// + Unknown = int.MaxValue +} diff --git a/DSharpPlus/Entities/Guild/DiscordRole.cs b/DSharpPlus/Entities/Guild/DiscordRole.cs index 56eacbeda1..5e4a1f98ce 100644 --- a/DSharpPlus/Entities/Guild/DiscordRole.cs +++ b/DSharpPlus/Entities/Guild/DiscordRole.cs @@ -1,226 +1,226 @@ -using System; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using DSharpPlus.Net.Abstractions; -using DSharpPlus.Net.Models; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents a discord role, to which users can be assigned. -/// -public class DiscordRole : SnowflakeObject, IEquatable -{ - /// - /// Gets the name of this role. - /// - [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] - public string Name { get; internal set; } - - /// - /// Gets the color of this role. - /// - [JsonIgnore] - public DiscordColor Color - => new(this.color); - - [JsonProperty("color", NullValueHandling = NullValueHandling.Ignore)] - internal int color; - - /// - /// Gets whether this role is hoisted. - /// - [JsonProperty("hoist", NullValueHandling = NullValueHandling.Ignore)] - public bool IsHoisted { get; internal set; } - - /// - /// The url for this role's icon, if set. - /// - public string IconUrl => this.IconHash != null ? $"https://cdn.discordapp.com/role-icons/{this.Id}/{this.IconHash}.png" : null; - - /// - /// The hash of this role's icon, if any. - /// - [JsonProperty("icon")] - public string IconHash { get; internal set; } - - /// - /// The emoji associated with this role's icon, if set. - /// - public DiscordEmoji Emoji => this.emoji != null ? DiscordEmoji.FromUnicode(this.emoji) : null; - - [JsonProperty("unicode_emoji")] - internal string emoji; - - /// - /// Gets the position of this role in the role hierarchy. - /// - [JsonProperty("position", NullValueHandling = NullValueHandling.Ignore)] - public int Position { get; internal set; } - - /// - /// Gets the permissions set for this role. - /// - [JsonProperty("permissions", NullValueHandling = NullValueHandling.Ignore)] - public DiscordPermissions Permissions { get; internal set; } - - /// - /// Gets whether this role is managed by an integration. - /// - [JsonProperty("managed", NullValueHandling = NullValueHandling.Ignore)] - public bool IsManaged { get; internal set; } - - /// - /// Gets whether this role is mentionable. - /// - [JsonProperty("mentionable", NullValueHandling = NullValueHandling.Ignore)] - public bool IsMentionable { get; internal set; } - - /// - /// Gets the tags this role has. - /// - [JsonProperty("tags", NullValueHandling = NullValueHandling.Ignore)] - public DiscordRoleTags Tags { get; internal set; } - - [JsonIgnore] - internal ulong guild_id = 0; - - /// - /// Gets a mention string for this role. If the role is mentionable, this string will mention all the users that belong to this role. - /// - public string Mention - => Formatter.Mention(this); - - #region Methods - /// - /// Modifies this role's position. - /// - /// New position - /// Reason why we moved it - /// - /// Thrown when the client does not have the permission. - /// Thrown when the role does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public async Task ModifyPositionAsync(int position, string reason = null) - { - DiscordRole[] roles = [.. this.Discord.Guilds[this.guild_id].Roles.Values.OrderByDescending(xr => xr.Position)]; - RestGuildRoleReorderPayload[] pmds = new RestGuildRoleReorderPayload[roles.Length]; - for (int i = 0; i < roles.Length; i++) - { - pmds[i] = new RestGuildRoleReorderPayload - { - RoleId = roles[i].Id, - Position = roles[i].Id == this.Id ? position : roles[i].Position <= position ? roles[i].Position - 1 : roles[i].Position - }; - } - - await this.Discord.ApiClient.ModifyGuildRolePositionsAsync(this.guild_id, pmds, reason); - } - - /// - /// Updates this role. - /// - /// New role name - /// New role permissions - /// New role color - /// New role hoist - /// Whether this role is mentionable - /// Reason why we made this change - /// The icon to add to this role - /// The emoji to add to this role. Must be unicode. - /// - /// Thrown when the client does not have the permission. - /// Thrown when the role does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public async Task ModifyAsync(string name = null, DiscordPermissions? permissions = null, DiscordColor? color = null, bool? hoist = null, bool? mentionable = null, string reason = null, Stream icon = null, DiscordEmoji emoji = null) - => await this.Discord.ApiClient.ModifyGuildRoleAsync(this.guild_id, this.Id, name, permissions, color?.Value, hoist, mentionable, icon, emoji?.ToString(), reason); - - /// Thrown when the client does not have the permission. - /// Thrown when the role does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public Task ModifyAsync(Action action) - { - RoleEditModel mdl = new(); - action(mdl); - - return ModifyAsync(mdl.Name, mdl.Permissions, mdl.Color, mdl.Hoist, mdl.Mentionable, mdl.AuditLogReason, mdl.Icon, mdl.Emoji); - } - - /// - /// Deletes this role. - /// - /// Reason as to why this role has been deleted. - /// - /// Thrown when the client does not have the permission. - /// Thrown when the role does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public async Task DeleteAsync(string? reason = null) - => await this.Discord.ApiClient.DeleteRoleAsync(this.guild_id, this.Id, reason); - #endregion - - internal DiscordRole() { } - - /// - /// Checks whether this role has specific permissions. - /// - /// Permissions to check for. - /// Whether the permissions are allowed or not. - public DiscordPermissionLevel CheckPermission(DiscordPermissions permissions) - => this.Permissions.HasAllPermissions(permissions) ? DiscordPermissionLevel.Allowed : DiscordPermissionLevel.Unset; - - /// - /// Returns a string representation of this role. - /// - /// String representation of this role. - public override string ToString() => $"Role {this.Id}; {this.Name}"; - - /// - /// Checks whether this is equal to another object. - /// - /// Object to compare to. - /// Whether the object is equal to this . - public override bool Equals(object obj) => Equals(obj as DiscordRole); - - /// - /// Checks whether this is equal to another . - /// - /// to compare to. - /// Whether the is equal to this . - public bool Equals(DiscordRole e) - => e switch - { - null => false, - _ => ReferenceEquals(this, e) || this.Id == e.Id - }; - - /// - /// Gets the hash code for this . - /// - /// The hash code for this . - public override int GetHashCode() => this.Id.GetHashCode(); - - /// - /// Gets whether the two objects are equal. - /// - /// First role to compare. - /// Second role to compare. - /// Whether the two roles are equal. - public static bool operator ==(DiscordRole e1, DiscordRole e2) - => e1 is null == e2 is null - && ((e1 is null && e2 is null) || e1.Id == e2.Id); - - /// - /// Gets whether the two objects are not equal. - /// - /// First role to compare. - /// Second role to compare. - /// Whether the two roles are not equal. - public static bool operator !=(DiscordRole e1, DiscordRole e2) - => !(e1 == e2); -} +using System; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using DSharpPlus.Net.Abstractions; +using DSharpPlus.Net.Models; +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Represents a discord role, to which users can be assigned. +/// +public class DiscordRole : SnowflakeObject, IEquatable +{ + /// + /// Gets the name of this role. + /// + [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] + public string Name { get; internal set; } + + /// + /// Gets the color of this role. + /// + [JsonIgnore] + public DiscordColor Color + => new(this.color); + + [JsonProperty("color", NullValueHandling = NullValueHandling.Ignore)] + internal int color; + + /// + /// Gets whether this role is hoisted. + /// + [JsonProperty("hoist", NullValueHandling = NullValueHandling.Ignore)] + public bool IsHoisted { get; internal set; } + + /// + /// The url for this role's icon, if set. + /// + public string IconUrl => this.IconHash != null ? $"https://cdn.discordapp.com/role-icons/{this.Id}/{this.IconHash}.png" : null; + + /// + /// The hash of this role's icon, if any. + /// + [JsonProperty("icon")] + public string IconHash { get; internal set; } + + /// + /// The emoji associated with this role's icon, if set. + /// + public DiscordEmoji Emoji => this.emoji != null ? DiscordEmoji.FromUnicode(this.emoji) : null; + + [JsonProperty("unicode_emoji")] + internal string emoji; + + /// + /// Gets the position of this role in the role hierarchy. + /// + [JsonProperty("position", NullValueHandling = NullValueHandling.Ignore)] + public int Position { get; internal set; } + + /// + /// Gets the permissions set for this role. + /// + [JsonProperty("permissions", NullValueHandling = NullValueHandling.Ignore)] + public DiscordPermissions Permissions { get; internal set; } + + /// + /// Gets whether this role is managed by an integration. + /// + [JsonProperty("managed", NullValueHandling = NullValueHandling.Ignore)] + public bool IsManaged { get; internal set; } + + /// + /// Gets whether this role is mentionable. + /// + [JsonProperty("mentionable", NullValueHandling = NullValueHandling.Ignore)] + public bool IsMentionable { get; internal set; } + + /// + /// Gets the tags this role has. + /// + [JsonProperty("tags", NullValueHandling = NullValueHandling.Ignore)] + public DiscordRoleTags Tags { get; internal set; } + + [JsonIgnore] + internal ulong guild_id = 0; + + /// + /// Gets a mention string for this role. If the role is mentionable, this string will mention all the users that belong to this role. + /// + public string Mention + => Formatter.Mention(this); + + #region Methods + /// + /// Modifies this role's position. + /// + /// New position + /// Reason why we moved it + /// + /// Thrown when the client does not have the permission. + /// Thrown when the role does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task ModifyPositionAsync(int position, string reason = null) + { + DiscordRole[] roles = [.. this.Discord.Guilds[this.guild_id].Roles.Values.OrderByDescending(xr => xr.Position)]; + RestGuildRoleReorderPayload[] pmds = new RestGuildRoleReorderPayload[roles.Length]; + for (int i = 0; i < roles.Length; i++) + { + pmds[i] = new RestGuildRoleReorderPayload + { + RoleId = roles[i].Id, + Position = roles[i].Id == this.Id ? position : roles[i].Position <= position ? roles[i].Position - 1 : roles[i].Position + }; + } + + await this.Discord.ApiClient.ModifyGuildRolePositionsAsync(this.guild_id, pmds, reason); + } + + /// + /// Updates this role. + /// + /// New role name + /// New role permissions + /// New role color + /// New role hoist + /// Whether this role is mentionable + /// Reason why we made this change + /// The icon to add to this role + /// The emoji to add to this role. Must be unicode. + /// + /// Thrown when the client does not have the permission. + /// Thrown when the role does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task ModifyAsync(string name = null, DiscordPermissions? permissions = null, DiscordColor? color = null, bool? hoist = null, bool? mentionable = null, string reason = null, Stream icon = null, DiscordEmoji emoji = null) + => await this.Discord.ApiClient.ModifyGuildRoleAsync(this.guild_id, this.Id, name, permissions, color?.Value, hoist, mentionable, icon, emoji?.ToString(), reason); + + /// Thrown when the client does not have the permission. + /// Thrown when the role does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public Task ModifyAsync(Action action) + { + RoleEditModel mdl = new(); + action(mdl); + + return ModifyAsync(mdl.Name, mdl.Permissions, mdl.Color, mdl.Hoist, mdl.Mentionable, mdl.AuditLogReason, mdl.Icon, mdl.Emoji); + } + + /// + /// Deletes this role. + /// + /// Reason as to why this role has been deleted. + /// + /// Thrown when the client does not have the permission. + /// Thrown when the role does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task DeleteAsync(string? reason = null) + => await this.Discord.ApiClient.DeleteRoleAsync(this.guild_id, this.Id, reason); + #endregion + + internal DiscordRole() { } + + /// + /// Checks whether this role has specific permissions. + /// + /// Permissions to check for. + /// Whether the permissions are allowed or not. + public DiscordPermissionLevel CheckPermission(DiscordPermissions permissions) + => this.Permissions.HasAllPermissions(permissions) ? DiscordPermissionLevel.Allowed : DiscordPermissionLevel.Unset; + + /// + /// Returns a string representation of this role. + /// + /// String representation of this role. + public override string ToString() => $"Role {this.Id}; {this.Name}"; + + /// + /// Checks whether this is equal to another object. + /// + /// Object to compare to. + /// Whether the object is equal to this . + public override bool Equals(object obj) => Equals(obj as DiscordRole); + + /// + /// Checks whether this is equal to another . + /// + /// to compare to. + /// Whether the is equal to this . + public bool Equals(DiscordRole e) + => e switch + { + null => false, + _ => ReferenceEquals(this, e) || this.Id == e.Id + }; + + /// + /// Gets the hash code for this . + /// + /// The hash code for this . + public override int GetHashCode() => this.Id.GetHashCode(); + + /// + /// Gets whether the two objects are equal. + /// + /// First role to compare. + /// Second role to compare. + /// Whether the two roles are equal. + public static bool operator ==(DiscordRole e1, DiscordRole e2) + => e1 is null == e2 is null + && ((e1 is null && e2 is null) || e1.Id == e2.Id); + + /// + /// Gets whether the two objects are not equal. + /// + /// First role to compare. + /// Second role to compare. + /// Whether the two roles are not equal. + public static bool operator !=(DiscordRole e1, DiscordRole e2) + => !(e1 == e2); +} diff --git a/DSharpPlus/Entities/Guild/DiscordRoleTags.cs b/DSharpPlus/Entities/Guild/DiscordRoleTags.cs index 9a082a3e44..4e0379e0ad 100644 --- a/DSharpPlus/Entities/Guild/DiscordRoleTags.cs +++ b/DSharpPlus/Entities/Guild/DiscordRoleTags.cs @@ -1,32 +1,32 @@ -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents a discord role tags. -/// -public class DiscordRoleTags -{ - /// - /// Gets the id of the bot this role belongs to. - /// - [JsonProperty("bot_id", NullValueHandling = NullValueHandling.Ignore)] - public ulong? BotId { get; internal set; } - - /// - /// Gets the id of the integration this role belongs to. - /// - [JsonProperty("integration_id", NullValueHandling = NullValueHandling.Ignore)] - public ulong? IntegrationId { get; internal set; } - - /// - /// Gets whether this is the guild's premium subscriber role. - /// - [JsonIgnore] - public bool IsPremiumSubscriber - => this.premiumSubscriber.HasValue && this.premiumSubscriber.Value is null; - - [JsonProperty("premium_subscriber", NullValueHandling = NullValueHandling.Include)] - internal Optional premiumSubscriber = false; - -} +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Represents a discord role tags. +/// +public class DiscordRoleTags +{ + /// + /// Gets the id of the bot this role belongs to. + /// + [JsonProperty("bot_id", NullValueHandling = NullValueHandling.Ignore)] + public ulong? BotId { get; internal set; } + + /// + /// Gets the id of the integration this role belongs to. + /// + [JsonProperty("integration_id", NullValueHandling = NullValueHandling.Ignore)] + public ulong? IntegrationId { get; internal set; } + + /// + /// Gets whether this is the guild's premium subscriber role. + /// + [JsonIgnore] + public bool IsPremiumSubscriber + => this.premiumSubscriber.HasValue && this.premiumSubscriber.Value is null; + + [JsonProperty("premium_subscriber", NullValueHandling = NullValueHandling.Include)] + internal Optional premiumSubscriber = false; + +} diff --git a/DSharpPlus/Entities/Guild/ScheduledEvents/DiscordScheduledGuildEvent.cs b/DSharpPlus/Entities/Guild/ScheduledEvents/DiscordScheduledGuildEvent.cs index 179a0432c6..3b9870fe15 100644 --- a/DSharpPlus/Entities/Guild/ScheduledEvents/DiscordScheduledGuildEvent.cs +++ b/DSharpPlus/Entities/Guild/ScheduledEvents/DiscordScheduledGuildEvent.cs @@ -1,114 +1,114 @@ -using System; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// A scheduled event on a guild, which notifies all people that are interested in it. -/// -public sealed class DiscordScheduledGuildEvent : SnowflakeObject -{ - /// - /// The name of the event. - /// - [JsonProperty("name")] - public string Name { get; internal set; } = default!; - - /// - /// The description - /// - [JsonProperty("description")] - public string Description { get; internal set; } = default!; - - /// - /// The time at which this event will begin. - /// - [JsonProperty("scheduled_start_time")] - public DateTimeOffset StartTime { get; internal set; } - - /// - /// The time at which the event will end, or null if it doesn't have an end time. - /// - [JsonProperty("scheduled_end_time")] - public DateTimeOffset? EndTime { get; internal set; } - - /// - /// The guild this event is scheduled for. - /// - [JsonIgnore] - public DiscordGuild Guild => (this.Discord as DiscordClient)!.InternalGetCachedGuild(this.GuildId); - - /// - /// The channel this event is scheduled for, if applicable. - /// - [JsonIgnore] - public DiscordChannel? Channel => this.ChannelId.HasValue ? this.Guild.GetChannel(this.ChannelId.Value) : null; - - /// - /// The id of the channel this event is scheduled in, if any. - /// - [JsonProperty("channel_id")] - public ulong? ChannelId { get; internal set; } - - /// - /// The id of the guild this event is scheduled for. - /// - [JsonProperty("guild_id")] - public ulong GuildId { get; set; } - - /// - /// The user that created this event. - /// - [JsonProperty("creator")] - public DiscordUser? Creator { get; internal set; } - - /// - /// The id of the user that created this event. - /// - [JsonProperty("creator_id")] - public ulong? CreatorId { get; internal set; } - - /// - /// The privacy of this event. - /// - [JsonProperty("privacy_level")] - public DiscordScheduledGuildEventPrivacyLevel PrivacyLevel { get; internal set; } - - /// - /// The current status of this event. - /// - [JsonProperty("status")] - public DiscordScheduledGuildEventStatus Status { get; internal set; } - - /// - /// Metadata associated with this event. - /// - [JsonProperty("entity_metadata")] - public DiscordScheduledGuildEventMetadata? Metadata { get; internal set; } - - /// - /// What type of event this is. - /// - [JsonProperty("entity_type")] - public DiscordScheduledGuildEventType Type { get; internal set; } - - /// - /// How many users are interested in this event. - /// - [JsonProperty("user_count")] - public int? UserCount { get; internal set; } - - /// - /// The cover image hash of this event. - /// - [JsonProperty("image")] - public string? Image { get; internal set; } - - /// - /// The shareable link to this event. - /// - [JsonIgnore] - public string ShareLink => $"https://discord.com/events/{this.GuildId}/{this.Id}"; - - internal DiscordScheduledGuildEvent() { } -} +using System; +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// A scheduled event on a guild, which notifies all people that are interested in it. +/// +public sealed class DiscordScheduledGuildEvent : SnowflakeObject +{ + /// + /// The name of the event. + /// + [JsonProperty("name")] + public string Name { get; internal set; } = default!; + + /// + /// The description + /// + [JsonProperty("description")] + public string Description { get; internal set; } = default!; + + /// + /// The time at which this event will begin. + /// + [JsonProperty("scheduled_start_time")] + public DateTimeOffset StartTime { get; internal set; } + + /// + /// The time at which the event will end, or null if it doesn't have an end time. + /// + [JsonProperty("scheduled_end_time")] + public DateTimeOffset? EndTime { get; internal set; } + + /// + /// The guild this event is scheduled for. + /// + [JsonIgnore] + public DiscordGuild Guild => (this.Discord as DiscordClient)!.InternalGetCachedGuild(this.GuildId); + + /// + /// The channel this event is scheduled for, if applicable. + /// + [JsonIgnore] + public DiscordChannel? Channel => this.ChannelId.HasValue ? this.Guild.GetChannel(this.ChannelId.Value) : null; + + /// + /// The id of the channel this event is scheduled in, if any. + /// + [JsonProperty("channel_id")] + public ulong? ChannelId { get; internal set; } + + /// + /// The id of the guild this event is scheduled for. + /// + [JsonProperty("guild_id")] + public ulong GuildId { get; set; } + + /// + /// The user that created this event. + /// + [JsonProperty("creator")] + public DiscordUser? Creator { get; internal set; } + + /// + /// The id of the user that created this event. + /// + [JsonProperty("creator_id")] + public ulong? CreatorId { get; internal set; } + + /// + /// The privacy of this event. + /// + [JsonProperty("privacy_level")] + public DiscordScheduledGuildEventPrivacyLevel PrivacyLevel { get; internal set; } + + /// + /// The current status of this event. + /// + [JsonProperty("status")] + public DiscordScheduledGuildEventStatus Status { get; internal set; } + + /// + /// Metadata associated with this event. + /// + [JsonProperty("entity_metadata")] + public DiscordScheduledGuildEventMetadata? Metadata { get; internal set; } + + /// + /// What type of event this is. + /// + [JsonProperty("entity_type")] + public DiscordScheduledGuildEventType Type { get; internal set; } + + /// + /// How many users are interested in this event. + /// + [JsonProperty("user_count")] + public int? UserCount { get; internal set; } + + /// + /// The cover image hash of this event. + /// + [JsonProperty("image")] + public string? Image { get; internal set; } + + /// + /// The shareable link to this event. + /// + [JsonIgnore] + public string ShareLink => $"https://discord.com/events/{this.GuildId}/{this.Id}"; + + internal DiscordScheduledGuildEvent() { } +} diff --git a/DSharpPlus/Entities/Guild/ScheduledEvents/DiscordScheduledGuildEventMetadata.cs b/DSharpPlus/Entities/Guild/ScheduledEvents/DiscordScheduledGuildEventMetadata.cs index 8091266cae..9737c4cb1f 100644 --- a/DSharpPlus/Entities/Guild/ScheduledEvents/DiscordScheduledGuildEventMetadata.cs +++ b/DSharpPlus/Entities/Guild/ScheduledEvents/DiscordScheduledGuildEventMetadata.cs @@ -1,19 +1,19 @@ -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Metadata for a . -/// -public sealed class DiscordScheduledGuildEventMetadata -{ - /// - /// If this is an external event, where this event is hosted. - /// - [JsonProperty("location")] - public string Location { get; internal set; } - - internal DiscordScheduledGuildEventMetadata() { } - - public DiscordScheduledGuildEventMetadata(string location) => this.Location = location; -} +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Metadata for a . +/// +public sealed class DiscordScheduledGuildEventMetadata +{ + /// + /// If this is an external event, where this event is hosted. + /// + [JsonProperty("location")] + public string Location { get; internal set; } + + internal DiscordScheduledGuildEventMetadata() { } + + public DiscordScheduledGuildEventMetadata(string location) => this.Location = location; +} diff --git a/DSharpPlus/Entities/Guild/ScheduledEvents/DiscordScheduledGuildEventPrivacyLevel.cs b/DSharpPlus/Entities/Guild/ScheduledEvents/DiscordScheduledGuildEventPrivacyLevel.cs index 9bcb1d4506..2bc7a6cf79 100644 --- a/DSharpPlus/Entities/Guild/ScheduledEvents/DiscordScheduledGuildEventPrivacyLevel.cs +++ b/DSharpPlus/Entities/Guild/ScheduledEvents/DiscordScheduledGuildEventPrivacyLevel.cs @@ -1,17 +1,17 @@ -namespace DSharpPlus.Entities; - - -/// -/// Privacy level for a . -/// -public enum DiscordScheduledGuildEventPrivacyLevel -{ - /// - /// This event is public. - /// - Public = 1, - /// - /// This event is only available to the members of the guild. - /// - GuildOnly = 2, -} +namespace DSharpPlus.Entities; + + +/// +/// Privacy level for a . +/// +public enum DiscordScheduledGuildEventPrivacyLevel +{ + /// + /// This event is public. + /// + Public = 1, + /// + /// This event is only available to the members of the guild. + /// + GuildOnly = 2, +} diff --git a/DSharpPlus/Entities/Guild/ScheduledEvents/DiscordScheduledGuildEventStatus.cs b/DSharpPlus/Entities/Guild/ScheduledEvents/DiscordScheduledGuildEventStatus.cs index 1f7b80b60f..c388057ca6 100644 --- a/DSharpPlus/Entities/Guild/ScheduledEvents/DiscordScheduledGuildEventStatus.cs +++ b/DSharpPlus/Entities/Guild/ScheduledEvents/DiscordScheduledGuildEventStatus.cs @@ -1,27 +1,27 @@ -namespace DSharpPlus.Entities; - - -/// -/// Represents the status of a . -/// -public enum DiscordScheduledGuildEventStatus -{ - /// - /// This event is scheduled. - /// - Scheduled = 1, - /// - /// This event is currently running. - /// - Active = 2, - - /// - /// This event has finished running. - /// - Completed = 3, - - /// - /// This event has been cancelled. - /// - Cancelled = 4 -} +namespace DSharpPlus.Entities; + + +/// +/// Represents the status of a . +/// +public enum DiscordScheduledGuildEventStatus +{ + /// + /// This event is scheduled. + /// + Scheduled = 1, + /// + /// This event is currently running. + /// + Active = 2, + + /// + /// This event has finished running. + /// + Completed = 3, + + /// + /// This event has been cancelled. + /// + Cancelled = 4 +} diff --git a/DSharpPlus/Entities/Guild/ScheduledEvents/DiscordScheduledGuildEventType.cs b/DSharpPlus/Entities/Guild/ScheduledEvents/DiscordScheduledGuildEventType.cs index 7851d867d3..89aa8949e2 100644 --- a/DSharpPlus/Entities/Guild/ScheduledEvents/DiscordScheduledGuildEventType.cs +++ b/DSharpPlus/Entities/Guild/ScheduledEvents/DiscordScheduledGuildEventType.cs @@ -1,22 +1,22 @@ -namespace DSharpPlus.Entities; - - -/// -/// Declares the type of a . -/// -public enum DiscordScheduledGuildEventType -{ - /// - /// The event will be hosted in a stage channel. - /// - StageInstance = 1, - /// - /// The event will be hosted in a voice channel. - /// - VoiceChannel = 2, - - /// - /// The event will be hosted in a custom location. - /// - External = 3 -} +namespace DSharpPlus.Entities; + + +/// +/// Declares the type of a . +/// +public enum DiscordScheduledGuildEventType +{ + /// + /// The event will be hosted in a stage channel. + /// + StageInstance = 1, + /// + /// The event will be hosted in a voice channel. + /// + VoiceChannel = 2, + + /// + /// The event will be hosted in a custom location. + /// + External = 3 +} diff --git a/DSharpPlus/Entities/Guild/Widget/DiscordWidget.cs b/DSharpPlus/Entities/Guild/Widget/DiscordWidget.cs index 7545077d8c..b192d635e8 100644 --- a/DSharpPlus/Entities/Guild/Widget/DiscordWidget.cs +++ b/DSharpPlus/Entities/Guild/Widget/DiscordWidget.cs @@ -1,43 +1,43 @@ -using System.Collections.Generic; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents a Discord guild's widget. -/// -public class DiscordWidget : SnowflakeObject -{ - [JsonIgnore] - public DiscordGuild Guild { get; internal set; } - - /// - /// Gets the guild's name. - /// - [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] - public string Name { get; internal set; } - - /// - /// Gets the guild's invite URL. - /// - [JsonProperty("instant_invite", NullValueHandling = NullValueHandling.Ignore)] - public string InstantInviteUrl { get; internal set; } - - /// - /// Gets the number of online members. - /// - [JsonProperty("presence_count", NullValueHandling = NullValueHandling.Ignore)] - public int PresenceCount { get; internal set; } - - /// - /// Gets a list of online members. - /// - [JsonProperty("members", NullValueHandling = NullValueHandling.Ignore)] - public IReadOnlyList Members { get; internal set; } - - /// - /// Gets a list of widget channels. - /// - [JsonIgnore] - public IReadOnlyList Channels { get; internal set; } -} +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Represents a Discord guild's widget. +/// +public class DiscordWidget : SnowflakeObject +{ + [JsonIgnore] + public DiscordGuild Guild { get; internal set; } + + /// + /// Gets the guild's name. + /// + [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] + public string Name { get; internal set; } + + /// + /// Gets the guild's invite URL. + /// + [JsonProperty("instant_invite", NullValueHandling = NullValueHandling.Ignore)] + public string InstantInviteUrl { get; internal set; } + + /// + /// Gets the number of online members. + /// + [JsonProperty("presence_count", NullValueHandling = NullValueHandling.Ignore)] + public int PresenceCount { get; internal set; } + + /// + /// Gets a list of online members. + /// + [JsonProperty("members", NullValueHandling = NullValueHandling.Ignore)] + public IReadOnlyList Members { get; internal set; } + + /// + /// Gets a list of widget channels. + /// + [JsonIgnore] + public IReadOnlyList Channels { get; internal set; } +} diff --git a/DSharpPlus/Entities/Guild/Widget/DiscordWidgetMember.cs b/DSharpPlus/Entities/Guild/Widget/DiscordWidgetMember.cs index c40dfa3935..fb391203bd 100644 --- a/DSharpPlus/Entities/Guild/Widget/DiscordWidgetMember.cs +++ b/DSharpPlus/Entities/Guild/Widget/DiscordWidgetMember.cs @@ -1,45 +1,45 @@ -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents a member within a Discord guild's widget. -/// -public class DiscordWidgetMember -{ - /// - /// Gets the member's identifier within the widget. - /// - [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)] - public ulong Id { get; internal set; } - - /// - /// Gets the member's username. - /// - [JsonProperty("username", NullValueHandling = NullValueHandling.Ignore)] - public string Username { get; internal set; } - - /// - /// Gets the member's discriminator. - /// - [JsonProperty("discriminator", NullValueHandling = NullValueHandling.Ignore)] - public string Discriminator { get; internal set; } - - /// - /// Gets the member's avatar. - /// - [JsonProperty("avatar", NullValueHandling = NullValueHandling.Ignore)] - public string Avatar { get; internal set; } - - /// - /// Gets the member's online status. - /// - [JsonProperty("status", NullValueHandling = NullValueHandling.Ignore)] - public string Status { get; internal set; } - - /// - /// Gets the member's avatar url. - /// - [JsonProperty("avatar_url", NullValueHandling = NullValueHandling.Ignore)] - public string AvatarUrl { get; internal set; } -} +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Represents a member within a Discord guild's widget. +/// +public class DiscordWidgetMember +{ + /// + /// Gets the member's identifier within the widget. + /// + [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)] + public ulong Id { get; internal set; } + + /// + /// Gets the member's username. + /// + [JsonProperty("username", NullValueHandling = NullValueHandling.Ignore)] + public string Username { get; internal set; } + + /// + /// Gets the member's discriminator. + /// + [JsonProperty("discriminator", NullValueHandling = NullValueHandling.Ignore)] + public string Discriminator { get; internal set; } + + /// + /// Gets the member's avatar. + /// + [JsonProperty("avatar", NullValueHandling = NullValueHandling.Ignore)] + public string Avatar { get; internal set; } + + /// + /// Gets the member's online status. + /// + [JsonProperty("status", NullValueHandling = NullValueHandling.Ignore)] + public string Status { get; internal set; } + + /// + /// Gets the member's avatar url. + /// + [JsonProperty("avatar_url", NullValueHandling = NullValueHandling.Ignore)] + public string AvatarUrl { get; internal set; } +} diff --git a/DSharpPlus/Entities/Guild/Widget/DiscordWidgetSettings.cs b/DSharpPlus/Entities/Guild/Widget/DiscordWidgetSettings.cs index 6bab10bcd8..89c363e989 100644 --- a/DSharpPlus/Entities/Guild/Widget/DiscordWidgetSettings.cs +++ b/DSharpPlus/Entities/Guild/Widget/DiscordWidgetSettings.cs @@ -1,29 +1,29 @@ -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents a Discord guild's widget settings. -/// -public class DiscordWidgetSettings -{ - internal DiscordGuild Guild { get; set; } - - /// - /// Gets the guild's widget channel id. - /// - [JsonProperty("channel_id", NullValueHandling = NullValueHandling.Ignore)] - public ulong ChannelId { get; internal set; } - - /// - /// Gets the guild's widget channel. - /// - public DiscordChannel Channel - => this.Guild?.GetChannel(this.ChannelId); - - /// - /// Gets if the guild's widget is enabled. - /// - [JsonProperty("enabled", NullValueHandling = NullValueHandling.Ignore)] - public bool IsEnabled { get; internal set; } -} +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Represents a Discord guild's widget settings. +/// +public class DiscordWidgetSettings +{ + internal DiscordGuild Guild { get; set; } + + /// + /// Gets the guild's widget channel id. + /// + [JsonProperty("channel_id", NullValueHandling = NullValueHandling.Ignore)] + public ulong ChannelId { get; internal set; } + + /// + /// Gets the guild's widget channel. + /// + public DiscordChannel Channel + => this.Guild?.GetChannel(this.ChannelId); + + /// + /// Gets if the guild's widget is enabled. + /// + [JsonProperty("enabled", NullValueHandling = NullValueHandling.Ignore)] + public bool IsEnabled { get; internal set; } +} diff --git a/DSharpPlus/Entities/Integration/DiscordApplicationIntegrationType.cs b/DSharpPlus/Entities/Integration/DiscordApplicationIntegrationType.cs index 22d850258d..28a8c9ada3 100644 --- a/DSharpPlus/Entities/Integration/DiscordApplicationIntegrationType.cs +++ b/DSharpPlus/Entities/Integration/DiscordApplicationIntegrationType.cs @@ -1,18 +1,18 @@ -namespace DSharpPlus.Entities; - - -/// -/// Represents the type of integration for an application. -/// -public enum DiscordApplicationIntegrationType -{ - /// - /// Represents that the integration can be installed for a guild. - /// - GuildInstall, - - /// - /// Represents that the integration can be installed for a user. - /// - UserInstall, -} +namespace DSharpPlus.Entities; + + +/// +/// Represents the type of integration for an application. +/// +public enum DiscordApplicationIntegrationType +{ + /// + /// Represents that the integration can be installed for a guild. + /// + GuildInstall, + + /// + /// Represents that the integration can be installed for a user. + /// + UserInstall, +} diff --git a/DSharpPlus/Entities/Integration/DiscordIntegration.cs b/DSharpPlus/Entities/Integration/DiscordIntegration.cs index 86106fa81d..3a4247f177 100644 --- a/DSharpPlus/Entities/Integration/DiscordIntegration.cs +++ b/DSharpPlus/Entities/Integration/DiscordIntegration.cs @@ -1,72 +1,72 @@ -using System; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents a Discord integration. These appear on the profile as linked 3rd party accounts. -/// -public class DiscordIntegration : SnowflakeObject -{ - /// - /// Gets the integration name. - /// - [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] - public string Name { get; internal set; } - - /// - /// Gets the integration type. - /// - [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] - public string Type { get; internal set; } - - /// - /// Gets whether this integration is enabled. - /// - [JsonProperty("enabled", NullValueHandling = NullValueHandling.Ignore)] - public bool IsEnabled { get; internal set; } - - /// - /// Gets whether this integration is syncing. - /// - [JsonProperty("syncing", NullValueHandling = NullValueHandling.Ignore)] - public bool IsSyncing { get; internal set; } - - /// - /// Gets ID of the role this integration uses for subscribers. - /// - [JsonProperty("role_id", NullValueHandling = NullValueHandling.Ignore)] - public ulong RoleId { get; internal set; } - - /// - /// Gets the expiration behaviour. - /// - [JsonProperty("expire_behavior", NullValueHandling = NullValueHandling.Ignore)] - public int ExpireBehavior { get; internal set; } - - /// - /// Gets the grace period before expiring subscribers. - /// - [JsonProperty("expire_grace_period", NullValueHandling = NullValueHandling.Ignore)] - public int ExpireGracePeriod { get; internal set; } - - /// - /// Gets the user that owns this integration. - /// - [JsonProperty("user", NullValueHandling = NullValueHandling.Ignore)] - public DiscordUser User { get; internal set; } - - /// - /// Gets the 3rd party service account for this integration. - /// - [JsonProperty("account", NullValueHandling = NullValueHandling.Ignore)] - public DiscordIntegrationAccount Account { get; internal set; } - - /// - /// Gets the date and time this integration was last synced. - /// - [JsonProperty("synced_at", NullValueHandling = NullValueHandling.Ignore)] - public DateTimeOffset SyncedAt { get; internal set; } - - internal DiscordIntegration() { } -} +using System; +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Represents a Discord integration. These appear on the profile as linked 3rd party accounts. +/// +public class DiscordIntegration : SnowflakeObject +{ + /// + /// Gets the integration name. + /// + [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] + public string Name { get; internal set; } + + /// + /// Gets the integration type. + /// + [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] + public string Type { get; internal set; } + + /// + /// Gets whether this integration is enabled. + /// + [JsonProperty("enabled", NullValueHandling = NullValueHandling.Ignore)] + public bool IsEnabled { get; internal set; } + + /// + /// Gets whether this integration is syncing. + /// + [JsonProperty("syncing", NullValueHandling = NullValueHandling.Ignore)] + public bool IsSyncing { get; internal set; } + + /// + /// Gets ID of the role this integration uses for subscribers. + /// + [JsonProperty("role_id", NullValueHandling = NullValueHandling.Ignore)] + public ulong RoleId { get; internal set; } + + /// + /// Gets the expiration behaviour. + /// + [JsonProperty("expire_behavior", NullValueHandling = NullValueHandling.Ignore)] + public int ExpireBehavior { get; internal set; } + + /// + /// Gets the grace period before expiring subscribers. + /// + [JsonProperty("expire_grace_period", NullValueHandling = NullValueHandling.Ignore)] + public int ExpireGracePeriod { get; internal set; } + + /// + /// Gets the user that owns this integration. + /// + [JsonProperty("user", NullValueHandling = NullValueHandling.Ignore)] + public DiscordUser User { get; internal set; } + + /// + /// Gets the 3rd party service account for this integration. + /// + [JsonProperty("account", NullValueHandling = NullValueHandling.Ignore)] + public DiscordIntegrationAccount Account { get; internal set; } + + /// + /// Gets the date and time this integration was last synced. + /// + [JsonProperty("synced_at", NullValueHandling = NullValueHandling.Ignore)] + public DateTimeOffset SyncedAt { get; internal set; } + + internal DiscordIntegration() { } +} diff --git a/DSharpPlus/Entities/Integration/DiscordIntegrationAccount.cs b/DSharpPlus/Entities/Integration/DiscordIntegrationAccount.cs index 84f7943450..dbddb23dd3 100644 --- a/DSharpPlus/Entities/Integration/DiscordIntegrationAccount.cs +++ b/DSharpPlus/Entities/Integration/DiscordIntegrationAccount.cs @@ -1,23 +1,23 @@ -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents a Discord integration account. -/// -public class DiscordIntegrationAccount -{ - /// - /// Gets the ID of the account. - /// - [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)] - public string Id { get; internal set; } - - /// - /// Gets the name of the account. - /// - [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] - public string Name { get; internal set; } - - internal DiscordIntegrationAccount() { } -} +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Represents a Discord integration account. +/// +public class DiscordIntegrationAccount +{ + /// + /// Gets the ID of the account. + /// + [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)] + public string Id { get; internal set; } + + /// + /// Gets the name of the account. + /// + [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] + public string Name { get; internal set; } + + internal DiscordIntegrationAccount() { } +} diff --git a/DSharpPlus/Entities/Integration/DiscordInteractionContextType.cs b/DSharpPlus/Entities/Integration/DiscordInteractionContextType.cs index 54429cf8d0..f26201d150 100644 --- a/DSharpPlus/Entities/Integration/DiscordInteractionContextType.cs +++ b/DSharpPlus/Entities/Integration/DiscordInteractionContextType.cs @@ -1,23 +1,23 @@ -namespace DSharpPlus.Entities; - - -/// -/// Represents the type of context in which an interaction was created. -/// -public enum DiscordInteractionContextType -{ - /// - /// The interaction is in a guild. - /// - Guild, - - /// - /// The interaction is in a DM with the bot associated with the application. (This is to say, your bot.) - /// - BotDM, - - /// - /// The interaction is in a [group] DM. - /// - PrivateChannel, -} +namespace DSharpPlus.Entities; + + +/// +/// Represents the type of context in which an interaction was created. +/// +public enum DiscordInteractionContextType +{ + /// + /// The interaction is in a guild. + /// + Guild, + + /// + /// The interaction is in a DM with the bot associated with the application. (This is to say, your bot.) + /// + BotDM, + + /// + /// The interaction is in a [group] DM. + /// + PrivateChannel, +} diff --git a/DSharpPlus/Entities/Interaction/Application/DiscordApplicationCommand.cs b/DSharpPlus/Entities/Interaction/Application/DiscordApplicationCommand.cs index 00cc30c083..c0a297bfe7 100644 --- a/DSharpPlus/Entities/Interaction/Application/DiscordApplicationCommand.cs +++ b/DSharpPlus/Entities/Interaction/Application/DiscordApplicationCommand.cs @@ -1,227 +1,227 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Globalization; -using System.Linq; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents a command that is registered to an application. -/// -public sealed partial class DiscordApplicationCommand : SnowflakeObject, IEquatable -{ - /// - /// Gets the unique ID of this command's application. - /// - [JsonProperty("application_id")] - public ulong ApplicationId { get; internal set; } - - /// - /// Gets the type of this application command. - /// - [JsonProperty("type")] - public DiscordApplicationCommandType Type { get; internal set; } - - /// - /// Gets the name of this command. - /// - [JsonProperty("name")] - public string Name { get; internal set; } - - /// - /// Gets the description of this command. - /// - [JsonProperty("description")] - public string Description { get; internal set; } - - /// - /// Gets the potential parameters for this command. - /// - [JsonProperty("options", NullValueHandling = NullValueHandling.Ignore)] - public IReadOnlyList Options { get; internal set; } - - /// - /// Gets whether the command is enabled by default when the application is added to a guild. - /// - [JsonProperty("default_permission")] - public bool? DefaultPermission { get; internal set; } - - /// - /// Whether this command can be invoked in DMs. - /// - [JsonProperty("dm_permission")] - public bool? AllowDMUsage { get; internal set; } - - /// - /// What permissions this command requires to be invoked. - /// - [JsonProperty("default_member_permissions")] - public DiscordPermissions? DefaultMemberPermissions { get; internal set; } - - /// - /// Whether this command is age-restricted. - /// - [JsonProperty("nsfw")] - public bool? NSFW { get; internal set; } - - /// - /// Gets the auto-incrementing version number for this command. - /// - [JsonProperty("version")] - public ulong Version { get; internal set; } - - /// - /// Gets the localization dictionary for the field. - /// - [JsonProperty("name_localizations")] - public IReadOnlyDictionary? NameLocalizations { get; internal set; } - - /// - /// Gets the localization dictionary for the field. - /// - [JsonProperty("description_localizations")] - public IReadOnlyDictionary? DescriptionLocalizations { get; internal set; } - - /// - /// Contexts in which this command can be invoked. - /// - [JsonProperty("contexts")] - public IReadOnlyList? Contexts { get; internal set; } - - /// - /// Contexts in which this command can be installed. - /// - [JsonProperty("integration_types")] - public IReadOnlyList? IntegrationTypes { get; internal set; } - - /// - /// Gets the command's mention string. - /// - [JsonIgnore] - public string Mention - => Formatter.Mention(this); - - /// - /// Creates a new instance of a . - /// - /// The name of the command. - /// The description of the command. - /// Optional parameters for this command. - /// Whether the command is enabled by default when the application is added to a guild. - /// The type of the application command - /// Localization dictionary for field. Values follow the same restrictions as . - /// Localization dictionary for field. Values follow the same restrictions as . - /// Whether this command can be invoked in DMs. - /// What permissions this command requires to be invoked. - /// Whether the command is age restricted. - /// The contexts in which the command is allowed to be run in. - /// The installation contexts the command can be installed to. - public DiscordApplicationCommand - ( - string name, - string description, - IEnumerable options = null, - bool? defaultPermission = null, - DiscordApplicationCommandType type = DiscordApplicationCommandType.SlashCommand, - IReadOnlyDictionary name_localizations = null, - IReadOnlyDictionary description_localizations = null, - bool? allowDMUsage = null, - DiscordPermissions? defaultMemberPermissions = null, - bool? nsfw = null, - IReadOnlyList? contexts = null, - IReadOnlyList? integrationTypes = null - ) - { - if (type is DiscordApplicationCommandType.SlashCommand) - { - if (!Utilities.IsValidSlashCommandName(name)) - { - throw new ArgumentException($"Invalid slash command name specified: {name}. It must be below 32 characters and not contain any whitespace.", nameof(name)); - } - - if (name.Any(char.IsUpper)) - { - throw new ArgumentException("Slash command name cannot have any upper case characters.", nameof(name)); - } - - if (description.Length > 100) - { - throw new ArgumentException("Slash command description cannot exceed 100 characters.", nameof(description)); - } - } - else if (type is DiscordApplicationCommandType.UserContextMenu or DiscordApplicationCommandType.MessageContextMenu) - { - if (options?.Any() ?? false) - { - throw new ArgumentException("Context menus do not support options."); - } - } - - ReadOnlyCollection? optionsList = options != null ? new ReadOnlyCollection(options.ToList()) : null; - - this.Type = type; - this.Name = name; - this.Description = description; - this.Options = optionsList; - this.DefaultPermission = defaultPermission; - this.NameLocalizations = name_localizations; - this.DescriptionLocalizations = description_localizations; - this.AllowDMUsage = allowDMUsage; - this.DefaultMemberPermissions = defaultMemberPermissions; - this.NSFW = nsfw; - this.Contexts = contexts; - this.IntegrationTypes = integrationTypes; - } - - /// - /// Creates a mention for a subcommand. - /// - /// The name of the subgroup and/or subcommand. - /// Formatted mention. - public string GetSubcommandMention(params string[] name) => !this.Options.Any(x => x.Name == name[0]) - ? throw new ArgumentException("Specified subgroup/subcommand doesn't exist.") - : $""; - - /// - /// Checks whether this object is equal to another object. - /// - /// The command to compare to. - /// Whether the command is equal to this . - public bool Equals(DiscordApplicationCommand other) - => this.Id == other.Id; - - /// - /// Determines if two objects are equal. - /// - /// The first command object. - /// The second command object. - /// Whether the two objects are equal. - public static bool operator ==(DiscordApplicationCommand e1, DiscordApplicationCommand e2) - => e1.Equals(e2); - - /// - /// Determines if two objects are not equal. - /// - /// The first command object. - /// The second command object. - /// Whether the two objects are not equal. - public static bool operator !=(DiscordApplicationCommand e1, DiscordApplicationCommand e2) - => !(e1 == e2); - - /// - /// Determines if a is equal to the current . - /// - /// The object to compare to. - /// Whether the two objects are not equal. - public override bool Equals(object other) - => other is DiscordApplicationCommand dac && Equals(dac); - - /// - /// Gets the hash code for this . - /// - /// The hash code for this . - public override int GetHashCode() - => this.Id.GetHashCode(); -} +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Globalization; +using System.Linq; +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Represents a command that is registered to an application. +/// +public sealed partial class DiscordApplicationCommand : SnowflakeObject, IEquatable +{ + /// + /// Gets the unique ID of this command's application. + /// + [JsonProperty("application_id")] + public ulong ApplicationId { get; internal set; } + + /// + /// Gets the type of this application command. + /// + [JsonProperty("type")] + public DiscordApplicationCommandType Type { get; internal set; } + + /// + /// Gets the name of this command. + /// + [JsonProperty("name")] + public string Name { get; internal set; } + + /// + /// Gets the description of this command. + /// + [JsonProperty("description")] + public string Description { get; internal set; } + + /// + /// Gets the potential parameters for this command. + /// + [JsonProperty("options", NullValueHandling = NullValueHandling.Ignore)] + public IReadOnlyList Options { get; internal set; } + + /// + /// Gets whether the command is enabled by default when the application is added to a guild. + /// + [JsonProperty("default_permission")] + public bool? DefaultPermission { get; internal set; } + + /// + /// Whether this command can be invoked in DMs. + /// + [JsonProperty("dm_permission")] + public bool? AllowDMUsage { get; internal set; } + + /// + /// What permissions this command requires to be invoked. + /// + [JsonProperty("default_member_permissions")] + public DiscordPermissions? DefaultMemberPermissions { get; internal set; } + + /// + /// Whether this command is age-restricted. + /// + [JsonProperty("nsfw")] + public bool? NSFW { get; internal set; } + + /// + /// Gets the auto-incrementing version number for this command. + /// + [JsonProperty("version")] + public ulong Version { get; internal set; } + + /// + /// Gets the localization dictionary for the field. + /// + [JsonProperty("name_localizations")] + public IReadOnlyDictionary? NameLocalizations { get; internal set; } + + /// + /// Gets the localization dictionary for the field. + /// + [JsonProperty("description_localizations")] + public IReadOnlyDictionary? DescriptionLocalizations { get; internal set; } + + /// + /// Contexts in which this command can be invoked. + /// + [JsonProperty("contexts")] + public IReadOnlyList? Contexts { get; internal set; } + + /// + /// Contexts in which this command can be installed. + /// + [JsonProperty("integration_types")] + public IReadOnlyList? IntegrationTypes { get; internal set; } + + /// + /// Gets the command's mention string. + /// + [JsonIgnore] + public string Mention + => Formatter.Mention(this); + + /// + /// Creates a new instance of a . + /// + /// The name of the command. + /// The description of the command. + /// Optional parameters for this command. + /// Whether the command is enabled by default when the application is added to a guild. + /// The type of the application command + /// Localization dictionary for field. Values follow the same restrictions as . + /// Localization dictionary for field. Values follow the same restrictions as . + /// Whether this command can be invoked in DMs. + /// What permissions this command requires to be invoked. + /// Whether the command is age restricted. + /// The contexts in which the command is allowed to be run in. + /// The installation contexts the command can be installed to. + public DiscordApplicationCommand + ( + string name, + string description, + IEnumerable options = null, + bool? defaultPermission = null, + DiscordApplicationCommandType type = DiscordApplicationCommandType.SlashCommand, + IReadOnlyDictionary name_localizations = null, + IReadOnlyDictionary description_localizations = null, + bool? allowDMUsage = null, + DiscordPermissions? defaultMemberPermissions = null, + bool? nsfw = null, + IReadOnlyList? contexts = null, + IReadOnlyList? integrationTypes = null + ) + { + if (type is DiscordApplicationCommandType.SlashCommand) + { + if (!Utilities.IsValidSlashCommandName(name)) + { + throw new ArgumentException($"Invalid slash command name specified: {name}. It must be below 32 characters and not contain any whitespace.", nameof(name)); + } + + if (name.Any(char.IsUpper)) + { + throw new ArgumentException("Slash command name cannot have any upper case characters.", nameof(name)); + } + + if (description.Length > 100) + { + throw new ArgumentException("Slash command description cannot exceed 100 characters.", nameof(description)); + } + } + else if (type is DiscordApplicationCommandType.UserContextMenu or DiscordApplicationCommandType.MessageContextMenu) + { + if (options?.Any() ?? false) + { + throw new ArgumentException("Context menus do not support options."); + } + } + + ReadOnlyCollection? optionsList = options != null ? new ReadOnlyCollection(options.ToList()) : null; + + this.Type = type; + this.Name = name; + this.Description = description; + this.Options = optionsList; + this.DefaultPermission = defaultPermission; + this.NameLocalizations = name_localizations; + this.DescriptionLocalizations = description_localizations; + this.AllowDMUsage = allowDMUsage; + this.DefaultMemberPermissions = defaultMemberPermissions; + this.NSFW = nsfw; + this.Contexts = contexts; + this.IntegrationTypes = integrationTypes; + } + + /// + /// Creates a mention for a subcommand. + /// + /// The name of the subgroup and/or subcommand. + /// Formatted mention. + public string GetSubcommandMention(params string[] name) => !this.Options.Any(x => x.Name == name[0]) + ? throw new ArgumentException("Specified subgroup/subcommand doesn't exist.") + : $""; + + /// + /// Checks whether this object is equal to another object. + /// + /// The command to compare to. + /// Whether the command is equal to this . + public bool Equals(DiscordApplicationCommand other) + => this.Id == other.Id; + + /// + /// Determines if two objects are equal. + /// + /// The first command object. + /// The second command object. + /// Whether the two objects are equal. + public static bool operator ==(DiscordApplicationCommand e1, DiscordApplicationCommand e2) + => e1.Equals(e2); + + /// + /// Determines if two objects are not equal. + /// + /// The first command object. + /// The second command object. + /// Whether the two objects are not equal. + public static bool operator !=(DiscordApplicationCommand e1, DiscordApplicationCommand e2) + => !(e1 == e2); + + /// + /// Determines if a is equal to the current . + /// + /// The object to compare to. + /// Whether the two objects are not equal. + public override bool Equals(object other) + => other is DiscordApplicationCommand dac && Equals(dac); + + /// + /// Gets the hash code for this . + /// + /// The hash code for this . + public override int GetHashCode() + => this.Id.GetHashCode(); +} diff --git a/DSharpPlus/Entities/Interaction/Application/DiscordApplicationCommandOption.cs b/DSharpPlus/Entities/Interaction/Application/DiscordApplicationCommandOption.cs index 14b383b4b4..934a012be8 100644 --- a/DSharpPlus/Entities/Interaction/Application/DiscordApplicationCommandOption.cs +++ b/DSharpPlus/Entities/Interaction/Application/DiscordApplicationCommandOption.cs @@ -1,156 +1,156 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents a parameter for a . -/// -public sealed class DiscordApplicationCommandOption -{ - /// - /// Gets the type of this command parameter. - /// - [JsonProperty("type")] - public DiscordApplicationCommandOptionType Type { get; internal set; } - - /// - /// Gets the name of this command parameter. - /// - [JsonProperty("name")] - public string Name { get; internal set; } - - /// - /// Gets the description of this command parameter. - /// - [JsonProperty("description")] - public string Description { get; internal set; } - - /// - /// Gets whether this option will auto-complete. - /// - [JsonProperty("autocomplete")] - public bool? AutoComplete { get; internal set; } - - /// - /// Gets whether this command parameter is required. - /// - [JsonProperty("required", NullValueHandling = NullValueHandling.Ignore)] - public bool? Required { get; internal set; } - - /// - /// Gets the optional choices for this command parameter. Not applicable for auto-complete options. - /// - [JsonProperty("choices", NullValueHandling = NullValueHandling.Ignore)] - public IReadOnlyList Choices { get; internal set; } - - /// - /// Gets the optional subcommand parameters for this parameter. - /// - [JsonProperty("options", NullValueHandling = NullValueHandling.Ignore)] - public IReadOnlyList Options { get; internal set; } - - /// - /// Gets the channel types this command parameter is restricted to, if of type .. - /// - [JsonProperty("channel_types", NullValueHandling = NullValueHandling.Ignore)] - public IReadOnlyList ChannelTypes { get; internal set; } - - /// - /// Gets the minimum value for this slash command parameter. - /// - [JsonProperty("min_value", NullValueHandling = NullValueHandling.Ignore)] - public object MinValue { get; internal set; } - - /// - /// Gets the maximum value for this slash command parameter. - /// - [JsonProperty("max_value", NullValueHandling = NullValueHandling.Ignore)] - public object MaxValue { get; internal set; } - - /// - /// Gets the minimum allowed length for this slash command parameter. - /// - [JsonProperty("min_length", NullValueHandling = NullValueHandling.Ignore)] - public int? MinLength { get; internal set; } - - /// - /// Gets the maximum allowed length for this slash command parameter. - /// - [JsonProperty("max_length", NullValueHandling = NullValueHandling.Ignore)] - public int? MaxLength { get; internal set; } - - /// - /// Localized names for this option. - /// - [JsonProperty("name_localizations", NullValueHandling = NullValueHandling.Include)] - public IReadOnlyDictionary NameLocalizations { get; internal set; } - - /// - /// Localized descriptions for this option. - /// - [JsonProperty("description_localizations", NullValueHandling = NullValueHandling.Include)] - public IReadOnlyDictionary DescriptionLocalizations { get; internal set; } - - /// - /// Creates a new instance of a . - /// - /// The name of this parameter. - /// The description of the parameter. - /// The type of this parameter. - /// Whether the parameter is required. - /// The optional choice selection for this parameter. - /// The optional subcommands for this parameter. - /// The channel types to be restricted to for this parameter, if of type . - /// Whether this parameter is autocomplete. If true, must not be provided. - /// The minimum value for this parameter. Only valid for types or . - /// The maximum value for this parameter. Only valid for types or . - /// Name localizations for this parameter. - /// Description localizations for this parameter. - /// The minimum allowed length for this parameter. Only valid for type . - /// The maximum allowed length for this parameter. Only valid for type . - public DiscordApplicationCommandOption(string name, string description, DiscordApplicationCommandOptionType type, bool? required = null, IEnumerable choices = null, IEnumerable options = null, IEnumerable channelTypes = null, bool? autocomplete = null, object minValue = null, object maxValue = null, IReadOnlyDictionary name_localizations = null, IReadOnlyDictionary description_localizations = null, int? minLength = null, int? maxLength = null) - { - if (!Utilities.IsValidSlashCommandName(name)) - { - throw new ArgumentException($"Invalid slash command option name specified: {name}. It must be below 32 characters and not contain any whitespace.", nameof(name)); - } - - if (name.Any(char.IsUpper)) - { - throw new ArgumentException("Slash command option name cannot have any upper case characters.", nameof(name)); - } - - if (description.Length > 100) - { - throw new ArgumentException("Slash command option description cannot exceed 100 characters.", nameof(description)); - } - - if ((autocomplete ?? false) && (choices?.Any() ?? false)) - { - throw new InvalidOperationException("Auto-complete slash command options cannot provide choices."); - } - - ReadOnlyCollection? choiceList = choices != null ? new ReadOnlyCollection(choices.ToList()) : null; - ReadOnlyCollection? optionList = options != null ? new ReadOnlyCollection(options.ToList()) : null; - ReadOnlyCollection? channelTypeList = channelTypes != null ? new ReadOnlyCollection(channelTypes.ToList()) : null; - - this.Name = name; - this.Description = description; - this.Type = type; - this.AutoComplete = autocomplete; - this.Required = required; - this.Choices = choiceList; - this.Options = optionList; - this.ChannelTypes = channelTypeList; - this.MinValue = minValue; - this.MaxValue = maxValue; - this.MinLength = minLength; - this.MaxLength = maxLength; - this.NameLocalizations = name_localizations; - this.DescriptionLocalizations = description_localizations; - } -} +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Represents a parameter for a . +/// +public sealed class DiscordApplicationCommandOption +{ + /// + /// Gets the type of this command parameter. + /// + [JsonProperty("type")] + public DiscordApplicationCommandOptionType Type { get; internal set; } + + /// + /// Gets the name of this command parameter. + /// + [JsonProperty("name")] + public string Name { get; internal set; } + + /// + /// Gets the description of this command parameter. + /// + [JsonProperty("description")] + public string Description { get; internal set; } + + /// + /// Gets whether this option will auto-complete. + /// + [JsonProperty("autocomplete")] + public bool? AutoComplete { get; internal set; } + + /// + /// Gets whether this command parameter is required. + /// + [JsonProperty("required", NullValueHandling = NullValueHandling.Ignore)] + public bool? Required { get; internal set; } + + /// + /// Gets the optional choices for this command parameter. Not applicable for auto-complete options. + /// + [JsonProperty("choices", NullValueHandling = NullValueHandling.Ignore)] + public IReadOnlyList Choices { get; internal set; } + + /// + /// Gets the optional subcommand parameters for this parameter. + /// + [JsonProperty("options", NullValueHandling = NullValueHandling.Ignore)] + public IReadOnlyList Options { get; internal set; } + + /// + /// Gets the channel types this command parameter is restricted to, if of type .. + /// + [JsonProperty("channel_types", NullValueHandling = NullValueHandling.Ignore)] + public IReadOnlyList ChannelTypes { get; internal set; } + + /// + /// Gets the minimum value for this slash command parameter. + /// + [JsonProperty("min_value", NullValueHandling = NullValueHandling.Ignore)] + public object MinValue { get; internal set; } + + /// + /// Gets the maximum value for this slash command parameter. + /// + [JsonProperty("max_value", NullValueHandling = NullValueHandling.Ignore)] + public object MaxValue { get; internal set; } + + /// + /// Gets the minimum allowed length for this slash command parameter. + /// + [JsonProperty("min_length", NullValueHandling = NullValueHandling.Ignore)] + public int? MinLength { get; internal set; } + + /// + /// Gets the maximum allowed length for this slash command parameter. + /// + [JsonProperty("max_length", NullValueHandling = NullValueHandling.Ignore)] + public int? MaxLength { get; internal set; } + + /// + /// Localized names for this option. + /// + [JsonProperty("name_localizations", NullValueHandling = NullValueHandling.Include)] + public IReadOnlyDictionary NameLocalizations { get; internal set; } + + /// + /// Localized descriptions for this option. + /// + [JsonProperty("description_localizations", NullValueHandling = NullValueHandling.Include)] + public IReadOnlyDictionary DescriptionLocalizations { get; internal set; } + + /// + /// Creates a new instance of a . + /// + /// The name of this parameter. + /// The description of the parameter. + /// The type of this parameter. + /// Whether the parameter is required. + /// The optional choice selection for this parameter. + /// The optional subcommands for this parameter. + /// The channel types to be restricted to for this parameter, if of type . + /// Whether this parameter is autocomplete. If true, must not be provided. + /// The minimum value for this parameter. Only valid for types or . + /// The maximum value for this parameter. Only valid for types or . + /// Name localizations for this parameter. + /// Description localizations for this parameter. + /// The minimum allowed length for this parameter. Only valid for type . + /// The maximum allowed length for this parameter. Only valid for type . + public DiscordApplicationCommandOption(string name, string description, DiscordApplicationCommandOptionType type, bool? required = null, IEnumerable choices = null, IEnumerable options = null, IEnumerable channelTypes = null, bool? autocomplete = null, object minValue = null, object maxValue = null, IReadOnlyDictionary name_localizations = null, IReadOnlyDictionary description_localizations = null, int? minLength = null, int? maxLength = null) + { + if (!Utilities.IsValidSlashCommandName(name)) + { + throw new ArgumentException($"Invalid slash command option name specified: {name}. It must be below 32 characters and not contain any whitespace.", nameof(name)); + } + + if (name.Any(char.IsUpper)) + { + throw new ArgumentException("Slash command option name cannot have any upper case characters.", nameof(name)); + } + + if (description.Length > 100) + { + throw new ArgumentException("Slash command option description cannot exceed 100 characters.", nameof(description)); + } + + if ((autocomplete ?? false) && (choices?.Any() ?? false)) + { + throw new InvalidOperationException("Auto-complete slash command options cannot provide choices."); + } + + ReadOnlyCollection? choiceList = choices != null ? new ReadOnlyCollection(choices.ToList()) : null; + ReadOnlyCollection? optionList = options != null ? new ReadOnlyCollection(options.ToList()) : null; + ReadOnlyCollection? channelTypeList = channelTypes != null ? new ReadOnlyCollection(channelTypes.ToList()) : null; + + this.Name = name; + this.Description = description; + this.Type = type; + this.AutoComplete = autocomplete; + this.Required = required; + this.Choices = choiceList; + this.Options = optionList; + this.ChannelTypes = channelTypeList; + this.MinValue = minValue; + this.MaxValue = maxValue; + this.MinLength = minLength; + this.MaxLength = maxLength; + this.NameLocalizations = name_localizations; + this.DescriptionLocalizations = description_localizations; + } +} diff --git a/DSharpPlus/Entities/Interaction/Application/DiscordApplicationCommandOptionChoice.cs b/DSharpPlus/Entities/Interaction/Application/DiscordApplicationCommandOptionChoice.cs index 000873c849..cabbd6f6d6 100644 --- a/DSharpPlus/Entities/Interaction/Application/DiscordApplicationCommandOptionChoice.cs +++ b/DSharpPlus/Entities/Interaction/Application/DiscordApplicationCommandOptionChoice.cs @@ -1,133 +1,133 @@ -using System; -using System.Collections.Generic; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents a choice for a parameter. -/// -public sealed class DiscordApplicationCommandOptionChoice -{ - /// - /// Gets the name of this choice. - /// - [JsonProperty("name")] - public string Name { get; set; } - - /// - /// Gets the value of this choice. This will either be a type of / , or . - /// - [JsonProperty("value")] - public object Value { get; set; } - - /// - /// Gets the localized names for this choice. - /// - /// - /// The keys must be appropriate locales as documented by Discord: - /// . - /// - [JsonProperty("name_localizations")] - public IReadOnlyDictionary? NameLocalizations { get; set; } - - [JsonConstructor] - private DiscordApplicationCommandOptionChoice() - { - this.Name = null!; - this.Value = null!; - this.NameLocalizations = null; - } - - /// - /// Creates a new instance of a . - /// - private DiscordApplicationCommandOptionChoice( - string name, - IReadOnlyDictionary? nameLocalizations - ) - { - if (name.Length is < 1 or > 100) - { - throw new ArgumentException( - "Application command choice name cannot be empty or exceed 100 characters.", - nameof(name) - ); - } - - if (nameLocalizations is not null) - { - foreach ((string locale, string localized) in nameLocalizations) - { - if (localized.Length is < 1 or > 100) - { - throw new ArgumentException( - $"Localized application command choice name for locale {locale} cannot be empty or exceed 100 characters. Value: '{localized}'", - nameof(nameLocalizations) - ); - } - } - } - - this.Name = name; - this.Value = null!; - this.NameLocalizations = nameLocalizations; - } - - /// - /// The name of this choice. - /// The value of this choice. - /// - /// Localized names for this choice. The keys must be appropriate locales as documented by Discord: - /// . - /// - public DiscordApplicationCommandOptionChoice( - string name, - string value, - IReadOnlyDictionary? nameLocalizations = null - ) - : this(name, nameLocalizations) - { - if (value.Length > 100) - { - throw new ArgumentException( - "Application command choice value cannot exceed 100 characters.", - nameof(value) - ); - } - - this.Value = value; - } - - /// - public DiscordApplicationCommandOptionChoice( - string name, - int value, - IReadOnlyDictionary? nameLocalizations = null - ) - : this(name, nameLocalizations) => this.Value = value; - - /// - public DiscordApplicationCommandOptionChoice( - string name, - long value, - IReadOnlyDictionary? nameLocalizations = null - ) - : this(name, nameLocalizations) => this.Value = value; - - /// - public DiscordApplicationCommandOptionChoice( - string name, - double value, - IReadOnlyDictionary? nameLocalizations = null - ) - : this(name, nameLocalizations) => this.Value = value; - - /// - public DiscordApplicationCommandOptionChoice( - string name, - float value, - IReadOnlyDictionary? nameLocalizations = null - ) - : this(name, nameLocalizations) => this.Value = value; -} +using System; +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Represents a choice for a parameter. +/// +public sealed class DiscordApplicationCommandOptionChoice +{ + /// + /// Gets the name of this choice. + /// + [JsonProperty("name")] + public string Name { get; set; } + + /// + /// Gets the value of this choice. This will either be a type of / , or . + /// + [JsonProperty("value")] + public object Value { get; set; } + + /// + /// Gets the localized names for this choice. + /// + /// + /// The keys must be appropriate locales as documented by Discord: + /// . + /// + [JsonProperty("name_localizations")] + public IReadOnlyDictionary? NameLocalizations { get; set; } + + [JsonConstructor] + private DiscordApplicationCommandOptionChoice() + { + this.Name = null!; + this.Value = null!; + this.NameLocalizations = null; + } + + /// + /// Creates a new instance of a . + /// + private DiscordApplicationCommandOptionChoice( + string name, + IReadOnlyDictionary? nameLocalizations + ) + { + if (name.Length is < 1 or > 100) + { + throw new ArgumentException( + "Application command choice name cannot be empty or exceed 100 characters.", + nameof(name) + ); + } + + if (nameLocalizations is not null) + { + foreach ((string locale, string localized) in nameLocalizations) + { + if (localized.Length is < 1 or > 100) + { + throw new ArgumentException( + $"Localized application command choice name for locale {locale} cannot be empty or exceed 100 characters. Value: '{localized}'", + nameof(nameLocalizations) + ); + } + } + } + + this.Name = name; + this.Value = null!; + this.NameLocalizations = nameLocalizations; + } + + /// + /// The name of this choice. + /// The value of this choice. + /// + /// Localized names for this choice. The keys must be appropriate locales as documented by Discord: + /// . + /// + public DiscordApplicationCommandOptionChoice( + string name, + string value, + IReadOnlyDictionary? nameLocalizations = null + ) + : this(name, nameLocalizations) + { + if (value.Length > 100) + { + throw new ArgumentException( + "Application command choice value cannot exceed 100 characters.", + nameof(value) + ); + } + + this.Value = value; + } + + /// + public DiscordApplicationCommandOptionChoice( + string name, + int value, + IReadOnlyDictionary? nameLocalizations = null + ) + : this(name, nameLocalizations) => this.Value = value; + + /// + public DiscordApplicationCommandOptionChoice( + string name, + long value, + IReadOnlyDictionary? nameLocalizations = null + ) + : this(name, nameLocalizations) => this.Value = value; + + /// + public DiscordApplicationCommandOptionChoice( + string name, + double value, + IReadOnlyDictionary? nameLocalizations = null + ) + : this(name, nameLocalizations) => this.Value = value; + + /// + public DiscordApplicationCommandOptionChoice( + string name, + float value, + IReadOnlyDictionary? nameLocalizations = null + ) + : this(name, nameLocalizations) => this.Value = value; +} diff --git a/DSharpPlus/Entities/Interaction/Application/DiscordApplicationCommandOptionType.cs b/DSharpPlus/Entities/Interaction/Application/DiscordApplicationCommandOptionType.cs index b060ac518e..f0f0277c40 100644 --- a/DSharpPlus/Entities/Interaction/Application/DiscordApplicationCommandOptionType.cs +++ b/DSharpPlus/Entities/Interaction/Application/DiscordApplicationCommandOptionType.cs @@ -1,63 +1,63 @@ -namespace DSharpPlus.Entities; - - -/// -/// Represents the type of parameter when invoking an interaction. -/// -public enum DiscordApplicationCommandOptionType -{ - /// - /// Whether this parameter is another subcommand. - /// - SubCommand = 1, - - /// - /// Whether this parameter is apart of a subcommand group. - /// - SubCommandGroup, - - /// - /// Whether this parameter is a string. - /// - String, - - /// - /// Whether this parameter is an integer. - /// - Integer, - - /// - /// Whether this parameter is a boolean. - /// - Boolean, - - /// - /// Whether this parameter is a Discord user. - /// - User, - - /// - /// Whether this parameter is a Discord channel. - /// - Channel, - - /// - /// Whether this parameter is a Discord role. - /// - Role, - - /// - /// Whether this parameter is a mentionable (role or user). - /// - Mentionable, - - /// - /// Whether this parameter is a double. - /// - Number, - - /// - /// Whether this parameter is a Discord attachment. - /// - Attachment -} +namespace DSharpPlus.Entities; + + +/// +/// Represents the type of parameter when invoking an interaction. +/// +public enum DiscordApplicationCommandOptionType +{ + /// + /// Whether this parameter is another subcommand. + /// + SubCommand = 1, + + /// + /// Whether this parameter is apart of a subcommand group. + /// + SubCommandGroup, + + /// + /// Whether this parameter is a string. + /// + String, + + /// + /// Whether this parameter is an integer. + /// + Integer, + + /// + /// Whether this parameter is a boolean. + /// + Boolean, + + /// + /// Whether this parameter is a Discord user. + /// + User, + + /// + /// Whether this parameter is a Discord channel. + /// + Channel, + + /// + /// Whether this parameter is a Discord role. + /// + Role, + + /// + /// Whether this parameter is a mentionable (role or user). + /// + Mentionable, + + /// + /// Whether this parameter is a double. + /// + Number, + + /// + /// Whether this parameter is a Discord attachment. + /// + Attachment +} diff --git a/DSharpPlus/Entities/Interaction/Application/DiscordApplicationCommandPermissionType.cs b/DSharpPlus/Entities/Interaction/Application/DiscordApplicationCommandPermissionType.cs index 847f8464ae..08860c4f93 100644 --- a/DSharpPlus/Entities/Interaction/Application/DiscordApplicationCommandPermissionType.cs +++ b/DSharpPlus/Entities/Interaction/Application/DiscordApplicationCommandPermissionType.cs @@ -1,8 +1,8 @@ -namespace DSharpPlus.Entities; - - -public enum DiscordApplicationCommandPermissionType -{ - Role = 1, - User -} +namespace DSharpPlus.Entities; + + +public enum DiscordApplicationCommandPermissionType +{ + Role = 1, + User +} diff --git a/DSharpPlus/Entities/Interaction/Application/DiscordApplicationCommandType.cs b/DSharpPlus/Entities/Interaction/Application/DiscordApplicationCommandType.cs index 6b5bfe813d..ed57563735 100644 --- a/DSharpPlus/Entities/Interaction/Application/DiscordApplicationCommandType.cs +++ b/DSharpPlus/Entities/Interaction/Application/DiscordApplicationCommandType.cs @@ -1,28 +1,28 @@ -namespace DSharpPlus.Entities; - - -/// -/// Represents the type of a . -/// -public enum DiscordApplicationCommandType -{ - /// - /// This command is registered as a slash-command, aka "Chat Input". - /// - SlashCommand = 1, - - /// - /// This command is registered as a user context menu, and is applicable when interacting a user. - /// - UserContextMenu = 2, - - /// - /// This command is registered as a message context menu, and is applicable when interacting with a message. - /// - MessageContextMenu = 3, - - /// - /// This command serves as the primary entry point into the app's activity. - /// - ActivityEntryPoint = 4, -} +namespace DSharpPlus.Entities; + + +/// +/// Represents the type of a . +/// +public enum DiscordApplicationCommandType +{ + /// + /// This command is registered as a slash-command, aka "Chat Input". + /// + SlashCommand = 1, + + /// + /// This command is registered as a user context menu, and is applicable when interacting a user. + /// + UserContextMenu = 2, + + /// + /// This command is registered as a message context menu, and is applicable when interacting with a message. + /// + MessageContextMenu = 3, + + /// + /// This command serves as the primary entry point into the app's activity. + /// + ActivityEntryPoint = 4, +} diff --git a/DSharpPlus/Entities/Interaction/Application/DiscordAutoCompleteChoice.cs b/DSharpPlus/Entities/Interaction/Application/DiscordAutoCompleteChoice.cs index f0bf2877c1..d20e6736ab 100644 --- a/DSharpPlus/Entities/Interaction/Application/DiscordAutoCompleteChoice.cs +++ b/DSharpPlus/Entities/Interaction/Application/DiscordAutoCompleteChoice.cs @@ -1,69 +1,69 @@ -using System; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents an option for a user to select for auto-completion. -/// -public sealed class DiscordAutoCompleteChoice -{ - /// - /// Gets the name of this option which will be presented to the user. - /// - [JsonProperty("name")] - public string Name { get; internal set; } - - /// - /// Gets the value of this option. This may be a string or an integer. - /// - [JsonProperty("value")] - public object? Value { get; internal set; } - - [JsonConstructor] - private DiscordAutoCompleteChoice() => this.Name = null!; - - /// - /// Creates a new instance of . - /// - private DiscordAutoCompleteChoice(string name) - { - if (name.Length is < 1 or > 100) - { - throw new ArgumentOutOfRangeException(nameof(name), "Application command choice name cannot be empty or exceed 100 characters."); - } - - this.Name = name; - } - - /// - /// The name of this option, which will be presented to the user. - /// The value of this option. - public DiscordAutoCompleteChoice(string name, object? value) : this(name) - { - this.Value = value switch - { - string s => CheckStringValue(s), - byte b => b, - sbyte sb => sb, - short s => s, - ushort us => us, - int i => this.Value = i, - uint ui => this.Value = ui, - long l => this.Value = l, - ulong ul => this.Value = ul, - double d => this.Value = d, - float f => this.Value = f, - decimal dec => this.Value = dec, - null => null, - _ => throw new ArgumentException("Invalid value type.", nameof(value)) - }; - } - - private static string CheckStringValue(string value) - { - return value.Length > 100 - ? throw new ArgumentException("Application command choice value cannot exceed 100 characters.", nameof(value)) - : value; - } -} +using System; +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Represents an option for a user to select for auto-completion. +/// +public sealed class DiscordAutoCompleteChoice +{ + /// + /// Gets the name of this option which will be presented to the user. + /// + [JsonProperty("name")] + public string Name { get; internal set; } + + /// + /// Gets the value of this option. This may be a string or an integer. + /// + [JsonProperty("value")] + public object? Value { get; internal set; } + + [JsonConstructor] + private DiscordAutoCompleteChoice() => this.Name = null!; + + /// + /// Creates a new instance of . + /// + private DiscordAutoCompleteChoice(string name) + { + if (name.Length is < 1 or > 100) + { + throw new ArgumentOutOfRangeException(nameof(name), "Application command choice name cannot be empty or exceed 100 characters."); + } + + this.Name = name; + } + + /// + /// The name of this option, which will be presented to the user. + /// The value of this option. + public DiscordAutoCompleteChoice(string name, object? value) : this(name) + { + this.Value = value switch + { + string s => CheckStringValue(s), + byte b => b, + sbyte sb => sb, + short s => s, + ushort us => us, + int i => this.Value = i, + uint ui => this.Value = ui, + long l => this.Value = l, + ulong ul => this.Value = ul, + double d => this.Value = d, + float f => this.Value = f, + decimal dec => this.Value = dec, + null => null, + _ => throw new ArgumentException("Invalid value type.", nameof(value)) + }; + } + + private static string CheckStringValue(string value) + { + return value.Length > 100 + ? throw new ArgumentException("Application command choice value cannot exceed 100 characters.", nameof(value)) + : value; + } +} diff --git a/DSharpPlus/Entities/Interaction/Components/DiscordActionRowComponent.cs b/DSharpPlus/Entities/Interaction/Components/DiscordActionRowComponent.cs index b75a410966..a6aedc6aba 100644 --- a/DSharpPlus/Entities/Interaction/Components/DiscordActionRowComponent.cs +++ b/DSharpPlus/Entities/Interaction/Components/DiscordActionRowComponent.cs @@ -1,20 +1,20 @@ -using System.Collections.Generic; -using System.Linq; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents a row of components. Action rows can have up to five components. -/// -public sealed class DiscordActionRowComponent : DiscordComponent -{ - /// - /// The components contained within the action row. - /// - [JsonProperty("components", NullValueHandling = NullValueHandling.Ignore)] - public IReadOnlyList Components { get; internal set; } = []; - - public DiscordActionRowComponent(IEnumerable components) : this() => this.Components = components.ToList().AsReadOnly(); - internal DiscordActionRowComponent() => this.Type = DiscordComponentType.ActionRow; // For Json.NET -} +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Represents a row of components. Action rows can have up to five components. +/// +public sealed class DiscordActionRowComponent : DiscordComponent +{ + /// + /// The components contained within the action row. + /// + [JsonProperty("components", NullValueHandling = NullValueHandling.Ignore)] + public IReadOnlyList Components { get; internal set; } = []; + + public DiscordActionRowComponent(IEnumerable components) : this() => this.Components = components.ToList().AsReadOnly(); + internal DiscordActionRowComponent() => this.Type = DiscordComponentType.ActionRow; // For Json.NET +} diff --git a/DSharpPlus/Entities/Interaction/Components/DiscordButtonComponent.cs b/DSharpPlus/Entities/Interaction/Components/DiscordButtonComponent.cs index ef4b6ea1d1..f666ee7792 100644 --- a/DSharpPlus/Entities/Interaction/Components/DiscordButtonComponent.cs +++ b/DSharpPlus/Entities/Interaction/Components/DiscordButtonComponent.cs @@ -1,90 +1,90 @@ -using DSharpPlus.EventArgs; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents a button that can be pressed. Fires when pressed. -/// -public class DiscordButtonComponent : DiscordComponent -{ - /// - /// The style of the button. - /// - [JsonProperty("style", NullValueHandling = NullValueHandling.Ignore)] - public DiscordButtonStyle Style { get; internal set; } - - /// - /// The text to apply to the button. If this is not specified becomes required. - /// - [JsonProperty("label", NullValueHandling = NullValueHandling.Ignore)] - public string Label { get; internal set; } - - /// - /// Whether this button can be pressed. - /// - [JsonProperty("disabled", NullValueHandling = NullValueHandling.Ignore)] - public bool Disabled { get; internal set; } - - /// - /// The emoji to add to the button. Can be used in conjunction with a label, or as standalone. Must be added if label is not specified. - /// - [JsonProperty("emoji", NullValueHandling = NullValueHandling.Ignore)] - public DiscordComponentEmoji Emoji { get; internal set; } - - /// - /// Enables this component if it was disabled before. - /// - /// The current component. - public DiscordButtonComponent Enable() - { - this.Disabled = false; - return this; - } - - /// - /// Disables this component. - /// - /// The current component. - public DiscordButtonComponent Disable() - { - this.Disabled = true; - return this; - } - - /// - /// Constructs a new . - /// - internal DiscordButtonComponent() => this.Type = DiscordComponentType.Button; - - /// - /// Constucts a new button based on another button. - /// - /// The button to copy. - public DiscordButtonComponent(DiscordButtonComponent other) : this() - { - this.CustomId = other.CustomId; - this.Style = other.Style; - this.Label = other.Label; - this.Disabled = other.Disabled; - this.Emoji = other.Emoji; - } - - /// - /// Constructs a new button with the specified options. - /// - /// The style/color of the button. - /// The Id to assign to the button. This is sent back when a user presses it. - /// The text to display on the button, up to 80 characters. Can be left blank if is set. - /// Whether this button should be initialized as being disabled. User sees a greyed out button that cannot be interacted with. - /// The emoji to add to the button. This is required if is empty or null. - public DiscordButtonComponent(DiscordButtonStyle style, string customId, string label, bool disabled = false, DiscordComponentEmoji emoji = null) - { - this.Style = style; - this.Label = label; - this.CustomId = customId; - this.Disabled = disabled; - this.Emoji = emoji; - this.Type = DiscordComponentType.Button; - } -} +using DSharpPlus.EventArgs; +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Represents a button that can be pressed. Fires when pressed. +/// +public class DiscordButtonComponent : DiscordComponent +{ + /// + /// The style of the button. + /// + [JsonProperty("style", NullValueHandling = NullValueHandling.Ignore)] + public DiscordButtonStyle Style { get; internal set; } + + /// + /// The text to apply to the button. If this is not specified becomes required. + /// + [JsonProperty("label", NullValueHandling = NullValueHandling.Ignore)] + public string Label { get; internal set; } + + /// + /// Whether this button can be pressed. + /// + [JsonProperty("disabled", NullValueHandling = NullValueHandling.Ignore)] + public bool Disabled { get; internal set; } + + /// + /// The emoji to add to the button. Can be used in conjunction with a label, or as standalone. Must be added if label is not specified. + /// + [JsonProperty("emoji", NullValueHandling = NullValueHandling.Ignore)] + public DiscordComponentEmoji Emoji { get; internal set; } + + /// + /// Enables this component if it was disabled before. + /// + /// The current component. + public DiscordButtonComponent Enable() + { + this.Disabled = false; + return this; + } + + /// + /// Disables this component. + /// + /// The current component. + public DiscordButtonComponent Disable() + { + this.Disabled = true; + return this; + } + + /// + /// Constructs a new . + /// + internal DiscordButtonComponent() => this.Type = DiscordComponentType.Button; + + /// + /// Constucts a new button based on another button. + /// + /// The button to copy. + public DiscordButtonComponent(DiscordButtonComponent other) : this() + { + this.CustomId = other.CustomId; + this.Style = other.Style; + this.Label = other.Label; + this.Disabled = other.Disabled; + this.Emoji = other.Emoji; + } + + /// + /// Constructs a new button with the specified options. + /// + /// The style/color of the button. + /// The Id to assign to the button. This is sent back when a user presses it. + /// The text to display on the button, up to 80 characters. Can be left blank if is set. + /// Whether this button should be initialized as being disabled. User sees a greyed out button that cannot be interacted with. + /// The emoji to add to the button. This is required if is empty or null. + public DiscordButtonComponent(DiscordButtonStyle style, string customId, string label, bool disabled = false, DiscordComponentEmoji emoji = null) + { + this.Style = style; + this.Label = label; + this.CustomId = customId; + this.Disabled = disabled; + this.Emoji = emoji; + this.Type = DiscordComponentType.Button; + } +} diff --git a/DSharpPlus/Entities/Interaction/Components/DiscordButtonStyle.cs b/DSharpPlus/Entities/Interaction/Components/DiscordButtonStyle.cs index d9e140e763..48c569b41a 100644 --- a/DSharpPlus/Entities/Interaction/Components/DiscordButtonStyle.cs +++ b/DSharpPlus/Entities/Interaction/Components/DiscordButtonStyle.cs @@ -1,28 +1,28 @@ -namespace DSharpPlus.Entities; - - -/// -/// Represents a button's style/color. -/// -public enum DiscordButtonStyle : int -{ - /// - /// Blurple button. - /// - Primary = 1, - - /// - /// Grey button. - /// - Secondary = 2, - - /// - /// Green button. - /// - Success = 3, - - /// - /// Red button. - /// - Danger = 4, -} +namespace DSharpPlus.Entities; + + +/// +/// Represents a button's style/color. +/// +public enum DiscordButtonStyle : int +{ + /// + /// Blurple button. + /// + Primary = 1, + + /// + /// Grey button. + /// + Secondary = 2, + + /// + /// Green button. + /// + Success = 3, + + /// + /// Red button. + /// + Danger = 4, +} diff --git a/DSharpPlus/Entities/Interaction/Components/DiscordComponent.cs b/DSharpPlus/Entities/Interaction/Components/DiscordComponent.cs index b44fdbcd76..dfe49423b0 100644 --- a/DSharpPlus/Entities/Interaction/Components/DiscordComponent.cs +++ b/DSharpPlus/Entities/Interaction/Components/DiscordComponent.cs @@ -1,33 +1,33 @@ -using DSharpPlus.Net.Serialization; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// A component to attach to a message. -/// -[JsonConverter(typeof(DiscordComponentJsonConverter))] -public class DiscordComponent -{ - /// - /// The type of component this represents. - /// - [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] - public DiscordComponentType Type { get; internal set; } - - /// - /// The Id of this component, if applicable. Not applicable on ActionRow(s) and link buttons. - /// - [JsonProperty("custom_id", NullValueHandling = NullValueHandling.Ignore)] - public string CustomId { get; internal set; } - - /// - /// The ID of the component - not to be confused with ; this is a numeric ID only used for identifying the component within an array. - /// - /// If this field is not set, it is generated in an auto-incrementing manner server-side. - /// - [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)] - public int Id { get; set; } - - internal DiscordComponent() { } -} +using DSharpPlus.Net.Serialization; +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// A component to attach to a message. +/// +[JsonConverter(typeof(DiscordComponentJsonConverter))] +public class DiscordComponent +{ + /// + /// The type of component this represents. + /// + [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] + public DiscordComponentType Type { get; internal set; } + + /// + /// The Id of this component, if applicable. Not applicable on ActionRow(s) and link buttons. + /// + [JsonProperty("custom_id", NullValueHandling = NullValueHandling.Ignore)] + public string CustomId { get; internal set; } + + /// + /// The ID of the component - not to be confused with ; this is a numeric ID only used for identifying the component within an array. + /// + /// If this field is not set, it is generated in an auto-incrementing manner server-side. + /// + [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)] + public int Id { get; set; } + + internal DiscordComponent() { } +} diff --git a/DSharpPlus/Entities/Interaction/Components/DiscordComponentType.cs b/DSharpPlus/Entities/Interaction/Components/DiscordComponentType.cs index f32327215e..73e6010fe3 100644 --- a/DSharpPlus/Entities/Interaction/Components/DiscordComponentType.cs +++ b/DSharpPlus/Entities/Interaction/Components/DiscordComponentType.cs @@ -1,84 +1,84 @@ -namespace DSharpPlus.Entities; - - -/// -/// Represents a type of component. -/// -public enum DiscordComponentType -{ - /// - /// A row of components. - /// - ActionRow = 1, - - /// - /// A button. - /// - Button = 2, - - /// - /// A select menu that allows arbitrary, bot-defined strings to be selected. - /// - StringSelect = 3, - - /// - /// An input field. - /// - FormInput = 4, - - /// - /// A select menu that allows users to be selected. - /// - UserSelect = 5, - - /// - /// A select menu that allows roles to be selected. - /// - RoleSelect = 6, - - /// - /// A select menu that allows either roles or users to be selected. - /// - MentionableSelect = 7, - - /// - /// A select menu that allows channels to be selected. - /// - ChannelSelect = 8, - - /// - /// A section of text with optional media (button, thumbnail) accessory. - /// - Section = 9, - - /// - /// A display of text, up to 4000 characters (unified). - /// - TextDisplay = 10, - - /// - /// A thumbnail. - /// - Thumbnail = 11, - - /// - /// A gallery of media. - /// - MediaGallery = 12, - - /// - /// A singular, arbitrary file. - /// - File = 13, - - /// - /// A separator between other components. - /// - Separator = 14, - - /// - /// A container for other components; can be styled with an accent color like embeds. - /// - Container = 17 - -} +namespace DSharpPlus.Entities; + + +/// +/// Represents a type of component. +/// +public enum DiscordComponentType +{ + /// + /// A row of components. + /// + ActionRow = 1, + + /// + /// A button. + /// + Button = 2, + + /// + /// A select menu that allows arbitrary, bot-defined strings to be selected. + /// + StringSelect = 3, + + /// + /// An input field. + /// + FormInput = 4, + + /// + /// A select menu that allows users to be selected. + /// + UserSelect = 5, + + /// + /// A select menu that allows roles to be selected. + /// + RoleSelect = 6, + + /// + /// A select menu that allows either roles or users to be selected. + /// + MentionableSelect = 7, + + /// + /// A select menu that allows channels to be selected. + /// + ChannelSelect = 8, + + /// + /// A section of text with optional media (button, thumbnail) accessory. + /// + Section = 9, + + /// + /// A display of text, up to 4000 characters (unified). + /// + TextDisplay = 10, + + /// + /// A thumbnail. + /// + Thumbnail = 11, + + /// + /// A gallery of media. + /// + MediaGallery = 12, + + /// + /// A singular, arbitrary file. + /// + File = 13, + + /// + /// A separator between other components. + /// + Separator = 14, + + /// + /// A container for other components; can be styled with an accent color like embeds. + /// + Container = 17 + +} diff --git a/DSharpPlus/Entities/Interaction/Components/DiscordEmojiComponent.cs b/DSharpPlus/Entities/Interaction/Components/DiscordEmojiComponent.cs index 013091b8cb..5a155e2529 100644 --- a/DSharpPlus/Entities/Interaction/Components/DiscordEmojiComponent.cs +++ b/DSharpPlus/Entities/Interaction/Components/DiscordEmojiComponent.cs @@ -1,57 +1,57 @@ -using System; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents an emoji to add to a component. -/// -public sealed class DiscordComponentEmoji -{ - /// - /// The Id of the emoji to use. - /// - [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)] - public ulong Id { get; set; } - - /// - /// The name of the emoji to use. Ignored if is set. - /// - [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] - public string Name { get; set; } - - /// - /// Constructs a new component emoji to add to a . - /// - public DiscordComponentEmoji() { } - - /// - /// Constructs a new component emoji from an emoji Id. - /// - /// The Id of the emoji to use. Any valid emoji Id can be passed. - public DiscordComponentEmoji(ulong id) => this.Id = id; - - /// - /// Constructs a new component emoji from unicode. - /// - /// The unicode emoji to set. - public DiscordComponentEmoji(string name) - { - if (!DiscordEmoji.IsValidUnicode(name)) - { - throw new ArgumentException("Only unicode emojis can be passed."); - } - - this.Name = name; - } - - /// - /// Constructs a new component emoji from an existing . - /// - /// The emoji to use. - public DiscordComponentEmoji(DiscordEmoji emoji) - { - this.Id = emoji.Id; - this.Name = emoji.Name; // Name is ignored if the Id is present. // - } -} +using System; +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Represents an emoji to add to a component. +/// +public sealed class DiscordComponentEmoji +{ + /// + /// The Id of the emoji to use. + /// + [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)] + public ulong Id { get; set; } + + /// + /// The name of the emoji to use. Ignored if is set. + /// + [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] + public string Name { get; set; } + + /// + /// Constructs a new component emoji to add to a . + /// + public DiscordComponentEmoji() { } + + /// + /// Constructs a new component emoji from an emoji Id. + /// + /// The Id of the emoji to use. Any valid emoji Id can be passed. + public DiscordComponentEmoji(ulong id) => this.Id = id; + + /// + /// Constructs a new component emoji from unicode. + /// + /// The unicode emoji to set. + public DiscordComponentEmoji(string name) + { + if (!DiscordEmoji.IsValidUnicode(name)) + { + throw new ArgumentException("Only unicode emojis can be passed."); + } + + this.Name = name; + } + + /// + /// Constructs a new component emoji from an existing . + /// + /// The emoji to use. + public DiscordComponentEmoji(DiscordEmoji emoji) + { + this.Id = emoji.Id; + this.Name = emoji.Name; // Name is ignored if the Id is present. // + } +} diff --git a/DSharpPlus/Entities/Interaction/Components/DiscordLinkButtonComponent.cs b/DSharpPlus/Entities/Interaction/Components/DiscordLinkButtonComponent.cs index 15508c1bec..b1509b1f67 100644 --- a/DSharpPlus/Entities/Interaction/Components/DiscordLinkButtonComponent.cs +++ b/DSharpPlus/Entities/Interaction/Components/DiscordLinkButtonComponent.cs @@ -1,54 +1,54 @@ -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents a link button. Clicking a link button does not send an interaction. -/// - -public sealed class DiscordLinkButtonComponent : DiscordButtonComponent -{ - /// - /// The url to open when pressing this button. - /// - [JsonProperty("url", NullValueHandling = NullValueHandling.Ignore)] - public string Url { get; set; } - - /// - /// The text to add to this button. If this is not specified, must be. - /// - [JsonProperty("label", NullValueHandling = NullValueHandling.Ignore)] - public new string Label { get; set; } - - /// - /// Whether this button can be pressed. - /// - [JsonProperty("disabled", NullValueHandling = NullValueHandling.Ignore)] - public new bool Disabled { get; set; } - - /// - /// The emoji to add to the button. Can be used in conjunction with a label, or as standalone. Must be added if label is not specified. - /// - [JsonProperty("emoji", NullValueHandling = NullValueHandling.Ignore)] - public new DiscordComponentEmoji Emoji { get; set; } - - [JsonProperty("style", NullValueHandling = NullValueHandling.Ignore)] - internal new int Style { get; } = 5; // Link = 5; Discord throws 400 otherwise // - - internal DiscordLinkButtonComponent() => this.Type = DiscordComponentType.Button; - - /// - /// Constructs a new . This type of button does not send back and interaction when pressed. - /// - /// The url to set the button to. - /// The text to display on the button. Can be left blank if is set. - /// Whether or not this button can be pressed. - /// The emoji to set with this button. This is required if is null or empty. - public DiscordLinkButtonComponent(string url, string label, bool disabled = false, DiscordComponentEmoji emoji = null) : this() - { - this.Url = url; - this.Label = label; - this.Disabled = disabled; - this.Emoji = emoji; - } -} +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Represents a link button. Clicking a link button does not send an interaction. +/// + +public sealed class DiscordLinkButtonComponent : DiscordButtonComponent +{ + /// + /// The url to open when pressing this button. + /// + [JsonProperty("url", NullValueHandling = NullValueHandling.Ignore)] + public string Url { get; set; } + + /// + /// The text to add to this button. If this is not specified, must be. + /// + [JsonProperty("label", NullValueHandling = NullValueHandling.Ignore)] + public new string Label { get; set; } + + /// + /// Whether this button can be pressed. + /// + [JsonProperty("disabled", NullValueHandling = NullValueHandling.Ignore)] + public new bool Disabled { get; set; } + + /// + /// The emoji to add to the button. Can be used in conjunction with a label, or as standalone. Must be added if label is not specified. + /// + [JsonProperty("emoji", NullValueHandling = NullValueHandling.Ignore)] + public new DiscordComponentEmoji Emoji { get; set; } + + [JsonProperty("style", NullValueHandling = NullValueHandling.Ignore)] + internal new int Style { get; } = 5; // Link = 5; Discord throws 400 otherwise // + + internal DiscordLinkButtonComponent() => this.Type = DiscordComponentType.Button; + + /// + /// Constructs a new . This type of button does not send back and interaction when pressed. + /// + /// The url to set the button to. + /// The text to display on the button. Can be left blank if is set. + /// Whether or not this button can be pressed. + /// The emoji to set with this button. This is required if is null or empty. + public DiscordLinkButtonComponent(string url, string label, bool disabled = false, DiscordComponentEmoji emoji = null) : this() + { + this.Url = url; + this.Label = label; + this.Disabled = disabled; + this.Emoji = emoji; + } +} diff --git a/DSharpPlus/Entities/Interaction/Components/DiscordSelectDefaultValueType.cs b/DSharpPlus/Entities/Interaction/Components/DiscordSelectDefaultValueType.cs index ed09237ea7..526f63a297 100644 --- a/DSharpPlus/Entities/Interaction/Components/DiscordSelectDefaultValueType.cs +++ b/DSharpPlus/Entities/Interaction/Components/DiscordSelectDefaultValueType.cs @@ -1,12 +1,12 @@ -namespace DSharpPlus; - - -/// -/// Type of a default value for a select component. -/// -public enum DiscordSelectDefaultValueType -{ - User, - Role, - Channel -} +namespace DSharpPlus; + + +/// +/// Type of a default value for a select component. +/// +public enum DiscordSelectDefaultValueType +{ + User, + Role, + Channel +} diff --git a/DSharpPlus/Entities/Interaction/Components/DiscordTextInputComponent.cs b/DSharpPlus/Entities/Interaction/Components/DiscordTextInputComponent.cs index bec25b4525..82ac139658 100644 --- a/DSharpPlus/Entities/Interaction/Components/DiscordTextInputComponent.cs +++ b/DSharpPlus/Entities/Interaction/Components/DiscordTextInputComponent.cs @@ -1,88 +1,88 @@ -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// A text-input field. Like selects, this can only be used once per action row. -/// -public sealed class DiscordTextInputComponent : DiscordComponent -{ - /// - /// Optional placeholder text for this input. - /// - [JsonProperty("placeholder", NullValueHandling = NullValueHandling.Ignore)] - public string? Placeholder { get; set; } - - /// - /// Label text to put above this input. - /// - [JsonProperty("label")] - public string Label { get; set; } = default!; - - /// - /// Pre-filled value for this input. - /// - [JsonProperty("value")] - public string? Value { get; set; } - - /// - /// Optional minimum length for this input. - /// - [JsonProperty("min_length", NullValueHandling = NullValueHandling.Ignore)] - public int MinimumLength { get; set; } - - /// - /// Optional maximum length for this input. Must be a positive integer, if set. - /// - [JsonProperty("max_length", NullValueHandling = NullValueHandling.Ignore)] - public int? MaximumLength { get; set; } - - /// - /// Whether this input is required. - /// - [JsonProperty("required")] - public bool Required { get; set; } - - /// - /// Style of this input. - /// - [JsonProperty("style")] - public DiscordTextInputStyle Style { get; set; } - - public DiscordTextInputComponent() => this.Type = DiscordComponentType.FormInput; - - /// - /// Constructs a new text input field. - /// - /// The label for the field, placed above the input itself. - /// The ID of this field. - /// Placeholder text for the field. - /// A pre-filled value for this field. - /// Whether this field is required. - /// The style of this field. A single-ling short, or multi-line paragraph. - /// The minimum input length. - /// The maximum input length. Must be greater than the minimum, if set. - public DiscordTextInputComponent - ( - string label, - string customId, - string? placeholder = null, - string? value = null, - bool required = true, - DiscordTextInputStyle style = - DiscordTextInputStyle.Short, - int min_length = 0, - int? max_length = null - ) - { - this.CustomId = customId; - this.Type = DiscordComponentType.FormInput; - this.Label = label; - this.Required = required; - this.Placeholder = placeholder; - this.MinimumLength = min_length; - this.MaximumLength = max_length; - this.Style = style; - this.Value = value; - } -} +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// A text-input field. Like selects, this can only be used once per action row. +/// +public sealed class DiscordTextInputComponent : DiscordComponent +{ + /// + /// Optional placeholder text for this input. + /// + [JsonProperty("placeholder", NullValueHandling = NullValueHandling.Ignore)] + public string? Placeholder { get; set; } + + /// + /// Label text to put above this input. + /// + [JsonProperty("label")] + public string Label { get; set; } = default!; + + /// + /// Pre-filled value for this input. + /// + [JsonProperty("value")] + public string? Value { get; set; } + + /// + /// Optional minimum length for this input. + /// + [JsonProperty("min_length", NullValueHandling = NullValueHandling.Ignore)] + public int MinimumLength { get; set; } + + /// + /// Optional maximum length for this input. Must be a positive integer, if set. + /// + [JsonProperty("max_length", NullValueHandling = NullValueHandling.Ignore)] + public int? MaximumLength { get; set; } + + /// + /// Whether this input is required. + /// + [JsonProperty("required")] + public bool Required { get; set; } + + /// + /// Style of this input. + /// + [JsonProperty("style")] + public DiscordTextInputStyle Style { get; set; } + + public DiscordTextInputComponent() => this.Type = DiscordComponentType.FormInput; + + /// + /// Constructs a new text input field. + /// + /// The label for the field, placed above the input itself. + /// The ID of this field. + /// Placeholder text for the field. + /// A pre-filled value for this field. + /// Whether this field is required. + /// The style of this field. A single-ling short, or multi-line paragraph. + /// The minimum input length. + /// The maximum input length. Must be greater than the minimum, if set. + public DiscordTextInputComponent + ( + string label, + string customId, + string? placeholder = null, + string? value = null, + bool required = true, + DiscordTextInputStyle style = + DiscordTextInputStyle.Short, + int min_length = 0, + int? max_length = null + ) + { + this.CustomId = customId; + this.Type = DiscordComponentType.FormInput; + this.Label = label; + this.Required = required; + this.Placeholder = placeholder; + this.MinimumLength = min_length; + this.MaximumLength = max_length; + this.Style = style; + this.Value = value; + } +} diff --git a/DSharpPlus/Entities/Interaction/Components/DiscordTextInputStyle.cs b/DSharpPlus/Entities/Interaction/Components/DiscordTextInputStyle.cs index 5de6629421..bb5becd894 100644 --- a/DSharpPlus/Entities/Interaction/Components/DiscordTextInputStyle.cs +++ b/DSharpPlus/Entities/Interaction/Components/DiscordTextInputStyle.cs @@ -1,17 +1,17 @@ -namespace DSharpPlus.Entities; - - -/// -/// The style for a -/// -public enum DiscordTextInputStyle -{ - /// - /// A short, single-line input - /// - Short = 1, - /// - /// A longer, multi-line input - /// - Paragraph = 2 -} +namespace DSharpPlus.Entities; + + +/// +/// The style for a +/// +public enum DiscordTextInputStyle +{ + /// + /// A short, single-line input + /// + Short = 1, + /// + /// A longer, multi-line input + /// + Paragraph = 2 +} diff --git a/DSharpPlus/Entities/Interaction/Components/Selects/BaseDiscordSelectComponent.cs b/DSharpPlus/Entities/Interaction/Components/Selects/BaseDiscordSelectComponent.cs index d2027aa272..0bacc4c888 100644 --- a/DSharpPlus/Entities/Interaction/Components/Selects/BaseDiscordSelectComponent.cs +++ b/DSharpPlus/Entities/Interaction/Components/Selects/BaseDiscordSelectComponent.cs @@ -1,72 +1,72 @@ -using System; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents a base class for all select-menus. -/// -public abstract class BaseDiscordSelectComponent : DiscordComponent -{ - /// - /// The text to show when no option is selected. - /// - [JsonProperty("placeholder", NullValueHandling = NullValueHandling.Ignore)] - public string Placeholder { get; internal set; } - - /// - /// Whether this dropdown can be interacted with. - /// - [JsonProperty("disabled", NullValueHandling = NullValueHandling.Ignore)] - public bool Disabled { get; internal set; } - - /// - /// The minimum amount of options that can be selected. Must be less than or equal to . Defaults to one. - /// - [JsonProperty("min_values", NullValueHandling = NullValueHandling.Ignore)] - public int? MinimumSelectedValues { get; internal set; } - - /// - /// The maximum amount of options that can be selected. Must be greater than or equal to zero or . Defaults to one. - /// - [JsonProperty("max_values", NullValueHandling = NullValueHandling.Ignore)] - public int? MaximumSelectedValues { get; internal set; } - - // used by Newtonsoft.Json - public BaseDiscordSelectComponent() - { - } - - internal BaseDiscordSelectComponent - ( - DiscordComponentType type, - string customId, - string placeholder, - bool disabled = false, - int minOptions = 1, - int maxOptions = 1 - ) - { - this.Type = type; - this.CustomId = customId; - this.Placeholder = placeholder; - this.Disabled = disabled; - this.MinimumSelectedValues = minOptions; - this.MaximumSelectedValues = maxOptions; - - if (this.MinimumSelectedValues < 0) - { - throw new ArgumentOutOfRangeException(nameof(minOptions), "Minimum selected values must be greater than or equal to zero."); - } - - if (this.MaximumSelectedValues < 1) - { - throw new ArgumentOutOfRangeException(nameof(maxOptions), "Maximum selected values must be greater than or equal to one."); - } - - if (this.MinimumSelectedValues > this.MaximumSelectedValues) - { - throw new ArgumentOutOfRangeException(nameof(minOptions), "Minimum selected values must be less than or equal to maximum selected values."); - } - } -} +using System; +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Represents a base class for all select-menus. +/// +public abstract class BaseDiscordSelectComponent : DiscordComponent +{ + /// + /// The text to show when no option is selected. + /// + [JsonProperty("placeholder", NullValueHandling = NullValueHandling.Ignore)] + public string Placeholder { get; internal set; } + + /// + /// Whether this dropdown can be interacted with. + /// + [JsonProperty("disabled", NullValueHandling = NullValueHandling.Ignore)] + public bool Disabled { get; internal set; } + + /// + /// The minimum amount of options that can be selected. Must be less than or equal to . Defaults to one. + /// + [JsonProperty("min_values", NullValueHandling = NullValueHandling.Ignore)] + public int? MinimumSelectedValues { get; internal set; } + + /// + /// The maximum amount of options that can be selected. Must be greater than or equal to zero or . Defaults to one. + /// + [JsonProperty("max_values", NullValueHandling = NullValueHandling.Ignore)] + public int? MaximumSelectedValues { get; internal set; } + + // used by Newtonsoft.Json + public BaseDiscordSelectComponent() + { + } + + internal BaseDiscordSelectComponent + ( + DiscordComponentType type, + string customId, + string placeholder, + bool disabled = false, + int minOptions = 1, + int maxOptions = 1 + ) + { + this.Type = type; + this.CustomId = customId; + this.Placeholder = placeholder; + this.Disabled = disabled; + this.MinimumSelectedValues = minOptions; + this.MaximumSelectedValues = maxOptions; + + if (this.MinimumSelectedValues < 0) + { + throw new ArgumentOutOfRangeException(nameof(minOptions), "Minimum selected values must be greater than or equal to zero."); + } + + if (this.MaximumSelectedValues < 1) + { + throw new ArgumentOutOfRangeException(nameof(maxOptions), "Maximum selected values must be greater than or equal to one."); + } + + if (this.MinimumSelectedValues > this.MaximumSelectedValues) + { + throw new ArgumentOutOfRangeException(nameof(minOptions), "Minimum selected values must be less than or equal to maximum selected values."); + } + } +} diff --git a/DSharpPlus/Entities/Interaction/Components/Selects/DiscordChannelSelectComponent.cs b/DSharpPlus/Entities/Interaction/Components/Selects/DiscordChannelSelectComponent.cs index 4f80967544..2f8ce16263 100644 --- a/DSharpPlus/Entities/Interaction/Components/Selects/DiscordChannelSelectComponent.cs +++ b/DSharpPlus/Entities/Interaction/Components/Selects/DiscordChannelSelectComponent.cs @@ -1,114 +1,114 @@ -using System.Collections.Generic; -using System.Linq; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -public sealed class DiscordChannelSelectComponent : BaseDiscordSelectComponent -{ - [JsonProperty("channel_types", NullValueHandling = NullValueHandling.Ignore)] - public IReadOnlyList ChannelTypes { get; internal set; } - - [JsonProperty("default_values", NullValueHandling = NullValueHandling.Ignore)] - private readonly List defaultValues = []; - - /// - /// The default values for this component. - /// - [JsonIgnore] - public IReadOnlyList DefaultValues => this.defaultValues; - - /// - /// Adds a default channel to this component. - /// - /// Channel to add - public DiscordChannelSelectComponent AddDefaultChannel(DiscordChannel channel) - { - DiscordSelectDefaultValue defaultValue = new(channel.Id, DiscordSelectDefaultValueType.Channel); - this.defaultValues.Add(defaultValue); - return this; - } - - /// - /// Adds a collections of DiscordChannel as default values. - /// - /// Collection of DiscordChannel - public DiscordChannelSelectComponent AddDefaultChannels(IEnumerable channels) - { - foreach (DiscordChannel value in channels) - { - DiscordSelectDefaultValue defaultValue = new(value.Id, DiscordSelectDefaultValueType.Channel); - this.defaultValues.Add(defaultValue); - } - - return this; - } - - /// - /// Adds a default channel to this component. - /// - /// Id of a DiscordChannel - public DiscordChannelSelectComponent AddDefaultChannel(ulong id) - { - DiscordSelectDefaultValue defaultValue = new(id, DiscordSelectDefaultValueType.Channel); - this.defaultValues.Add(defaultValue); - return this; - } - - /// - /// Collections of channel ids to add as default values. - /// - /// Collection of DiscordChannel ids - public DiscordChannelSelectComponent AddDefaultChannels(IEnumerable ids) - { - foreach (ulong value in ids) - { - DiscordSelectDefaultValue defaultValue = new(value, DiscordSelectDefaultValueType.Channel); - this.defaultValues.Add(defaultValue); - } - - return this; - } - - /// - /// Enables this component. - /// - /// The current component. - public DiscordChannelSelectComponent Enable() - { - this.Disabled = false; - return this; - } - - /// - /// Disables this component. - /// - /// The current component. - public DiscordChannelSelectComponent Disable() - { - this.Disabled = true; - return this; - } - - internal DiscordChannelSelectComponent() => this.Type = DiscordComponentType.ChannelSelect; - - /// - /// Creates a new channel select component. - /// - /// The ID of this component. - /// Placeholder text that's shown when no options are selected. - /// Optional channel types to filter by. - /// Whether this component is disabled. - /// The minimum amount of options to be selected. - /// The maximum amount of options to be selected, up to 25. - public DiscordChannelSelectComponent - ( - string customId, - string placeholder, - IEnumerable? channelTypes = null, - bool disabled = false, - int minOptions = 1, - int maxOptions = 1 - ) : base(DiscordComponentType.ChannelSelect, customId, placeholder, disabled, minOptions, maxOptions) => - this.ChannelTypes = channelTypes?.ToList(); -} +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +public sealed class DiscordChannelSelectComponent : BaseDiscordSelectComponent +{ + [JsonProperty("channel_types", NullValueHandling = NullValueHandling.Ignore)] + public IReadOnlyList ChannelTypes { get; internal set; } + + [JsonProperty("default_values", NullValueHandling = NullValueHandling.Ignore)] + private readonly List defaultValues = []; + + /// + /// The default values for this component. + /// + [JsonIgnore] + public IReadOnlyList DefaultValues => this.defaultValues; + + /// + /// Adds a default channel to this component. + /// + /// Channel to add + public DiscordChannelSelectComponent AddDefaultChannel(DiscordChannel channel) + { + DiscordSelectDefaultValue defaultValue = new(channel.Id, DiscordSelectDefaultValueType.Channel); + this.defaultValues.Add(defaultValue); + return this; + } + + /// + /// Adds a collections of DiscordChannel as default values. + /// + /// Collection of DiscordChannel + public DiscordChannelSelectComponent AddDefaultChannels(IEnumerable channels) + { + foreach (DiscordChannel value in channels) + { + DiscordSelectDefaultValue defaultValue = new(value.Id, DiscordSelectDefaultValueType.Channel); + this.defaultValues.Add(defaultValue); + } + + return this; + } + + /// + /// Adds a default channel to this component. + /// + /// Id of a DiscordChannel + public DiscordChannelSelectComponent AddDefaultChannel(ulong id) + { + DiscordSelectDefaultValue defaultValue = new(id, DiscordSelectDefaultValueType.Channel); + this.defaultValues.Add(defaultValue); + return this; + } + + /// + /// Collections of channel ids to add as default values. + /// + /// Collection of DiscordChannel ids + public DiscordChannelSelectComponent AddDefaultChannels(IEnumerable ids) + { + foreach (ulong value in ids) + { + DiscordSelectDefaultValue defaultValue = new(value, DiscordSelectDefaultValueType.Channel); + this.defaultValues.Add(defaultValue); + } + + return this; + } + + /// + /// Enables this component. + /// + /// The current component. + public DiscordChannelSelectComponent Enable() + { + this.Disabled = false; + return this; + } + + /// + /// Disables this component. + /// + /// The current component. + public DiscordChannelSelectComponent Disable() + { + this.Disabled = true; + return this; + } + + internal DiscordChannelSelectComponent() => this.Type = DiscordComponentType.ChannelSelect; + + /// + /// Creates a new channel select component. + /// + /// The ID of this component. + /// Placeholder text that's shown when no options are selected. + /// Optional channel types to filter by. + /// Whether this component is disabled. + /// The minimum amount of options to be selected. + /// The maximum amount of options to be selected, up to 25. + public DiscordChannelSelectComponent + ( + string customId, + string placeholder, + IEnumerable? channelTypes = null, + bool disabled = false, + int minOptions = 1, + int maxOptions = 1 + ) : base(DiscordComponentType.ChannelSelect, customId, placeholder, disabled, minOptions, maxOptions) => + this.ChannelTypes = channelTypes?.ToList(); +} diff --git a/DSharpPlus/Entities/Interaction/Components/Selects/DiscordMentionableSelectComponent.cs b/DSharpPlus/Entities/Interaction/Components/Selects/DiscordMentionableSelectComponent.cs index 00c24c1585..01c8d60324 100644 --- a/DSharpPlus/Entities/Interaction/Components/Selects/DiscordMentionableSelectComponent.cs +++ b/DSharpPlus/Entities/Interaction/Components/Selects/DiscordMentionableSelectComponent.cs @@ -1,87 +1,87 @@ -using System; -using System.Collections.Generic; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -public sealed class DiscordMentionableSelectComponent : BaseDiscordSelectComponent -{ - [JsonProperty("default_values", NullValueHandling = NullValueHandling.Ignore)] - private readonly List defaultValues = []; - - /// - /// The default values for this component. - /// - [JsonIgnore] - public IReadOnlyList DefaultValues => this.defaultValues; - - /// - /// Adds a default role or user to this component. - /// - /// type of the default - /// Id of the default - public DiscordMentionableSelectComponent AddDefault(DiscordSelectDefaultValueType type, ulong id) - { - if (type == DiscordSelectDefaultValueType.Channel) - { - throw new ArgumentException("Mentionable select components do not support channel defaults"); - } - - DiscordSelectDefaultValue defaultValue = new(id, type); - this.defaultValues.Add(defaultValue); - return this; - } - - /// - /// Adds a collections of DiscordRoles or DiscordUsers to this component. All the ids must be of the same type. - /// - /// Type of the defaults - /// Collection of ids - public DiscordMentionableSelectComponent AddDefaults(DiscordSelectDefaultValueType type, IEnumerable ids) - { - if (type == DiscordSelectDefaultValueType.Channel) - { - throw new ArgumentException("Mentionable select components do not support channel defaults"); - } - - foreach (ulong id in ids) - { - DiscordSelectDefaultValue defaultValue = new(id, type); - this.defaultValues.Add(defaultValue); - } - - return this; - } - - /// - /// Enables this component. - /// - /// The current component. - public DiscordMentionableSelectComponent Enable() - { - this.Disabled = false; - return this; - } - /// - /// Disables this component. - /// - /// The current component. - public DiscordMentionableSelectComponent Disable() - { - this.Disabled = true; - return this; - } - - internal DiscordMentionableSelectComponent() => this.Type = DiscordComponentType.MentionableSelect; - - /// - /// Creates a new mentionable select component. - /// - /// The ID of this component. - /// Placeholder text that's shown when no options are selected. - /// Whether this component is disabled. - /// The minimum amount of options to be selected. - /// The maximum amount of options to be selected, up to 25. - public DiscordMentionableSelectComponent(string customId, string placeholder, bool disabled = false, int minOptions = 1, int maxOptions = 1) - : base(DiscordComponentType.MentionableSelect, customId, placeholder, disabled, minOptions, maxOptions) { } -} +using System; +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +public sealed class DiscordMentionableSelectComponent : BaseDiscordSelectComponent +{ + [JsonProperty("default_values", NullValueHandling = NullValueHandling.Ignore)] + private readonly List defaultValues = []; + + /// + /// The default values for this component. + /// + [JsonIgnore] + public IReadOnlyList DefaultValues => this.defaultValues; + + /// + /// Adds a default role or user to this component. + /// + /// type of the default + /// Id of the default + public DiscordMentionableSelectComponent AddDefault(DiscordSelectDefaultValueType type, ulong id) + { + if (type == DiscordSelectDefaultValueType.Channel) + { + throw new ArgumentException("Mentionable select components do not support channel defaults"); + } + + DiscordSelectDefaultValue defaultValue = new(id, type); + this.defaultValues.Add(defaultValue); + return this; + } + + /// + /// Adds a collections of DiscordRoles or DiscordUsers to this component. All the ids must be of the same type. + /// + /// Type of the defaults + /// Collection of ids + public DiscordMentionableSelectComponent AddDefaults(DiscordSelectDefaultValueType type, IEnumerable ids) + { + if (type == DiscordSelectDefaultValueType.Channel) + { + throw new ArgumentException("Mentionable select components do not support channel defaults"); + } + + foreach (ulong id in ids) + { + DiscordSelectDefaultValue defaultValue = new(id, type); + this.defaultValues.Add(defaultValue); + } + + return this; + } + + /// + /// Enables this component. + /// + /// The current component. + public DiscordMentionableSelectComponent Enable() + { + this.Disabled = false; + return this; + } + /// + /// Disables this component. + /// + /// The current component. + public DiscordMentionableSelectComponent Disable() + { + this.Disabled = true; + return this; + } + + internal DiscordMentionableSelectComponent() => this.Type = DiscordComponentType.MentionableSelect; + + /// + /// Creates a new mentionable select component. + /// + /// The ID of this component. + /// Placeholder text that's shown when no options are selected. + /// Whether this component is disabled. + /// The minimum amount of options to be selected. + /// The maximum amount of options to be selected, up to 25. + public DiscordMentionableSelectComponent(string customId, string placeholder, bool disabled = false, int minOptions = 1, int maxOptions = 1) + : base(DiscordComponentType.MentionableSelect, customId, placeholder, disabled, minOptions, maxOptions) { } +} diff --git a/DSharpPlus/Entities/Interaction/Components/Selects/DiscordRoleSelectComponent.cs b/DSharpPlus/Entities/Interaction/Components/Selects/DiscordRoleSelectComponent.cs index 9bb91ba647..ab76280f1e 100644 --- a/DSharpPlus/Entities/Interaction/Components/Selects/DiscordRoleSelectComponent.cs +++ b/DSharpPlus/Entities/Interaction/Components/Selects/DiscordRoleSelectComponent.cs @@ -1,100 +1,100 @@ -using System.Collections.Generic; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -public sealed class DiscordRoleSelectComponent : BaseDiscordSelectComponent -{ - [JsonProperty("default_values", NullValueHandling = NullValueHandling.Ignore)] - private readonly List defaultValues = []; - - /// - /// The default values for this component. - /// - [JsonIgnore] - public IReadOnlyList DefaultValues => this.defaultValues; - - /// - /// Adds a default role to this component. - /// - /// Role to add - public DiscordRoleSelectComponent AddDefaultRole(DiscordRole role) - { - DiscordSelectDefaultValue defaultValue = new(role.Id, DiscordSelectDefaultValueType.Role); - this.defaultValues.Add(defaultValue); - return this; - } - - /// - /// Adds a collections of DiscordRoles to this component. - /// - /// Collection of DiscordRoles - public DiscordRoleSelectComponent AddDefaultRoles(IEnumerable roles) - { - foreach (DiscordRole value in roles) - { - DiscordSelectDefaultValue defaultValue = new(value.Id, DiscordSelectDefaultValueType.Role); - this.defaultValues.Add(defaultValue); - } - - return this; - } - - /// - /// Adds a default role to this component. - /// - /// Id of a DiscordRole - public DiscordRoleSelectComponent AddDefaultRole(ulong id) - { - DiscordSelectDefaultValue defaultValue = new(id, DiscordSelectDefaultValueType.Role); - this.defaultValues.Add(defaultValue); - return this; - } - - /// - /// Collections of role ids to add as default values. - /// - /// Collection of DiscordRole ids - public DiscordRoleSelectComponent AddDefaultRoles(IEnumerable ids) - { - foreach (ulong value in ids) - { - DiscordSelectDefaultValue defaultValue = new(value, DiscordSelectDefaultValueType.Role); - this.defaultValues.Add(defaultValue); - } - - return this; - } - - /// - /// Enables this component. - /// - /// The current component. - public DiscordRoleSelectComponent Enable() - { - this.Disabled = false; - return this; - } - /// - /// Disables this component. - /// - /// The current component. - public DiscordRoleSelectComponent Disable() - { - this.Disabled = true; - return this; - } - - internal DiscordRoleSelectComponent() => this.Type = DiscordComponentType.RoleSelect; - - /// - /// Creates a new role select component. - /// - /// The ID of this component. - /// Placeholder text that's shown when no options are selected. - /// Whether this component is disabled. - /// The minimum amount of options to be selected. - /// The maximum amount of options to be selected, up to 25. - public DiscordRoleSelectComponent(string customId, string placeholder, bool disabled = false, int minOptions = 1, int maxOptions = 1) - : base(DiscordComponentType.RoleSelect, customId, placeholder, disabled, minOptions, maxOptions) { } -} +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +public sealed class DiscordRoleSelectComponent : BaseDiscordSelectComponent +{ + [JsonProperty("default_values", NullValueHandling = NullValueHandling.Ignore)] + private readonly List defaultValues = []; + + /// + /// The default values for this component. + /// + [JsonIgnore] + public IReadOnlyList DefaultValues => this.defaultValues; + + /// + /// Adds a default role to this component. + /// + /// Role to add + public DiscordRoleSelectComponent AddDefaultRole(DiscordRole role) + { + DiscordSelectDefaultValue defaultValue = new(role.Id, DiscordSelectDefaultValueType.Role); + this.defaultValues.Add(defaultValue); + return this; + } + + /// + /// Adds a collections of DiscordRoles to this component. + /// + /// Collection of DiscordRoles + public DiscordRoleSelectComponent AddDefaultRoles(IEnumerable roles) + { + foreach (DiscordRole value in roles) + { + DiscordSelectDefaultValue defaultValue = new(value.Id, DiscordSelectDefaultValueType.Role); + this.defaultValues.Add(defaultValue); + } + + return this; + } + + /// + /// Adds a default role to this component. + /// + /// Id of a DiscordRole + public DiscordRoleSelectComponent AddDefaultRole(ulong id) + { + DiscordSelectDefaultValue defaultValue = new(id, DiscordSelectDefaultValueType.Role); + this.defaultValues.Add(defaultValue); + return this; + } + + /// + /// Collections of role ids to add as default values. + /// + /// Collection of DiscordRole ids + public DiscordRoleSelectComponent AddDefaultRoles(IEnumerable ids) + { + foreach (ulong value in ids) + { + DiscordSelectDefaultValue defaultValue = new(value, DiscordSelectDefaultValueType.Role); + this.defaultValues.Add(defaultValue); + } + + return this; + } + + /// + /// Enables this component. + /// + /// The current component. + public DiscordRoleSelectComponent Enable() + { + this.Disabled = false; + return this; + } + /// + /// Disables this component. + /// + /// The current component. + public DiscordRoleSelectComponent Disable() + { + this.Disabled = true; + return this; + } + + internal DiscordRoleSelectComponent() => this.Type = DiscordComponentType.RoleSelect; + + /// + /// Creates a new role select component. + /// + /// The ID of this component. + /// Placeholder text that's shown when no options are selected. + /// Whether this component is disabled. + /// The minimum amount of options to be selected. + /// The maximum amount of options to be selected, up to 25. + public DiscordRoleSelectComponent(string customId, string placeholder, bool disabled = false, int minOptions = 1, int maxOptions = 1) + : base(DiscordComponentType.RoleSelect, customId, placeholder, disabled, minOptions, maxOptions) { } +} diff --git a/DSharpPlus/Entities/Interaction/Components/Selects/DiscordSelectComponent.cs b/DSharpPlus/Entities/Interaction/Components/Selects/DiscordSelectComponent.cs index 1b95cfa773..fecc3d5de2 100644 --- a/DSharpPlus/Entities/Interaction/Components/Selects/DiscordSelectComponent.cs +++ b/DSharpPlus/Entities/Interaction/Components/Selects/DiscordSelectComponent.cs @@ -1,44 +1,44 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// A select menu with multiple options to choose from. -/// -public sealed class DiscordSelectComponent : BaseDiscordSelectComponent -{ - /// - /// The options to pick from on this component. - /// - [JsonProperty("options", NullValueHandling = NullValueHandling.Ignore)] - public IReadOnlyList Options { get; internal set; } = Array.Empty(); - - /// - /// Enables this component. - /// - /// The current component. - public DiscordSelectComponent Enable() - { - this.Disabled = false; - return this; - } - - /// - /// Disables this component. - /// - /// The current component. - public DiscordSelectComponent Disable() - { - this.Disabled = true; - return this; - } - - internal DiscordSelectComponent() => this.Type = DiscordComponentType.StringSelect; - - public DiscordSelectComponent(string customId, string placeholder, IEnumerable options, bool disabled = false, int minOptions = 1, int maxOptions = 1) - : base(DiscordComponentType.StringSelect, customId, placeholder, disabled, minOptions, maxOptions) - => this.Options = options.ToArray(); -} +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// A select menu with multiple options to choose from. +/// +public sealed class DiscordSelectComponent : BaseDiscordSelectComponent +{ + /// + /// The options to pick from on this component. + /// + [JsonProperty("options", NullValueHandling = NullValueHandling.Ignore)] + public IReadOnlyList Options { get; internal set; } = Array.Empty(); + + /// + /// Enables this component. + /// + /// The current component. + public DiscordSelectComponent Enable() + { + this.Disabled = false; + return this; + } + + /// + /// Disables this component. + /// + /// The current component. + public DiscordSelectComponent Disable() + { + this.Disabled = true; + return this; + } + + internal DiscordSelectComponent() => this.Type = DiscordComponentType.StringSelect; + + public DiscordSelectComponent(string customId, string placeholder, IEnumerable options, bool disabled = false, int minOptions = 1, int maxOptions = 1) + : base(DiscordComponentType.StringSelect, customId, placeholder, disabled, minOptions, maxOptions) + => this.Options = options.ToArray(); +} diff --git a/DSharpPlus/Entities/Interaction/Components/Selects/DiscordSelectComponentOption.cs b/DSharpPlus/Entities/Interaction/Components/Selects/DiscordSelectComponentOption.cs index d190da9c0a..71fa32e77f 100644 --- a/DSharpPlus/Entities/Interaction/Components/Selects/DiscordSelectComponentOption.cs +++ b/DSharpPlus/Entities/Interaction/Components/Selects/DiscordSelectComponentOption.cs @@ -1,48 +1,48 @@ -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents options for . -/// -public sealed class DiscordSelectComponentOption -{ - /// - /// The label to add. This is required. - /// - [JsonProperty("label", NullValueHandling = NullValueHandling.Ignore)] - public string Label { get; internal set; } - - /// - /// The value of this option. Akin to the Custom Id of components. - /// - [JsonProperty("value", NullValueHandling = NullValueHandling.Ignore)] - public string Value { get; internal set; } - - /// - /// Whether this option is default. If true, this option will be pre-selected. Defaults to false. - /// - [JsonProperty("default", NullValueHandling = NullValueHandling.Ignore)] - public bool Default { get; internal set; } // false // - - /// - /// The description of this option. This is optional. - /// - [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] - public string Description { get; internal set; } - - /// - /// The emoji of this option. This is optional. - /// - [JsonProperty("emoji", NullValueHandling = NullValueHandling.Ignore)] - public DiscordComponentEmoji Emoji { get; internal set; } - - public DiscordSelectComponentOption(string label, string value, string description = null, bool isDefault = false, DiscordComponentEmoji emoji = null) - { - this.Label = label; - this.Value = value; - this.Description = description; - this.Default = isDefault; - this.Emoji = emoji; - } -} +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Represents options for . +/// +public sealed class DiscordSelectComponentOption +{ + /// + /// The label to add. This is required. + /// + [JsonProperty("label", NullValueHandling = NullValueHandling.Ignore)] + public string Label { get; internal set; } + + /// + /// The value of this option. Akin to the Custom Id of components. + /// + [JsonProperty("value", NullValueHandling = NullValueHandling.Ignore)] + public string Value { get; internal set; } + + /// + /// Whether this option is default. If true, this option will be pre-selected. Defaults to false. + /// + [JsonProperty("default", NullValueHandling = NullValueHandling.Ignore)] + public bool Default { get; internal set; } // false // + + /// + /// The description of this option. This is optional. + /// + [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] + public string Description { get; internal set; } + + /// + /// The emoji of this option. This is optional. + /// + [JsonProperty("emoji", NullValueHandling = NullValueHandling.Ignore)] + public DiscordComponentEmoji Emoji { get; internal set; } + + public DiscordSelectComponentOption(string label, string value, string description = null, bool isDefault = false, DiscordComponentEmoji emoji = null) + { + this.Label = label; + this.Value = value; + this.Description = description; + this.Default = isDefault; + this.Emoji = emoji; + } +} diff --git a/DSharpPlus/Entities/Interaction/Components/Selects/DiscordSelectDefaultValue.cs b/DSharpPlus/Entities/Interaction/Components/Selects/DiscordSelectDefaultValue.cs index bc512059a8..5270ae2892 100644 --- a/DSharpPlus/Entities/Interaction/Components/Selects/DiscordSelectDefaultValue.cs +++ b/DSharpPlus/Entities/Interaction/Components/Selects/DiscordSelectDefaultValue.cs @@ -1,25 +1,25 @@ -using System; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -public class DiscordSelectDefaultValue -{ - [JsonProperty("id")] - public ulong Id { get; internal set; } - - [JsonProperty("type")] - public string Type { get; internal set; } - - public DiscordSelectDefaultValue(ulong id, DiscordSelectDefaultValueType type) - { - this.Id = id; - this.Type = type switch - { - DiscordSelectDefaultValueType.Channel => "channel", - DiscordSelectDefaultValueType.User => "user", - DiscordSelectDefaultValueType.Role => "role", - _ => throw new ArgumentOutOfRangeException(nameof(type), type, null) - }; - } -} +using System; +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +public class DiscordSelectDefaultValue +{ + [JsonProperty("id")] + public ulong Id { get; internal set; } + + [JsonProperty("type")] + public string Type { get; internal set; } + + public DiscordSelectDefaultValue(ulong id, DiscordSelectDefaultValueType type) + { + this.Id = id; + this.Type = type switch + { + DiscordSelectDefaultValueType.Channel => "channel", + DiscordSelectDefaultValueType.User => "user", + DiscordSelectDefaultValueType.Role => "role", + _ => throw new ArgumentOutOfRangeException(nameof(type), type, null) + }; + } +} diff --git a/DSharpPlus/Entities/Interaction/Components/Selects/DiscordUserSelectComponent.cs b/DSharpPlus/Entities/Interaction/Components/Selects/DiscordUserSelectComponent.cs index 3b7272bdd7..1600ad67e8 100644 --- a/DSharpPlus/Entities/Interaction/Components/Selects/DiscordUserSelectComponent.cs +++ b/DSharpPlus/Entities/Interaction/Components/Selects/DiscordUserSelectComponent.cs @@ -1,104 +1,104 @@ -using System.Collections.Generic; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// A select component that allows users to be selected. -/// -public sealed class DiscordUserSelectComponent : BaseDiscordSelectComponent -{ - [JsonProperty("default_values", NullValueHandling = NullValueHandling.Ignore)] - private readonly List defaultValues = []; - - /// - /// The default values for this component. - /// - [JsonIgnore] - public IReadOnlyList DefaultValues => this.defaultValues; - - /// - /// Adds a default user to this component. - /// - /// User to add - public DiscordUserSelectComponent AddDefaultUser(DiscordUser value) - { - DiscordSelectDefaultValue defaultValue = new(value.Id, DiscordSelectDefaultValueType.User); - this.defaultValues.Add(defaultValue); - return this; - } - - /// - /// Collections of DiscordUser to add as default values. - /// - /// Collection of DiscordUser - public DiscordUserSelectComponent AddDefaultUsers(IEnumerable values) - { - foreach (DiscordUser value in values) - { - DiscordSelectDefaultValue defaultValue = new(value.Id, DiscordSelectDefaultValueType.User); - this.defaultValues.Add(defaultValue); - } - - return this; - } - - /// - /// Adds a default user to this component. - /// - /// Id of a DiscordUser - public DiscordUserSelectComponent AddDefaultUser(ulong value) - { - DiscordSelectDefaultValue defaultValue = new(value, DiscordSelectDefaultValueType.User); - this.defaultValues.Add(defaultValue); - return this; - } - - /// - /// Collections of user ids to add as default values. - /// - /// Collection of DiscordUser ids - public DiscordUserSelectComponent AddDefaultUsers(IEnumerable values) - { - foreach (ulong value in values) - { - DiscordSelectDefaultValue defaultValue = new(value, DiscordSelectDefaultValueType.User); - this.defaultValues.Add(defaultValue); - } - - return this; - } - - /// - /// Enables this component. - /// - /// The current component. - public DiscordUserSelectComponent Enable() - { - this.Disabled = false; - return this; - } - - /// - /// Disables this component. - /// - /// The current component. - public DiscordUserSelectComponent Disable() - { - this.Disabled = true; - return this; - } - - internal DiscordUserSelectComponent() => this.Type = DiscordComponentType.UserSelect; - - /// - /// Creates a new user select component. - /// - /// The ID of this component. - /// Placeholder text that's shown when no options are selected. - /// Whether this component is disabled. - /// The minimum amount of options to be selected. - /// The maximum amount of options to be selected, up to 25. - public DiscordUserSelectComponent(string customId, string placeholder, bool disabled = false, int minOptions = 1, int maxOptions = 1) - : base(DiscordComponentType.UserSelect, customId, placeholder, disabled, minOptions, maxOptions) { } -} +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// A select component that allows users to be selected. +/// +public sealed class DiscordUserSelectComponent : BaseDiscordSelectComponent +{ + [JsonProperty("default_values", NullValueHandling = NullValueHandling.Ignore)] + private readonly List defaultValues = []; + + /// + /// The default values for this component. + /// + [JsonIgnore] + public IReadOnlyList DefaultValues => this.defaultValues; + + /// + /// Adds a default user to this component. + /// + /// User to add + public DiscordUserSelectComponent AddDefaultUser(DiscordUser value) + { + DiscordSelectDefaultValue defaultValue = new(value.Id, DiscordSelectDefaultValueType.User); + this.defaultValues.Add(defaultValue); + return this; + } + + /// + /// Collections of DiscordUser to add as default values. + /// + /// Collection of DiscordUser + public DiscordUserSelectComponent AddDefaultUsers(IEnumerable values) + { + foreach (DiscordUser value in values) + { + DiscordSelectDefaultValue defaultValue = new(value.Id, DiscordSelectDefaultValueType.User); + this.defaultValues.Add(defaultValue); + } + + return this; + } + + /// + /// Adds a default user to this component. + /// + /// Id of a DiscordUser + public DiscordUserSelectComponent AddDefaultUser(ulong value) + { + DiscordSelectDefaultValue defaultValue = new(value, DiscordSelectDefaultValueType.User); + this.defaultValues.Add(defaultValue); + return this; + } + + /// + /// Collections of user ids to add as default values. + /// + /// Collection of DiscordUser ids + public DiscordUserSelectComponent AddDefaultUsers(IEnumerable values) + { + foreach (ulong value in values) + { + DiscordSelectDefaultValue defaultValue = new(value, DiscordSelectDefaultValueType.User); + this.defaultValues.Add(defaultValue); + } + + return this; + } + + /// + /// Enables this component. + /// + /// The current component. + public DiscordUserSelectComponent Enable() + { + this.Disabled = false; + return this; + } + + /// + /// Disables this component. + /// + /// The current component. + public DiscordUserSelectComponent Disable() + { + this.Disabled = true; + return this; + } + + internal DiscordUserSelectComponent() => this.Type = DiscordComponentType.UserSelect; + + /// + /// Creates a new user select component. + /// + /// The ID of this component. + /// Placeholder text that's shown when no options are selected. + /// Whether this component is disabled. + /// The minimum amount of options to be selected. + /// The maximum amount of options to be selected, up to 25. + public DiscordUserSelectComponent(string customId, string placeholder, bool disabled = false, int minOptions = 1, int maxOptions = 1) + : base(DiscordComponentType.UserSelect, customId, placeholder, disabled, minOptions, maxOptions) { } +} diff --git a/DSharpPlus/Entities/Interaction/DiscordFollowupMessageBuilder.cs b/DSharpPlus/Entities/Interaction/DiscordFollowupMessageBuilder.cs index 0a49c9b5a2..d333998695 100644 --- a/DSharpPlus/Entities/Interaction/DiscordFollowupMessageBuilder.cs +++ b/DSharpPlus/Entities/Interaction/DiscordFollowupMessageBuilder.cs @@ -1,71 +1,71 @@ -using System; -using System.Linq; - -namespace DSharpPlus.Entities; - -/// -/// Constructs a followup message to an interaction. -/// -public sealed class DiscordFollowupMessageBuilder : BaseDiscordMessageBuilder -{ - /// - /// Whether this followup message should be ephemeral. - /// - public bool IsEphemeral - { - get => this.Flags.HasFlag(DiscordMessageFlags.Ephemeral); - set - { - if (value) - { - this.Flags |= DiscordMessageFlags.Ephemeral; - } - else - { - this.Flags &= ~DiscordMessageFlags.Ephemeral; - } - } - } - - /// - /// Constructs a new followup message builder - /// - public DiscordFollowupMessageBuilder() { } - - public DiscordFollowupMessageBuilder(DiscordFollowupMessageBuilder builder) : base(builder) => this.IsEphemeral = builder.IsEphemeral; - - /// - /// Copies the common properties from the passed builder. - /// - /// The builder to copy. - public DiscordFollowupMessageBuilder(IDiscordMessageBuilder builder) : base(builder) { } - - /// - /// Sets the followup message to be ephemeral. - /// - /// Ephemeral. - /// The builder to chain calls with. - public DiscordFollowupMessageBuilder AsEphemeral(bool ephemeral = true) - { - this.IsEphemeral = ephemeral; - return this; - } - - /// - /// Allows for clearing the Followup Message builder so that it can be used again to send a new message. - /// - public override void Clear() - { - this.IsEphemeral = false; - - base.Clear(); - } - - internal void Validate() - { - if (!this.Flags.HasFlag(DiscordMessageFlags.IsComponentsV2) && this.Files?.Count == 0 && string.IsNullOrEmpty(this.Content) && !this.Embeds.Any()) - { - throw new ArgumentException("You must specify content, an embed, or at least one file."); - } - } -} +using System; +using System.Linq; + +namespace DSharpPlus.Entities; + +/// +/// Constructs a followup message to an interaction. +/// +public sealed class DiscordFollowupMessageBuilder : BaseDiscordMessageBuilder +{ + /// + /// Whether this followup message should be ephemeral. + /// + public bool IsEphemeral + { + get => this.Flags.HasFlag(DiscordMessageFlags.Ephemeral); + set + { + if (value) + { + this.Flags |= DiscordMessageFlags.Ephemeral; + } + else + { + this.Flags &= ~DiscordMessageFlags.Ephemeral; + } + } + } + + /// + /// Constructs a new followup message builder + /// + public DiscordFollowupMessageBuilder() { } + + public DiscordFollowupMessageBuilder(DiscordFollowupMessageBuilder builder) : base(builder) => this.IsEphemeral = builder.IsEphemeral; + + /// + /// Copies the common properties from the passed builder. + /// + /// The builder to copy. + public DiscordFollowupMessageBuilder(IDiscordMessageBuilder builder) : base(builder) { } + + /// + /// Sets the followup message to be ephemeral. + /// + /// Ephemeral. + /// The builder to chain calls with. + public DiscordFollowupMessageBuilder AsEphemeral(bool ephemeral = true) + { + this.IsEphemeral = ephemeral; + return this; + } + + /// + /// Allows for clearing the Followup Message builder so that it can be used again to send a new message. + /// + public override void Clear() + { + this.IsEphemeral = false; + + base.Clear(); + } + + internal void Validate() + { + if (!this.Flags.HasFlag(DiscordMessageFlags.IsComponentsV2) && this.Files?.Count == 0 && string.IsNullOrEmpty(this.Content) && !this.Embeds.Any()) + { + throw new ArgumentException("You must specify content, an embed, or at least one file."); + } + } +} diff --git a/DSharpPlus/Entities/Interaction/DiscordInteraction.cs b/DSharpPlus/Entities/Interaction/DiscordInteraction.cs index 28c4cfe402..07f9d5e557 100644 --- a/DSharpPlus/Entities/Interaction/DiscordInteraction.cs +++ b/DSharpPlus/Entities/Interaction/DiscordInteraction.cs @@ -1,285 +1,285 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents an interaction that was invoked. -/// -public class DiscordInteraction : SnowflakeObject -{ - /// - /// Gets the response state of the interaction. - /// - [JsonIgnore] - public DiscordInteractionResponseState ResponseState { get; protected set; } - - /// - /// Gets the type of interaction invoked. - /// - [JsonProperty("type")] - public DiscordInteractionType Type { get; internal set; } - - /// - /// Gets the command data for this interaction. - /// - [JsonProperty("data", NullValueHandling = NullValueHandling.Ignore)] - public DiscordInteractionData Data { get; internal set; } - - /// - /// Gets the Id of the guild that invoked this interaction. Returns null if interaction was triggered in a private channel. - /// - [JsonIgnore] - public ulong? GuildId { get; internal set; } - - /// - /// Gets the guild that invoked this interaction. Returns null if interaction was triggered in a private channel. - /// - [JsonIgnore] - public DiscordGuild? Guild - => this.GuildId.HasValue ? (this.Discord as DiscordClient).InternalGetCachedGuild(this.GuildId) : null; - - /// - /// Gets the Id of the channel that invoked this interaction. - /// - [JsonProperty("channel_id")] - public ulong ChannelId { get; internal set; } - - /// - /// Gets the channel that invoked this interaction. - /// - [JsonIgnore] - public DiscordChannel Channel - { - get - { - DiscordClient client = (this.Discord as DiscordClient)!; - - DiscordChannel? cachedChannel = client.InternalGetCachedChannel(this.ChannelId, this.GuildId) ?? - client.InternalGetCachedThread(this.ChannelId, this.GuildId); - - if (cachedChannel is not null) - { - return cachedChannel; - } - - return new DiscordDmChannel - { - Id = this.ChannelId, - Type = DiscordChannelType.Private, - Discord = this.Discord, - Recipients = new DiscordUser[] { this.User } - }; - } - } - - /// - /// Gets the user that invoked this interaction. - /// This can be cast to a if created in a guild. - /// - [JsonIgnore] - public DiscordUser User { get; internal set; } - - /// - /// Gets the continuation token for responding to this interaction. - /// - [JsonProperty("token")] - public string Token { get; internal set; } - - /// - /// Gets the version number for this interaction type. - /// - [JsonProperty("version")] - public int Version { get; internal set; } - - /// - /// Gets the ID of the application that created this interaction. - /// - [JsonProperty("application_id")] - public ulong ApplicationId { get; internal set; } - - /// - /// The message this interaction was created with, if any. - /// - [JsonProperty("message")] - public DiscordMessage? Message { get; set; } - - /// - /// Gets the locale of the user that invoked this interaction. - /// - [JsonProperty("locale")] - public string? Locale { get; internal set; } - - /// - /// Gets the guild's preferred locale, if invoked in a guild. - /// - [JsonProperty("guild_locale")] - public string? GuildLocale { get; internal set; } - - /// - /// The permissions allowed to the application for the given context. - /// - /// - /// For guilds, this will be the bot's permissions. For group DMs, this is `ATTACH_FILES`, `EMBED_LINKS`, and `MENTION_EVERYONE`. - /// In the context of the bot's DM, it also includes `USE_EXTERNAL_EMOJI`. - /// - [JsonProperty("app_permissions", NullValueHandling = NullValueHandling.Ignore)] - public DiscordPermissions AppPermissions { get; internal set; } - - /// - /// Gets the interactions that authorized the interaction. - /// - /// This dictionary contains the following: - /// - /// - /// If the interaction is installed to a user, a key of and a value of the user's ID. - /// - /// - /// If the interaction is installed to a guild, a key of and a value of the guild's ID. - /// - /// - /// IF the interaction was sent from a guild context, the above holds true, otherwise the ID is 0. - /// - /// - /// - /// - /// - /// - [JsonIgnore] - public IReadOnlyDictionary AuthorizingIntegrationOwners => this.authorizingIntegrationOwners; - -#pragma warning disable CS0649 // Field is never assigned to, and will always have its default value null - // Justification: Used by JSON.NET - [JsonProperty("authorizing_integration_owners", NullValueHandling = NullValueHandling.Ignore)] - private readonly Dictionary authorizingIntegrationOwners; -#pragma warning restore CS0649 - - /// - /// Represents the context in which the interaction was executed in - /// - [JsonProperty("context", NullValueHandling = NullValueHandling.Ignore)] - public DiscordInteractionContextType? Context { get; internal set; } - - /// - /// Represents the maximum allowed size per-attachment for the given interaction context. - /// - /// - /// For guilds, this is MAX(guild_boost_limit, user_nitro_limit, standard_limit), and in all other contexts it is simply MAX(user_nitro_limit, standard_upload_limit) - /// - [JsonProperty("attachment_size_limit", NullValueHandling = NullValueHandling.Ignore)] - public int AttachmentSizeLimit { get; internal set; } - - /// - /// Creates a response to this interaction. - /// - /// The type of the response. - /// The data, if any, to send. - public virtual async Task CreateResponseAsync(DiscordInteractionResponseType type, DiscordInteractionResponseBuilder builder = null) - { - if (this.ResponseState is not DiscordInteractionResponseState.Unacknowledged) - { - throw new InvalidOperationException("A response has already been made to this interaction."); - } - - this.ResponseState = type == DiscordInteractionResponseType.DeferredChannelMessageWithSource - ? DiscordInteractionResponseState.Deferred - : DiscordInteractionResponseState.Replied; - - await this.Discord.ApiClient.CreateInteractionResponseAsync(this.Id, this.Token, type, builder); - } - - /// - /// Creates a deferred response to this interaction. - /// - /// Whether the response should be ephemeral. - public Task DeferAsync(bool ephemeral = false) => CreateResponseAsync( - DiscordInteractionResponseType.DeferredChannelMessageWithSource, - new DiscordInteractionResponseBuilder().AsEphemeral(ephemeral)); - - /// - /// Gets the original interaction response. - /// - /// The original message that was sent. - public async Task GetOriginalResponseAsync() => - await this.Discord.ApiClient.GetOriginalInteractionResponseAsync(this.Discord.CurrentApplication.Id, this.Token); - - /// - /// Edits the original interaction response. - /// - /// The webhook builder. - /// Attached files to keep. - /// The edited. - public async Task EditOriginalResponseAsync(DiscordWebhookBuilder builder, IEnumerable attachments = default) - { - builder.Validate(isInteractionResponse: true); - - return this.ResponseState is DiscordInteractionResponseState.Unacknowledged - ? throw new InvalidOperationException("A response has not been made to this interaction.") - : await this.Discord.ApiClient.EditOriginalInteractionResponseAsync(this.Discord.CurrentApplication.Id, this.Token, builder, attachments); - } - - /// - /// Deletes the original interaction response. - /// > - public async Task DeleteOriginalResponseAsync() - { - if (this.ResponseState is DiscordInteractionResponseState.Unacknowledged) - { - throw new InvalidOperationException("A response has not been made to this interaction."); - } - - await this.Discord.ApiClient.DeleteOriginalInteractionResponseAsync(this.Discord.CurrentApplication.Id, this.Token); - } - - /// - /// Creates a follow up message to this interaction. - /// - /// The webhook builder. - /// The created. - public async Task CreateFollowupMessageAsync(DiscordFollowupMessageBuilder builder) - { - builder.Validate(); - - this.ResponseState = DiscordInteractionResponseState.Replied; - - return await this.Discord.ApiClient.CreateFollowupMessageAsync(this.Discord.CurrentApplication.Id, this.Token, builder); - } - - /// - /// Gets a follow up message. - /// - /// The id of the follow up message. - public async Task GetFollowupMessageAsync(ulong messageId) => this.ResponseState is not DiscordInteractionResponseState.Replied - ? throw new InvalidOperationException("A response has not been made to this interaction.") - : await this.Discord.ApiClient.GetFollowupMessageAsync(this.Discord.CurrentApplication.Id, this.Token, messageId); - - /// - /// Edits a follow up message. - /// - /// The id of the follow up message. - /// The webhook builder. - /// Attached files to keep. - /// The edited. - public async Task EditFollowupMessageAsync(ulong messageId, DiscordWebhookBuilder builder, IEnumerable attachments = default) - { - builder.Validate(isFollowup: true); - - return await this.Discord.ApiClient.EditFollowupMessageAsync(this.Discord.CurrentApplication.Id, this.Token, messageId, builder, attachments); - } - - /// - /// Deletes a follow up message. - /// - /// The id of the follow up message. - public async Task DeleteFollowupMessageAsync(ulong messageId) - { - if (this.ResponseState is not DiscordInteractionResponseState.Replied) - { - throw new InvalidOperationException("A response has not been made to this interaction."); - } - - await this.Discord.ApiClient.DeleteFollowupMessageAsync(this.Discord.CurrentApplication.Id, this.Token, messageId); - } -} +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Represents an interaction that was invoked. +/// +public class DiscordInteraction : SnowflakeObject +{ + /// + /// Gets the response state of the interaction. + /// + [JsonIgnore] + public DiscordInteractionResponseState ResponseState { get; protected set; } + + /// + /// Gets the type of interaction invoked. + /// + [JsonProperty("type")] + public DiscordInteractionType Type { get; internal set; } + + /// + /// Gets the command data for this interaction. + /// + [JsonProperty("data", NullValueHandling = NullValueHandling.Ignore)] + public DiscordInteractionData Data { get; internal set; } + + /// + /// Gets the Id of the guild that invoked this interaction. Returns null if interaction was triggered in a private channel. + /// + [JsonIgnore] + public ulong? GuildId { get; internal set; } + + /// + /// Gets the guild that invoked this interaction. Returns null if interaction was triggered in a private channel. + /// + [JsonIgnore] + public DiscordGuild? Guild + => this.GuildId.HasValue ? (this.Discord as DiscordClient).InternalGetCachedGuild(this.GuildId) : null; + + /// + /// Gets the Id of the channel that invoked this interaction. + /// + [JsonProperty("channel_id")] + public ulong ChannelId { get; internal set; } + + /// + /// Gets the channel that invoked this interaction. + /// + [JsonIgnore] + public DiscordChannel Channel + { + get + { + DiscordClient client = (this.Discord as DiscordClient)!; + + DiscordChannel? cachedChannel = client.InternalGetCachedChannel(this.ChannelId, this.GuildId) ?? + client.InternalGetCachedThread(this.ChannelId, this.GuildId); + + if (cachedChannel is not null) + { + return cachedChannel; + } + + return new DiscordDmChannel + { + Id = this.ChannelId, + Type = DiscordChannelType.Private, + Discord = this.Discord, + Recipients = new DiscordUser[] { this.User } + }; + } + } + + /// + /// Gets the user that invoked this interaction. + /// This can be cast to a if created in a guild. + /// + [JsonIgnore] + public DiscordUser User { get; internal set; } + + /// + /// Gets the continuation token for responding to this interaction. + /// + [JsonProperty("token")] + public string Token { get; internal set; } + + /// + /// Gets the version number for this interaction type. + /// + [JsonProperty("version")] + public int Version { get; internal set; } + + /// + /// Gets the ID of the application that created this interaction. + /// + [JsonProperty("application_id")] + public ulong ApplicationId { get; internal set; } + + /// + /// The message this interaction was created with, if any. + /// + [JsonProperty("message")] + public DiscordMessage? Message { get; set; } + + /// + /// Gets the locale of the user that invoked this interaction. + /// + [JsonProperty("locale")] + public string? Locale { get; internal set; } + + /// + /// Gets the guild's preferred locale, if invoked in a guild. + /// + [JsonProperty("guild_locale")] + public string? GuildLocale { get; internal set; } + + /// + /// The permissions allowed to the application for the given context. + /// + /// + /// For guilds, this will be the bot's permissions. For group DMs, this is `ATTACH_FILES`, `EMBED_LINKS`, and `MENTION_EVERYONE`. + /// In the context of the bot's DM, it also includes `USE_EXTERNAL_EMOJI`. + /// + [JsonProperty("app_permissions", NullValueHandling = NullValueHandling.Ignore)] + public DiscordPermissions AppPermissions { get; internal set; } + + /// + /// Gets the interactions that authorized the interaction. + /// + /// This dictionary contains the following: + /// + /// + /// If the interaction is installed to a user, a key of and a value of the user's ID. + /// + /// + /// If the interaction is installed to a guild, a key of and a value of the guild's ID. + /// + /// + /// IF the interaction was sent from a guild context, the above holds true, otherwise the ID is 0. + /// + /// + /// + /// + /// + /// + [JsonIgnore] + public IReadOnlyDictionary AuthorizingIntegrationOwners => this.authorizingIntegrationOwners; + +#pragma warning disable CS0649 // Field is never assigned to, and will always have its default value null + // Justification: Used by JSON.NET + [JsonProperty("authorizing_integration_owners", NullValueHandling = NullValueHandling.Ignore)] + private readonly Dictionary authorizingIntegrationOwners; +#pragma warning restore CS0649 + + /// + /// Represents the context in which the interaction was executed in + /// + [JsonProperty("context", NullValueHandling = NullValueHandling.Ignore)] + public DiscordInteractionContextType? Context { get; internal set; } + + /// + /// Represents the maximum allowed size per-attachment for the given interaction context. + /// + /// + /// For guilds, this is MAX(guild_boost_limit, user_nitro_limit, standard_limit), and in all other contexts it is simply MAX(user_nitro_limit, standard_upload_limit) + /// + [JsonProperty("attachment_size_limit", NullValueHandling = NullValueHandling.Ignore)] + public int AttachmentSizeLimit { get; internal set; } + + /// + /// Creates a response to this interaction. + /// + /// The type of the response. + /// The data, if any, to send. + public virtual async Task CreateResponseAsync(DiscordInteractionResponseType type, DiscordInteractionResponseBuilder builder = null) + { + if (this.ResponseState is not DiscordInteractionResponseState.Unacknowledged) + { + throw new InvalidOperationException("A response has already been made to this interaction."); + } + + this.ResponseState = type == DiscordInteractionResponseType.DeferredChannelMessageWithSource + ? DiscordInteractionResponseState.Deferred + : DiscordInteractionResponseState.Replied; + + await this.Discord.ApiClient.CreateInteractionResponseAsync(this.Id, this.Token, type, builder); + } + + /// + /// Creates a deferred response to this interaction. + /// + /// Whether the response should be ephemeral. + public Task DeferAsync(bool ephemeral = false) => CreateResponseAsync( + DiscordInteractionResponseType.DeferredChannelMessageWithSource, + new DiscordInteractionResponseBuilder().AsEphemeral(ephemeral)); + + /// + /// Gets the original interaction response. + /// + /// The original message that was sent. + public async Task GetOriginalResponseAsync() => + await this.Discord.ApiClient.GetOriginalInteractionResponseAsync(this.Discord.CurrentApplication.Id, this.Token); + + /// + /// Edits the original interaction response. + /// + /// The webhook builder. + /// Attached files to keep. + /// The edited. + public async Task EditOriginalResponseAsync(DiscordWebhookBuilder builder, IEnumerable attachments = default) + { + builder.Validate(isInteractionResponse: true); + + return this.ResponseState is DiscordInteractionResponseState.Unacknowledged + ? throw new InvalidOperationException("A response has not been made to this interaction.") + : await this.Discord.ApiClient.EditOriginalInteractionResponseAsync(this.Discord.CurrentApplication.Id, this.Token, builder, attachments); + } + + /// + /// Deletes the original interaction response. + /// > + public async Task DeleteOriginalResponseAsync() + { + if (this.ResponseState is DiscordInteractionResponseState.Unacknowledged) + { + throw new InvalidOperationException("A response has not been made to this interaction."); + } + + await this.Discord.ApiClient.DeleteOriginalInteractionResponseAsync(this.Discord.CurrentApplication.Id, this.Token); + } + + /// + /// Creates a follow up message to this interaction. + /// + /// The webhook builder. + /// The created. + public async Task CreateFollowupMessageAsync(DiscordFollowupMessageBuilder builder) + { + builder.Validate(); + + this.ResponseState = DiscordInteractionResponseState.Replied; + + return await this.Discord.ApiClient.CreateFollowupMessageAsync(this.Discord.CurrentApplication.Id, this.Token, builder); + } + + /// + /// Gets a follow up message. + /// + /// The id of the follow up message. + public async Task GetFollowupMessageAsync(ulong messageId) => this.ResponseState is not DiscordInteractionResponseState.Replied + ? throw new InvalidOperationException("A response has not been made to this interaction.") + : await this.Discord.ApiClient.GetFollowupMessageAsync(this.Discord.CurrentApplication.Id, this.Token, messageId); + + /// + /// Edits a follow up message. + /// + /// The id of the follow up message. + /// The webhook builder. + /// Attached files to keep. + /// The edited. + public async Task EditFollowupMessageAsync(ulong messageId, DiscordWebhookBuilder builder, IEnumerable attachments = default) + { + builder.Validate(isFollowup: true); + + return await this.Discord.ApiClient.EditFollowupMessageAsync(this.Discord.CurrentApplication.Id, this.Token, messageId, builder, attachments); + } + + /// + /// Deletes a follow up message. + /// + /// The id of the follow up message. + public async Task DeleteFollowupMessageAsync(ulong messageId) + { + if (this.ResponseState is not DiscordInteractionResponseState.Replied) + { + throw new InvalidOperationException("A response has not been made to this interaction."); + } + + await this.Discord.ApiClient.DeleteFollowupMessageAsync(this.Discord.CurrentApplication.Id, this.Token, messageId); + } +} diff --git a/DSharpPlus/Entities/Interaction/DiscordInteractionApplicationCommandCallbackData.cs b/DSharpPlus/Entities/Interaction/DiscordInteractionApplicationCommandCallbackData.cs index 5f4ed4dd7d..6d2f9c5ceb 100644 --- a/DSharpPlus/Entities/Interaction/DiscordInteractionApplicationCommandCallbackData.cs +++ b/DSharpPlus/Entities/Interaction/DiscordInteractionApplicationCommandCallbackData.cs @@ -1,38 +1,38 @@ -using System.Collections.Generic; -using DSharpPlus.Net.Abstractions; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -internal class DiscordInteractionApplicationCommandCallbackData -{ - [JsonProperty("tts", NullValueHandling = NullValueHandling.Ignore)] - public bool? IsTTS { get; internal set; } - - [JsonProperty("custom_id", NullValueHandling = NullValueHandling.Ignore)] - public string CustomId { get; internal set; } - - [JsonProperty("title", NullValueHandling = NullValueHandling.Ignore)] - public string Title { get; internal set; } - - [JsonProperty("content", NullValueHandling = NullValueHandling.Ignore)] - public string Content { get; internal set; } - - [JsonProperty("embeds", NullValueHandling = NullValueHandling.Ignore)] - public IEnumerable Embeds { get; internal set; } - - [JsonProperty("allowed_mentions", NullValueHandling = NullValueHandling.Ignore)] - public DiscordMentions Mentions { get; internal set; } - - [JsonProperty("flags", NullValueHandling = NullValueHandling.Ignore)] - public DiscordMessageFlags? Flags { get; internal set; } - - [JsonProperty("components", NullValueHandling = NullValueHandling.Ignore)] - public IReadOnlyList Components { get; internal set; } - - [JsonProperty("choices")] - public IReadOnlyList Choices { get; internal set; } - - [JsonProperty("poll", NullValueHandling = NullValueHandling.Ignore)] - public PollCreatePayload? Poll { get; internal set; } -} +using System.Collections.Generic; +using DSharpPlus.Net.Abstractions; +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +internal class DiscordInteractionApplicationCommandCallbackData +{ + [JsonProperty("tts", NullValueHandling = NullValueHandling.Ignore)] + public bool? IsTTS { get; internal set; } + + [JsonProperty("custom_id", NullValueHandling = NullValueHandling.Ignore)] + public string CustomId { get; internal set; } + + [JsonProperty("title", NullValueHandling = NullValueHandling.Ignore)] + public string Title { get; internal set; } + + [JsonProperty("content", NullValueHandling = NullValueHandling.Ignore)] + public string Content { get; internal set; } + + [JsonProperty("embeds", NullValueHandling = NullValueHandling.Ignore)] + public IEnumerable Embeds { get; internal set; } + + [JsonProperty("allowed_mentions", NullValueHandling = NullValueHandling.Ignore)] + public DiscordMentions Mentions { get; internal set; } + + [JsonProperty("flags", NullValueHandling = NullValueHandling.Ignore)] + public DiscordMessageFlags? Flags { get; internal set; } + + [JsonProperty("components", NullValueHandling = NullValueHandling.Ignore)] + public IReadOnlyList Components { get; internal set; } + + [JsonProperty("choices")] + public IReadOnlyList Choices { get; internal set; } + + [JsonProperty("poll", NullValueHandling = NullValueHandling.Ignore)] + public PollCreatePayload? Poll { get; internal set; } +} diff --git a/DSharpPlus/Entities/Interaction/DiscordInteractionData.cs b/DSharpPlus/Entities/Interaction/DiscordInteractionData.cs index 2abd634e5b..3073a2fad7 100644 --- a/DSharpPlus/Entities/Interaction/DiscordInteractionData.cs +++ b/DSharpPlus/Entities/Interaction/DiscordInteractionData.cs @@ -1,102 +1,102 @@ -using System.Collections.Generic; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents the inner data payload of a . -/// -public sealed class DiscordInteractionData : SnowflakeObject -{ - /// - /// Gets the name of the invoked interaction. - /// - [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] - public string Name { get; internal set; } - - /// - /// Gets the parameters and values of the invoked interaction. - /// - [JsonProperty("options", NullValueHandling = NullValueHandling.Ignore)] - public IReadOnlyList Options { get; internal set; } - - /// - /// Gets the Discord snowflake objects resolved from this interaction's arguments. - /// - [JsonProperty("resolved", NullValueHandling = NullValueHandling.Ignore)] - public DiscordInteractionResolvedCollection Resolved { get; internal set; } - - /// - /// The Id of the component that invoked this interaction, or the Id of the modal the interaction was spawned from. - /// - [JsonProperty("custom_id", NullValueHandling = NullValueHandling.Ignore)] - public string CustomId { get; internal set; } - - /// - /// The title of the modal, if applicable. - /// - [JsonProperty("title", NullValueHandling = NullValueHandling.Ignore)] - public string Title { get; internal set; } - - /// - /// Components on this interaction. Only applies to modal interactions. - /// - public IReadOnlyList? Components => this.components; - - [JsonProperty("components", NullValueHandling = NullValueHandling.Ignore)] - internal List? components; - - /// - /// Gets all text input components on this interaction. - /// - public IReadOnlyList? TextInputComponents - { - get - { - if (this.Components is null) - { - return null; - } - - List components = []; - - foreach (DiscordComponent component in this.Components) - { - if (component is DiscordActionRowComponent actionRowComponent) - { - foreach (DiscordComponent subComponent in actionRowComponent.Components) - { - if (subComponent is DiscordTextInputComponent filteredComponent) - { - components.Add(filteredComponent); - } - } - } - else if (component is DiscordTextInputComponent filteredComponent) - { - components.Add(filteredComponent); - } - } - - return components; - } - } - - /// - /// The Id of the target. Applicable for context menus. - /// - [JsonProperty("target_id", NullValueHandling = NullValueHandling.Ignore)] - internal ulong? Target { get; set; } - - /// - /// The type of component that invoked this interaction, if applicable. - /// - [JsonProperty("component_type", NullValueHandling = NullValueHandling.Ignore)] - public DiscordComponentType ComponentType { get; internal set; } - - [JsonProperty("values", NullValueHandling = NullValueHandling.Ignore)] - public string[] Values { get; internal set; } = []; - - [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] - public DiscordApplicationCommandType Type { get; internal set; } -} +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Represents the inner data payload of a . +/// +public sealed class DiscordInteractionData : SnowflakeObject +{ + /// + /// Gets the name of the invoked interaction. + /// + [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] + public string Name { get; internal set; } + + /// + /// Gets the parameters and values of the invoked interaction. + /// + [JsonProperty("options", NullValueHandling = NullValueHandling.Ignore)] + public IReadOnlyList Options { get; internal set; } + + /// + /// Gets the Discord snowflake objects resolved from this interaction's arguments. + /// + [JsonProperty("resolved", NullValueHandling = NullValueHandling.Ignore)] + public DiscordInteractionResolvedCollection Resolved { get; internal set; } + + /// + /// The Id of the component that invoked this interaction, or the Id of the modal the interaction was spawned from. + /// + [JsonProperty("custom_id", NullValueHandling = NullValueHandling.Ignore)] + public string CustomId { get; internal set; } + + /// + /// The title of the modal, if applicable. + /// + [JsonProperty("title", NullValueHandling = NullValueHandling.Ignore)] + public string Title { get; internal set; } + + /// + /// Components on this interaction. Only applies to modal interactions. + /// + public IReadOnlyList? Components => this.components; + + [JsonProperty("components", NullValueHandling = NullValueHandling.Ignore)] + internal List? components; + + /// + /// Gets all text input components on this interaction. + /// + public IReadOnlyList? TextInputComponents + { + get + { + if (this.Components is null) + { + return null; + } + + List components = []; + + foreach (DiscordComponent component in this.Components) + { + if (component is DiscordActionRowComponent actionRowComponent) + { + foreach (DiscordComponent subComponent in actionRowComponent.Components) + { + if (subComponent is DiscordTextInputComponent filteredComponent) + { + components.Add(filteredComponent); + } + } + } + else if (component is DiscordTextInputComponent filteredComponent) + { + components.Add(filteredComponent); + } + } + + return components; + } + } + + /// + /// The Id of the target. Applicable for context menus. + /// + [JsonProperty("target_id", NullValueHandling = NullValueHandling.Ignore)] + internal ulong? Target { get; set; } + + /// + /// The type of component that invoked this interaction, if applicable. + /// + [JsonProperty("component_type", NullValueHandling = NullValueHandling.Ignore)] + public DiscordComponentType ComponentType { get; internal set; } + + [JsonProperty("values", NullValueHandling = NullValueHandling.Ignore)] + public string[] Values { get; internal set; } = []; + + [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] + public DiscordApplicationCommandType Type { get; internal set; } +} diff --git a/DSharpPlus/Entities/Interaction/DiscordInteractionDataOption.cs b/DSharpPlus/Entities/Interaction/DiscordInteractionDataOption.cs index 82cf8917ec..fedaf87771 100644 --- a/DSharpPlus/Entities/Interaction/DiscordInteractionDataOption.cs +++ b/DSharpPlus/Entities/Interaction/DiscordInteractionDataOption.cs @@ -1,67 +1,67 @@ -using System.Collections.Generic; -using System.Globalization; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents parameters for interaction commands. -/// -public sealed class DiscordInteractionDataOption -{ - /// - /// Gets the name of this interaction parameter. - /// - [JsonProperty("name")] - public string Name { get; internal set; } - - /// - /// Gets the type of this interaction parameter. - /// - [JsonProperty("type")] - public DiscordApplicationCommandOptionType Type { get; internal set; } - - /// - /// If this is an autocomplete option: Whether this option is currently active. - /// - [JsonProperty("focused")] - public bool Focused { get; internal set; } - - /// - /// Gets the raw value of this interaction parameter. - /// - [JsonProperty("value")] - public string? RawValue { get; internal set; } - - /// - /// Gets the value of this interaction parameter. - /// This can be cast to a , , , or depending on the - /// - [JsonIgnore] - public object? Value - { - get - { - return this.Type switch - { - _ when this.RawValue is null => null, - DiscordApplicationCommandOptionType.Boolean => bool.Parse(this.RawValue), - DiscordApplicationCommandOptionType.Integer => long.Parse(this.RawValue), - DiscordApplicationCommandOptionType.String => this.RawValue, - DiscordApplicationCommandOptionType.Channel => ulong.Parse(this.RawValue), - DiscordApplicationCommandOptionType.User => ulong.Parse(this.RawValue), - DiscordApplicationCommandOptionType.Role => ulong.Parse(this.RawValue), - DiscordApplicationCommandOptionType.Mentionable => ulong.Parse(this.RawValue), - DiscordApplicationCommandOptionType.Number => double.Parse(this.RawValue, CultureInfo.InvariantCulture), - DiscordApplicationCommandOptionType.Attachment => ulong.Parse(this.RawValue, CultureInfo.InvariantCulture), - _ => this.RawValue, - }; - } - } - - /// - /// Gets the additional parameters if this parameter is a subcommand. - /// - [JsonProperty("options", NullValueHandling = NullValueHandling.Ignore)] - public IReadOnlyList Options { get; internal set; } -} +using System.Collections.Generic; +using System.Globalization; +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Represents parameters for interaction commands. +/// +public sealed class DiscordInteractionDataOption +{ + /// + /// Gets the name of this interaction parameter. + /// + [JsonProperty("name")] + public string Name { get; internal set; } + + /// + /// Gets the type of this interaction parameter. + /// + [JsonProperty("type")] + public DiscordApplicationCommandOptionType Type { get; internal set; } + + /// + /// If this is an autocomplete option: Whether this option is currently active. + /// + [JsonProperty("focused")] + public bool Focused { get; internal set; } + + /// + /// Gets the raw value of this interaction parameter. + /// + [JsonProperty("value")] + public string? RawValue { get; internal set; } + + /// + /// Gets the value of this interaction parameter. + /// This can be cast to a , , , or depending on the + /// + [JsonIgnore] + public object? Value + { + get + { + return this.Type switch + { + _ when this.RawValue is null => null, + DiscordApplicationCommandOptionType.Boolean => bool.Parse(this.RawValue), + DiscordApplicationCommandOptionType.Integer => long.Parse(this.RawValue), + DiscordApplicationCommandOptionType.String => this.RawValue, + DiscordApplicationCommandOptionType.Channel => ulong.Parse(this.RawValue), + DiscordApplicationCommandOptionType.User => ulong.Parse(this.RawValue), + DiscordApplicationCommandOptionType.Role => ulong.Parse(this.RawValue), + DiscordApplicationCommandOptionType.Mentionable => ulong.Parse(this.RawValue), + DiscordApplicationCommandOptionType.Number => double.Parse(this.RawValue, CultureInfo.InvariantCulture), + DiscordApplicationCommandOptionType.Attachment => ulong.Parse(this.RawValue, CultureInfo.InvariantCulture), + _ => this.RawValue, + }; + } + } + + /// + /// Gets the additional parameters if this parameter is a subcommand. + /// + [JsonProperty("options", NullValueHandling = NullValueHandling.Ignore)] + public IReadOnlyList Options { get; internal set; } +} diff --git a/DSharpPlus/Entities/Interaction/DiscordInteractionResolvedCollection.cs b/DSharpPlus/Entities/Interaction/DiscordInteractionResolvedCollection.cs index 1568cb02f6..a28b2fa3fa 100644 --- a/DSharpPlus/Entities/Interaction/DiscordInteractionResolvedCollection.cs +++ b/DSharpPlus/Entities/Interaction/DiscordInteractionResolvedCollection.cs @@ -1,46 +1,46 @@ -using System.Collections.Generic; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents a collection of Discord snowflake objects resolved from interaction arguments. -/// -public sealed class DiscordInteractionResolvedCollection -{ - /// - /// Gets the resolved user objects, if any. - /// - [JsonProperty("users", NullValueHandling = NullValueHandling.Ignore)] - public IReadOnlyDictionary Users { get; internal set; } - - /// - /// Gets the resolved member objects, if any. - /// - [JsonProperty("members", NullValueHandling = NullValueHandling.Ignore)] - public IReadOnlyDictionary Members { get; internal set; } - - /// - /// Gets the resolved channel objects, if any. - /// - [JsonProperty("channels", NullValueHandling = NullValueHandling.Ignore)] - public IReadOnlyDictionary Channels { get; internal set; } - - /// - /// Gets the resolved role objects, if any. - /// - [JsonProperty("roles", NullValueHandling = NullValueHandling.Ignore)] - public IReadOnlyDictionary Roles { get; internal set; } - - /// - /// Gets the resolved message objects, if any. - /// - [JsonProperty("messages", NullValueHandling = NullValueHandling.Ignore)] - public IReadOnlyDictionary Messages { get; internal set; } - - /// - /// The resolved attachment objects, if any. - /// - [JsonProperty("attachments")] - public IReadOnlyDictionary Attachments { get; internal set; } -} +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Represents a collection of Discord snowflake objects resolved from interaction arguments. +/// +public sealed class DiscordInteractionResolvedCollection +{ + /// + /// Gets the resolved user objects, if any. + /// + [JsonProperty("users", NullValueHandling = NullValueHandling.Ignore)] + public IReadOnlyDictionary Users { get; internal set; } + + /// + /// Gets the resolved member objects, if any. + /// + [JsonProperty("members", NullValueHandling = NullValueHandling.Ignore)] + public IReadOnlyDictionary Members { get; internal set; } + + /// + /// Gets the resolved channel objects, if any. + /// + [JsonProperty("channels", NullValueHandling = NullValueHandling.Ignore)] + public IReadOnlyDictionary Channels { get; internal set; } + + /// + /// Gets the resolved role objects, if any. + /// + [JsonProperty("roles", NullValueHandling = NullValueHandling.Ignore)] + public IReadOnlyDictionary Roles { get; internal set; } + + /// + /// Gets the resolved message objects, if any. + /// + [JsonProperty("messages", NullValueHandling = NullValueHandling.Ignore)] + public IReadOnlyDictionary Messages { get; internal set; } + + /// + /// The resolved attachment objects, if any. + /// + [JsonProperty("attachments")] + public IReadOnlyDictionary Attachments { get; internal set; } +} diff --git a/DSharpPlus/Entities/Interaction/DiscordInteractionResponseBuilder.cs b/DSharpPlus/Entities/Interaction/DiscordInteractionResponseBuilder.cs index 187429a60f..07b241028f 100644 --- a/DSharpPlus/Entities/Interaction/DiscordInteractionResponseBuilder.cs +++ b/DSharpPlus/Entities/Interaction/DiscordInteractionResponseBuilder.cs @@ -1,150 +1,150 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace DSharpPlus.Entities; - -/// -/// Constructs an interaction response. -/// -public sealed class DiscordInteractionResponseBuilder : BaseDiscordMessageBuilder -{ - /// - /// Whether this interaction response should be ephemeral. - /// - public bool IsEphemeral - { - get => (this.Flags & DiscordMessageFlags.Ephemeral) == DiscordMessageFlags.Ephemeral; - set => _ = value ? this.Flags |= DiscordMessageFlags.Ephemeral : this.Flags &= ~DiscordMessageFlags.Ephemeral; - } - - /// - /// The custom id to send with this interaction response. Only applicable when creating a modal. - /// - public string CustomId { get; set; } - - /// - /// The title to send with this interaction response. Only applicable when creating a modal. - /// - public string Title { get; set; } - - /// - /// The choices to send on this interaction response. Mutually exclusive with content, embed, and components. - /// - public IReadOnlyList Choices => this.choices; - private readonly List choices = []; - - /// - /// Constructs a new empty interaction response builder. - /// - public DiscordInteractionResponseBuilder() { } - - /// - /// Copies the common properties from the passed builder. - /// - /// The builder to copy. - public DiscordInteractionResponseBuilder(IDiscordMessageBuilder builder) : base(builder) { } - - /// - /// Constructs a new interaction response builder based on the passed builder. - /// - /// The builder to copy. - public DiscordInteractionResponseBuilder(DiscordInteractionResponseBuilder builder) : base(builder) - { - this.IsEphemeral = builder.IsEphemeral; - this.choices.AddRange(builder.choices); - } - - /// - /// If responding with a modal, sets the title of the modal. - /// - /// - /// - public DiscordInteractionResponseBuilder WithTitle(string title) - { - if (string.IsNullOrEmpty(title) || title.Length > 256) - { - throw new ArgumentException("Title must be between 1 and 256 characters."); - } - - this.Title = title; - return this; - } - - /// - /// If responding with a modal, sets the custom id for the modal. - /// - /// The custom id of the modal. - /// - public DiscordInteractionResponseBuilder WithCustomId(string id) - { - if (string.IsNullOrEmpty(id) || id.Length > 100) - { - throw new ArgumentException("Custom ID must be between 1 and 100 characters."); - } - - this.CustomId = id; - return this; - } - - /// - /// Adds a single auto-complete choice to the builder. - /// - /// The choice to add. - /// The current builder to chain calls with. - public DiscordInteractionResponseBuilder AddAutoCompleteChoice(DiscordAutoCompleteChoice choice) - { - if (this.choices.Count >= 25) - { - throw new ArgumentException("Maximum of 25 choices per response."); - } - - this.choices.Add(choice); - return this; - } - - /// - /// Adds auto-complete choices to the builder. - /// - /// The choices to add. - /// The current builder to chain calls with. - public DiscordInteractionResponseBuilder AddAutoCompleteChoices(IEnumerable choices) - { - if (this.choices.Count >= 25 || this.choices.Count + choices.Count() > 25) - { - throw new ArgumentException("Maximum of 25 choices per response."); - } - - this.choices.AddRange(choices); - return this; - } - - /// - /// Adds auto-complete choices to the builder. - /// - /// The choices to add. - /// The current builder to chain calls with. - public DiscordInteractionResponseBuilder AddAutoCompleteChoices(params DiscordAutoCompleteChoice[] choices) - => AddAutoCompleteChoices((IEnumerable)choices); - - /// - /// Sets the interaction response to be ephemeral. - /// - /// Ephemeral. - public DiscordInteractionResponseBuilder AsEphemeral(bool ephemeral = true) - { - this.IsEphemeral = ephemeral; - return this; - } - - /// - /// Allows for clearing the Interaction Response Builder so that it can be used again to send a new response. - /// - public override void Clear() - { - this.IsEphemeral = false; - this.choices.Clear(); - - base.Clear(); - } -} +using System; +using System.Collections.Generic; +using System.Linq; + +namespace DSharpPlus.Entities; + +/// +/// Constructs an interaction response. +/// +public sealed class DiscordInteractionResponseBuilder : BaseDiscordMessageBuilder +{ + /// + /// Whether this interaction response should be ephemeral. + /// + public bool IsEphemeral + { + get => (this.Flags & DiscordMessageFlags.Ephemeral) == DiscordMessageFlags.Ephemeral; + set => _ = value ? this.Flags |= DiscordMessageFlags.Ephemeral : this.Flags &= ~DiscordMessageFlags.Ephemeral; + } + + /// + /// The custom id to send with this interaction response. Only applicable when creating a modal. + /// + public string CustomId { get; set; } + + /// + /// The title to send with this interaction response. Only applicable when creating a modal. + /// + public string Title { get; set; } + + /// + /// The choices to send on this interaction response. Mutually exclusive with content, embed, and components. + /// + public IReadOnlyList Choices => this.choices; + private readonly List choices = []; + + /// + /// Constructs a new empty interaction response builder. + /// + public DiscordInteractionResponseBuilder() { } + + /// + /// Copies the common properties from the passed builder. + /// + /// The builder to copy. + public DiscordInteractionResponseBuilder(IDiscordMessageBuilder builder) : base(builder) { } + + /// + /// Constructs a new interaction response builder based on the passed builder. + /// + /// The builder to copy. + public DiscordInteractionResponseBuilder(DiscordInteractionResponseBuilder builder) : base(builder) + { + this.IsEphemeral = builder.IsEphemeral; + this.choices.AddRange(builder.choices); + } + + /// + /// If responding with a modal, sets the title of the modal. + /// + /// + /// + public DiscordInteractionResponseBuilder WithTitle(string title) + { + if (string.IsNullOrEmpty(title) || title.Length > 256) + { + throw new ArgumentException("Title must be between 1 and 256 characters."); + } + + this.Title = title; + return this; + } + + /// + /// If responding with a modal, sets the custom id for the modal. + /// + /// The custom id of the modal. + /// + public DiscordInteractionResponseBuilder WithCustomId(string id) + { + if (string.IsNullOrEmpty(id) || id.Length > 100) + { + throw new ArgumentException("Custom ID must be between 1 and 100 characters."); + } + + this.CustomId = id; + return this; + } + + /// + /// Adds a single auto-complete choice to the builder. + /// + /// The choice to add. + /// The current builder to chain calls with. + public DiscordInteractionResponseBuilder AddAutoCompleteChoice(DiscordAutoCompleteChoice choice) + { + if (this.choices.Count >= 25) + { + throw new ArgumentException("Maximum of 25 choices per response."); + } + + this.choices.Add(choice); + return this; + } + + /// + /// Adds auto-complete choices to the builder. + /// + /// The choices to add. + /// The current builder to chain calls with. + public DiscordInteractionResponseBuilder AddAutoCompleteChoices(IEnumerable choices) + { + if (this.choices.Count >= 25 || this.choices.Count + choices.Count() > 25) + { + throw new ArgumentException("Maximum of 25 choices per response."); + } + + this.choices.AddRange(choices); + return this; + } + + /// + /// Adds auto-complete choices to the builder. + /// + /// The choices to add. + /// The current builder to chain calls with. + public DiscordInteractionResponseBuilder AddAutoCompleteChoices(params DiscordAutoCompleteChoice[] choices) + => AddAutoCompleteChoices((IEnumerable)choices); + + /// + /// Sets the interaction response to be ephemeral. + /// + /// Ephemeral. + public DiscordInteractionResponseBuilder AsEphemeral(bool ephemeral = true) + { + this.IsEphemeral = ephemeral; + return this; + } + + /// + /// Allows for clearing the Interaction Response Builder so that it can be used again to send a new response. + /// + public override void Clear() + { + this.IsEphemeral = false; + this.choices.Clear(); + + base.Clear(); + } +} diff --git a/DSharpPlus/Entities/Interaction/DiscordInteractionResponseState.cs b/DSharpPlus/Entities/Interaction/DiscordInteractionResponseState.cs index bffc4ad077..82b1e97d32 100644 --- a/DSharpPlus/Entities/Interaction/DiscordInteractionResponseState.cs +++ b/DSharpPlus/Entities/Interaction/DiscordInteractionResponseState.cs @@ -1,23 +1,23 @@ -namespace DSharpPlus.Entities; - - -/// -/// Represents the state of an interaction regarding responding. -/// -public enum DiscordInteractionResponseState -{ - /// - /// The interaction has not been acknowledged; a response is required. - /// - Unacknowledged = 0, - - /// - /// The interaction was deferred; a followup or edit is required. - /// - Deferred = 1, - - /// - /// The interaction was replied to; no further action is required. - /// - Replied = 2, -} +namespace DSharpPlus.Entities; + + +/// +/// Represents the state of an interaction regarding responding. +/// +public enum DiscordInteractionResponseState +{ + /// + /// The interaction has not been acknowledged; a response is required. + /// + Unacknowledged = 0, + + /// + /// The interaction was deferred; a followup or edit is required. + /// + Deferred = 1, + + /// + /// The interaction was replied to; no further action is required. + /// + Replied = 2, +} diff --git a/DSharpPlus/Entities/Interaction/DiscordInteractionResponseType.cs b/DSharpPlus/Entities/Interaction/DiscordInteractionResponseType.cs index 0ff3317f61..399a406bee 100644 --- a/DSharpPlus/Entities/Interaction/DiscordInteractionResponseType.cs +++ b/DSharpPlus/Entities/Interaction/DiscordInteractionResponseType.cs @@ -1,43 +1,43 @@ -namespace DSharpPlus.Entities; - - -/// -/// Represents the type of interaction response -/// -public enum DiscordInteractionResponseType -{ - /// - /// Acknowledges a Ping. - /// - Pong = 1, - - /// - /// Responds to the interaction with a message. - /// - ChannelMessageWithSource = 4, - - /// - /// Acknowledges an interaction to edit to a response later. The user sees a "thinking" state. - /// - DeferredChannelMessageWithSource = 5, - - /// - /// Acknowledges a component interaction to allow a response later. - /// - DeferredMessageUpdate = 6, - - /// - /// Responds to a component interaction by editing the message it's attached to. - /// - UpdateMessage = 7, - - /// - /// Responds to an auto-complete request. - /// - AutoCompleteResult = 8, - - /// - /// Respond to an interaction with a modal popup. - /// - Modal = 9, -} +namespace DSharpPlus.Entities; + + +/// +/// Represents the type of interaction response +/// +public enum DiscordInteractionResponseType +{ + /// + /// Acknowledges a Ping. + /// + Pong = 1, + + /// + /// Responds to the interaction with a message. + /// + ChannelMessageWithSource = 4, + + /// + /// Acknowledges an interaction to edit to a response later. The user sees a "thinking" state. + /// + DeferredChannelMessageWithSource = 5, + + /// + /// Acknowledges a component interaction to allow a response later. + /// + DeferredMessageUpdate = 6, + + /// + /// Responds to a component interaction by editing the message it's attached to. + /// + UpdateMessage = 7, + + /// + /// Responds to an auto-complete request. + /// + AutoCompleteResult = 8, + + /// + /// Respond to an interaction with a modal popup. + /// + Modal = 9, +} diff --git a/DSharpPlus/Entities/Interaction/DiscordInteractionType.cs b/DSharpPlus/Entities/Interaction/DiscordInteractionType.cs index ca04ba1575..993cd27afd 100644 --- a/DSharpPlus/Entities/Interaction/DiscordInteractionType.cs +++ b/DSharpPlus/Entities/Interaction/DiscordInteractionType.cs @@ -1,33 +1,33 @@ -namespace DSharpPlus.Entities; - - -/// -/// Represents the type of interaction used. -/// -public enum DiscordInteractionType -{ - /// - /// Sent when registering an HTTP interaction endpoint with Discord. Must be replied to with a Pong. - /// - Ping = 1, - - /// - /// An application command. - /// - ApplicationCommand = 2, - - /// - /// A component. - /// - Component = 3, - - /// - /// An autocomplete field. - /// - AutoComplete = 4, - - /// - /// A modal was submitted. - /// - ModalSubmit = 5 -} +namespace DSharpPlus.Entities; + + +/// +/// Represents the type of interaction used. +/// +public enum DiscordInteractionType +{ + /// + /// Sent when registering an HTTP interaction endpoint with Discord. Must be replied to with a Pong. + /// + Ping = 1, + + /// + /// An application command. + /// + ApplicationCommand = 2, + + /// + /// A component. + /// + Component = 3, + + /// + /// An autocomplete field. + /// + AutoComplete = 4, + + /// + /// A modal was submitted. + /// + ModalSubmit = 5 +} diff --git a/DSharpPlus/Entities/Interaction/Metadata/DiscordInteractionMetadata.cs b/DSharpPlus/Entities/Interaction/Metadata/DiscordInteractionMetadata.cs index a643d376d7..eed64a95d8 100644 --- a/DSharpPlus/Entities/Interaction/Metadata/DiscordInteractionMetadata.cs +++ b/DSharpPlus/Entities/Interaction/Metadata/DiscordInteractionMetadata.cs @@ -1,57 +1,57 @@ -using System.Collections.Generic; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -public abstract class DiscordInteractionMetadata : SnowflakeObject -{ - /// - /// The name of the invoked command. - /// - [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] - public string? Name { get; internal set; } - - /// - /// Type of interaction. - /// - [JsonProperty("type")] - public DiscordInteractionType Type { get; internal set; } - - /// - /// Discord user object for the invoking user, if invoked in a DM. - /// - [JsonIgnore] - public DiscordUser User => this.Discord.GetCachedOrEmptyUserInternal(this.UserId); - - /// - /// User object for the invoking user, if invoked in a DM. - /// - [JsonProperty("user_id")] - internal ulong UserId { get; set; } - - /// - /// Mapping of installation contexts that the interaction was authorized for to related user or guild IDs. - /// - [JsonIgnore] - public IReadOnlyDictionary AuthorizingIntegrationOwners => this.authorizingIntegrationOwners; - - /// - /// Mapping of installation contexts that the interaction was authorized for to related user or guild IDs. - /// -#pragma warning disable CS0649 // Field is never assigned to, and will always have its default value null - // Justification: Used by JSON.NET - [JsonProperty("authorizing_integration_owners", NullValueHandling = NullValueHandling.Ignore)] - private readonly Dictionary authorizingIntegrationOwners; -#pragma warning restore CS0649 - - /// - /// ID of the original response message, present only on follow-up messages. - /// - [JsonProperty("original_response_message_id", NullValueHandling = NullValueHandling.Ignore)] - public ulong? OriginalMessageID { get; internal set; } - - /// - /// Creates a new instance of a . - /// - internal DiscordInteractionMetadata() { } -} +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +public abstract class DiscordInteractionMetadata : SnowflakeObject +{ + /// + /// The name of the invoked command. + /// + [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] + public string? Name { get; internal set; } + + /// + /// Type of interaction. + /// + [JsonProperty("type")] + public DiscordInteractionType Type { get; internal set; } + + /// + /// Discord user object for the invoking user, if invoked in a DM. + /// + [JsonIgnore] + public DiscordUser User => this.Discord.GetCachedOrEmptyUserInternal(this.UserId); + + /// + /// User object for the invoking user, if invoked in a DM. + /// + [JsonProperty("user_id")] + internal ulong UserId { get; set; } + + /// + /// Mapping of installation contexts that the interaction was authorized for to related user or guild IDs. + /// + [JsonIgnore] + public IReadOnlyDictionary AuthorizingIntegrationOwners => this.authorizingIntegrationOwners; + + /// + /// Mapping of installation contexts that the interaction was authorized for to related user or guild IDs. + /// +#pragma warning disable CS0649 // Field is never assigned to, and will always have its default value null + // Justification: Used by JSON.NET + [JsonProperty("authorizing_integration_owners", NullValueHandling = NullValueHandling.Ignore)] + private readonly Dictionary authorizingIntegrationOwners; +#pragma warning restore CS0649 + + /// + /// ID of the original response message, present only on follow-up messages. + /// + [JsonProperty("original_response_message_id", NullValueHandling = NullValueHandling.Ignore)] + public ulong? OriginalMessageID { get; internal set; } + + /// + /// Creates a new instance of a . + /// + internal DiscordInteractionMetadata() { } +} diff --git a/DSharpPlus/Entities/Interaction/Permissions/DiscordApplicationCommandPermission.cs b/DSharpPlus/Entities/Interaction/Permissions/DiscordApplicationCommandPermission.cs index 43cd34f8c5..321e547886 100644 --- a/DSharpPlus/Entities/Interaction/Permissions/DiscordApplicationCommandPermission.cs +++ b/DSharpPlus/Entities/Interaction/Permissions/DiscordApplicationCommandPermission.cs @@ -1,53 +1,53 @@ -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents a permission for a application command. -/// -public class DiscordApplicationCommandPermission -{ - /// - /// The id of the role or the user this permission is for. - /// - [JsonProperty("id")] - public ulong Id { get; internal set; } - - /// - /// Gets the type of the permission. - /// - [JsonProperty("type")] - public DiscordApplicationCommandPermissionType Type { get; internal set; } - - /// - /// Gets whether the command is enabled for the role or user. - /// - [JsonProperty("permission")] - public bool Permission { get; internal set; } - - /// - /// Represents a permission for a application command. - /// - /// The role to construct the permission for. - /// Whether the command should be enabled for the role. - public DiscordApplicationCommandPermission(DiscordRole role, bool permission) - { - this.Id = role.Id; - this.Type = DiscordApplicationCommandPermissionType.Role; - this.Permission = permission; - } - - /// - /// Represents a permission for a application command. - /// - /// The member to construct the permission for. - /// Whether the command should be enabled for the role. - public DiscordApplicationCommandPermission(DiscordMember member, bool permission) - { - this.Id = member.Id; - this.Type = DiscordApplicationCommandPermissionType.User; - this.Permission = permission; - } - - internal DiscordApplicationCommandPermission() { } -} +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Represents a permission for a application command. +/// +public class DiscordApplicationCommandPermission +{ + /// + /// The id of the role or the user this permission is for. + /// + [JsonProperty("id")] + public ulong Id { get; internal set; } + + /// + /// Gets the type of the permission. + /// + [JsonProperty("type")] + public DiscordApplicationCommandPermissionType Type { get; internal set; } + + /// + /// Gets whether the command is enabled for the role or user. + /// + [JsonProperty("permission")] + public bool Permission { get; internal set; } + + /// + /// Represents a permission for a application command. + /// + /// The role to construct the permission for. + /// Whether the command should be enabled for the role. + public DiscordApplicationCommandPermission(DiscordRole role, bool permission) + { + this.Id = role.Id; + this.Type = DiscordApplicationCommandPermissionType.Role; + this.Permission = permission; + } + + /// + /// Represents a permission for a application command. + /// + /// The member to construct the permission for. + /// Whether the command should be enabled for the role. + public DiscordApplicationCommandPermission(DiscordMember member, bool permission) + { + this.Id = member.Id; + this.Type = DiscordApplicationCommandPermissionType.User; + this.Permission = permission; + } + + internal DiscordApplicationCommandPermission() { } +} diff --git a/DSharpPlus/Entities/Interaction/Permissions/DiscordGuildApplicationCommandPermissions.cs b/DSharpPlus/Entities/Interaction/Permissions/DiscordGuildApplicationCommandPermissions.cs index 228fc6bf52..3c7af15c84 100644 --- a/DSharpPlus/Entities/Interaction/Permissions/DiscordGuildApplicationCommandPermissions.cs +++ b/DSharpPlus/Entities/Interaction/Permissions/DiscordGuildApplicationCommandPermissions.cs @@ -1,49 +1,49 @@ -using System.Collections.Generic; -using System.Linq; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents the guild permissions for a application command. -/// -public class DiscordGuildApplicationCommandPermissions : SnowflakeObject -{ - /// - /// Gets the id of the application the command belongs to. - /// - [JsonProperty("application_id")] - public ulong ApplicationId { get; internal set; } - - /// - /// Gets the id of the guild. - /// - [JsonProperty("guild_id")] - public ulong GuildId { get; internal set; } - - /// - /// Gets the guild. - /// - [JsonIgnore] - public DiscordGuild Guild - => (this.Discord as DiscordClient).InternalGetCachedGuild(this.GuildId); - - /// - /// Gets the permissions for the application command in the guild. - /// - [JsonProperty("permissions")] - public IReadOnlyList Permissions { get; internal set; } - - internal DiscordGuildApplicationCommandPermissions() { } - - /// - /// Represents the guild application command permissions for a application command. - /// - /// The id of the command. - /// The permissions for the application command. - public DiscordGuildApplicationCommandPermissions(ulong commandId, IEnumerable permissions) - { - this.Id = commandId; - this.Permissions = permissions.ToList(); - } -} +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Represents the guild permissions for a application command. +/// +public class DiscordGuildApplicationCommandPermissions : SnowflakeObject +{ + /// + /// Gets the id of the application the command belongs to. + /// + [JsonProperty("application_id")] + public ulong ApplicationId { get; internal set; } + + /// + /// Gets the id of the guild. + /// + [JsonProperty("guild_id")] + public ulong GuildId { get; internal set; } + + /// + /// Gets the guild. + /// + [JsonIgnore] + public DiscordGuild Guild + => (this.Discord as DiscordClient).InternalGetCachedGuild(this.GuildId); + + /// + /// Gets the permissions for the application command in the guild. + /// + [JsonProperty("permissions")] + public IReadOnlyList Permissions { get; internal set; } + + internal DiscordGuildApplicationCommandPermissions() { } + + /// + /// Represents the guild application command permissions for a application command. + /// + /// The id of the command. + /// The permissions for the application command. + public DiscordGuildApplicationCommandPermissions(ulong commandId, IEnumerable permissions) + { + this.Id = commandId; + this.Permissions = permissions.ToList(); + } +} diff --git a/DSharpPlus/Entities/Invite/DiscordInvite.cs b/DSharpPlus/Entities/Invite/DiscordInvite.cs index 24b9614ad9..c6b8bbf1ee 100644 --- a/DSharpPlus/Entities/Invite/DiscordInvite.cs +++ b/DSharpPlus/Entities/Invite/DiscordInvite.cs @@ -1,151 +1,151 @@ -using System; -using System.Globalization; -using System.Threading.Tasks; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents a Discord invite. -/// -public class DiscordInvite -{ - internal BaseDiscordClient Discord { get; set; } - - /// - /// Gets the invite's code. - /// - [JsonProperty("code", NullValueHandling = NullValueHandling.Ignore)] - public string Code { get; internal set; } - - /// - /// Gets the guild this invite is for. - /// - [JsonProperty("guild", NullValueHandling = NullValueHandling.Ignore)] - public DiscordInviteGuild Guild { get; internal set; } - - /// - /// Gets the channel this invite is for. - /// - [JsonProperty("channel", NullValueHandling = NullValueHandling.Ignore)] - public DiscordInviteChannel Channel { get; internal set; } - - /// - /// Gets the partial user that is currently livestreaming. - /// - [JsonProperty("target_user", NullValueHandling = NullValueHandling.Ignore)] - public DiscordUser TargetUser { get; internal set; } - - /// - /// Gets the partial embedded application to open for a voice channel. - /// - [JsonProperty("target_application", NullValueHandling = NullValueHandling.Ignore)] - public DiscordApplication TargetApplication { get; internal set; } - /// - /// Gets the target application for this invite. - /// - [JsonProperty("target_type", NullValueHandling = NullValueHandling.Ignore)] - public DiscordInviteTargetType? TargetType { get; internal set; } - - /// - /// Gets the approximate guild online member count for the invite. - /// - [JsonProperty("approximate_presence_count", NullValueHandling = NullValueHandling.Ignore)] - public int? ApproximatePresenceCount { get; internal set; } - - /// - /// Gets the approximate guild total member count for the invite. - /// - [JsonProperty("approximate_member_count")] - public int? ApproximateMemberCount { get; internal set; } - - /// - /// Gets the user who created the invite. - /// - [JsonProperty("inviter", NullValueHandling = NullValueHandling.Ignore)] - public DiscordUser Inviter { get; internal set; } - - /// - /// Gets the number of times this invite has been used. - /// - [JsonProperty("uses", NullValueHandling = NullValueHandling.Ignore)] - public int Uses { get; internal set; } - - /// - /// Gets the max number of times this invite can be used. - /// - [JsonProperty("max_uses", NullValueHandling = NullValueHandling.Ignore)] - public int MaxUses { get; internal set; } - - /// - /// Gets duration in seconds after which the invite expires. - /// - [JsonProperty("max_age", NullValueHandling = NullValueHandling.Ignore)] - public int MaxAge { get; internal set; } - - /// - /// Gets whether this invite only grants temporary membership. - /// - [JsonProperty("temporary", NullValueHandling = NullValueHandling.Ignore)] - public bool IsTemporary { get; internal set; } - - /// - /// Gets the date and time this invite was created. - /// - [JsonProperty("created_at", NullValueHandling = NullValueHandling.Ignore)] - public DateTimeOffset CreatedAt { get; internal set; } - - /// - /// Gets whether this invite is revoked. - /// - [JsonProperty("revoked", NullValueHandling = NullValueHandling.Ignore)] - public bool IsRevoked { get; internal set; } - - /// - /// Gets the expiration date of this invite. - /// - [JsonIgnore] - public DateTimeOffset? ExpiresAt - => !string.IsNullOrWhiteSpace(this.ExpiresAtRaw) && DateTimeOffset.TryParse(this.ExpiresAtRaw, CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTimeOffset dto) ? dto : null; - - [JsonProperty("expires_at", NullValueHandling = NullValueHandling.Ignore)] - internal string ExpiresAtRaw { get; set; } - - /// - /// Gets stage instance data for this invite if it is for a stage instance channel. - /// - [JsonProperty("stage_instance")] - public DiscordStageInvite StageInstance { get; internal set; } - - internal DiscordInvite() { } - - /// - /// Deletes the invite. - /// - /// Reason for audit logs. - /// - /// Thrown when the client does not have the permission or 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 async Task DeleteAsync(string reason = null) - => await this.Discord.ApiClient.DeleteInviteAsync(this.Code, reason); - - /* - * Disabled due to API restrictions. - * - * /// - * /// Accepts an invite. Not available to bot accounts. Requires "guilds.join" scope or user token. Please note that accepting these via the API will get your account unverified. - * /// - * /// - * [Obsolete("Using this method will get your account unverified.")] - * public Task AcceptAsync() - * => this.Discord.rest_client.InternalAcceptInvite(Code); - */ - - /// - /// Converts this invite into an invite link. - /// - /// A discord.gg invite link. - public override string ToString() => $"https://discord.gg/{this.Code}"; -} +using System; +using System.Globalization; +using System.Threading.Tasks; +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Represents a Discord invite. +/// +public class DiscordInvite +{ + internal BaseDiscordClient Discord { get; set; } + + /// + /// Gets the invite's code. + /// + [JsonProperty("code", NullValueHandling = NullValueHandling.Ignore)] + public string Code { get; internal set; } + + /// + /// Gets the guild this invite is for. + /// + [JsonProperty("guild", NullValueHandling = NullValueHandling.Ignore)] + public DiscordInviteGuild Guild { get; internal set; } + + /// + /// Gets the channel this invite is for. + /// + [JsonProperty("channel", NullValueHandling = NullValueHandling.Ignore)] + public DiscordInviteChannel Channel { get; internal set; } + + /// + /// Gets the partial user that is currently livestreaming. + /// + [JsonProperty("target_user", NullValueHandling = NullValueHandling.Ignore)] + public DiscordUser TargetUser { get; internal set; } + + /// + /// Gets the partial embedded application to open for a voice channel. + /// + [JsonProperty("target_application", NullValueHandling = NullValueHandling.Ignore)] + public DiscordApplication TargetApplication { get; internal set; } + /// + /// Gets the target application for this invite. + /// + [JsonProperty("target_type", NullValueHandling = NullValueHandling.Ignore)] + public DiscordInviteTargetType? TargetType { get; internal set; } + + /// + /// Gets the approximate guild online member count for the invite. + /// + [JsonProperty("approximate_presence_count", NullValueHandling = NullValueHandling.Ignore)] + public int? ApproximatePresenceCount { get; internal set; } + + /// + /// Gets the approximate guild total member count for the invite. + /// + [JsonProperty("approximate_member_count")] + public int? ApproximateMemberCount { get; internal set; } + + /// + /// Gets the user who created the invite. + /// + [JsonProperty("inviter", NullValueHandling = NullValueHandling.Ignore)] + public DiscordUser Inviter { get; internal set; } + + /// + /// Gets the number of times this invite has been used. + /// + [JsonProperty("uses", NullValueHandling = NullValueHandling.Ignore)] + public int Uses { get; internal set; } + + /// + /// Gets the max number of times this invite can be used. + /// + [JsonProperty("max_uses", NullValueHandling = NullValueHandling.Ignore)] + public int MaxUses { get; internal set; } + + /// + /// Gets duration in seconds after which the invite expires. + /// + [JsonProperty("max_age", NullValueHandling = NullValueHandling.Ignore)] + public int MaxAge { get; internal set; } + + /// + /// Gets whether this invite only grants temporary membership. + /// + [JsonProperty("temporary", NullValueHandling = NullValueHandling.Ignore)] + public bool IsTemporary { get; internal set; } + + /// + /// Gets the date and time this invite was created. + /// + [JsonProperty("created_at", NullValueHandling = NullValueHandling.Ignore)] + public DateTimeOffset CreatedAt { get; internal set; } + + /// + /// Gets whether this invite is revoked. + /// + [JsonProperty("revoked", NullValueHandling = NullValueHandling.Ignore)] + public bool IsRevoked { get; internal set; } + + /// + /// Gets the expiration date of this invite. + /// + [JsonIgnore] + public DateTimeOffset? ExpiresAt + => !string.IsNullOrWhiteSpace(this.ExpiresAtRaw) && DateTimeOffset.TryParse(this.ExpiresAtRaw, CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTimeOffset dto) ? dto : null; + + [JsonProperty("expires_at", NullValueHandling = NullValueHandling.Ignore)] + internal string ExpiresAtRaw { get; set; } + + /// + /// Gets stage instance data for this invite if it is for a stage instance channel. + /// + [JsonProperty("stage_instance")] + public DiscordStageInvite StageInstance { get; internal set; } + + internal DiscordInvite() { } + + /// + /// Deletes the invite. + /// + /// Reason for audit logs. + /// + /// Thrown when the client does not have the permission or 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 async Task DeleteAsync(string reason = null) + => await this.Discord.ApiClient.DeleteInviteAsync(this.Code, reason); + + /* + * Disabled due to API restrictions. + * + * /// + * /// Accepts an invite. Not available to bot accounts. Requires "guilds.join" scope or user token. Please note that accepting these via the API will get your account unverified. + * /// + * /// + * [Obsolete("Using this method will get your account unverified.")] + * public Task AcceptAsync() + * => this.Discord.rest_client.InternalAcceptInvite(Code); + */ + + /// + /// Converts this invite into an invite link. + /// + /// A discord.gg invite link. + public override string ToString() => $"https://discord.gg/{this.Code}"; +} diff --git a/DSharpPlus/Entities/Invite/DiscordInviteChannel.cs b/DSharpPlus/Entities/Invite/DiscordInviteChannel.cs index 17855596ca..19e8d0a7e2 100644 --- a/DSharpPlus/Entities/Invite/DiscordInviteChannel.cs +++ b/DSharpPlus/Entities/Invite/DiscordInviteChannel.cs @@ -1,23 +1,23 @@ -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents the channel to which an invite is linked. -/// -public class DiscordInviteChannel : SnowflakeObject -{ - /// - /// Gets the name of the channel. - /// - [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] - public string Name { get; internal set; } = default!; - - /// - /// Gets the type of the channel. - /// - [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] - public DiscordChannelType Type { get; internal set; } - - internal DiscordInviteChannel() { } -} +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Represents the channel to which an invite is linked. +/// +public class DiscordInviteChannel : SnowflakeObject +{ + /// + /// Gets the name of the channel. + /// + [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] + public string Name { get; internal set; } = default!; + + /// + /// Gets the type of the channel. + /// + [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] + public DiscordChannelType Type { get; internal set; } + + internal DiscordInviteChannel() { } +} diff --git a/DSharpPlus/Entities/Invite/DiscordInviteGuild.cs b/DSharpPlus/Entities/Invite/DiscordInviteGuild.cs index 386c138b9e..babed94cd3 100644 --- a/DSharpPlus/Entities/Invite/DiscordInviteGuild.cs +++ b/DSharpPlus/Entities/Invite/DiscordInviteGuild.cs @@ -1,88 +1,88 @@ -using System.Collections.Generic; -using System.Globalization; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents a guild to which the user is invited. -/// -public class DiscordInviteGuild : SnowflakeObject -{ - /// - /// Gets the name of the guild. - /// - [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] - public string Name { get; internal set; } - - /// - /// Gets the guild icon's hash. - /// - [JsonProperty("icon", NullValueHandling = NullValueHandling.Ignore)] - public string IconHash { get; internal set; } - - /// - /// Gets the guild icon's url. - /// - [JsonIgnore] - public string IconUrl - => !string.IsNullOrWhiteSpace(this.IconHash) ? $"https://cdn.discordapp.com/icons/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.IconHash}.jpg" : null; - - /// - /// Gets the hash of guild's invite splash. - /// - [JsonProperty("splash", NullValueHandling = NullValueHandling.Ignore)] - internal string SplashHash { get; set; } - - /// - /// Gets the URL of guild's invite splash. - /// - [JsonIgnore] - public string SplashUrl - => !string.IsNullOrWhiteSpace(this.SplashHash) ? $"https://cdn.discordapp.com/splashes/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.SplashHash}.jpg" : null; - - /// - /// Gets the guild's banner hash, when applicable. - /// - [JsonProperty("banner", NullValueHandling = NullValueHandling.Ignore)] - public string Banner { get; internal set; } - - /// - /// Gets the guild's banner in url form. - /// - [JsonIgnore] - public string BannerUrl - => !string.IsNullOrWhiteSpace(this.Banner) ? $"https://cdn.discordapp.com/banners/{this.Id}/{this.Banner}" : null; - - /// - /// Gets the guild description, when applicable. - /// - [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] - public string Description { get; internal set; } - - /// - /// Gets a collection of this guild's features. - /// - [JsonProperty("features", NullValueHandling = NullValueHandling.Ignore)] - public IReadOnlyList Features { get; internal set; } - - /// - /// Gets the guild's verification level. - /// - [JsonProperty("verification_level", NullValueHandling = NullValueHandling.Ignore)] - public DiscordVerificationLevel VerificationLevel { get; internal set; } - - /// - /// Gets vanity URL code for this guild, when applicable. - /// - [JsonProperty("vanity_url_code")] - public string VanityUrlCode { get; internal set; } - - /// - /// Gets the guild's welcome screen, when applicable. - /// - [JsonProperty("welcome_screen", NullValueHandling = NullValueHandling.Ignore)] - public DiscordGuildWelcomeScreen WelcomeScreen { get; internal set; } - - internal DiscordInviteGuild() { } -} +using System.Collections.Generic; +using System.Globalization; +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Represents a guild to which the user is invited. +/// +public class DiscordInviteGuild : SnowflakeObject +{ + /// + /// Gets the name of the guild. + /// + [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] + public string Name { get; internal set; } + + /// + /// Gets the guild icon's hash. + /// + [JsonProperty("icon", NullValueHandling = NullValueHandling.Ignore)] + public string IconHash { get; internal set; } + + /// + /// Gets the guild icon's url. + /// + [JsonIgnore] + public string IconUrl + => !string.IsNullOrWhiteSpace(this.IconHash) ? $"https://cdn.discordapp.com/icons/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.IconHash}.jpg" : null; + + /// + /// Gets the hash of guild's invite splash. + /// + [JsonProperty("splash", NullValueHandling = NullValueHandling.Ignore)] + internal string SplashHash { get; set; } + + /// + /// Gets the URL of guild's invite splash. + /// + [JsonIgnore] + public string SplashUrl + => !string.IsNullOrWhiteSpace(this.SplashHash) ? $"https://cdn.discordapp.com/splashes/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.SplashHash}.jpg" : null; + + /// + /// Gets the guild's banner hash, when applicable. + /// + [JsonProperty("banner", NullValueHandling = NullValueHandling.Ignore)] + public string Banner { get; internal set; } + + /// + /// Gets the guild's banner in url form. + /// + [JsonIgnore] + public string BannerUrl + => !string.IsNullOrWhiteSpace(this.Banner) ? $"https://cdn.discordapp.com/banners/{this.Id}/{this.Banner}" : null; + + /// + /// Gets the guild description, when applicable. + /// + [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] + public string Description { get; internal set; } + + /// + /// Gets a collection of this guild's features. + /// + [JsonProperty("features", NullValueHandling = NullValueHandling.Ignore)] + public IReadOnlyList Features { get; internal set; } + + /// + /// Gets the guild's verification level. + /// + [JsonProperty("verification_level", NullValueHandling = NullValueHandling.Ignore)] + public DiscordVerificationLevel VerificationLevel { get; internal set; } + + /// + /// Gets vanity URL code for this guild, when applicable. + /// + [JsonProperty("vanity_url_code")] + public string VanityUrlCode { get; internal set; } + + /// + /// Gets the guild's welcome screen, when applicable. + /// + [JsonProperty("welcome_screen", NullValueHandling = NullValueHandling.Ignore)] + public DiscordGuildWelcomeScreen WelcomeScreen { get; internal set; } + + internal DiscordInviteGuild() { } +} diff --git a/DSharpPlus/Entities/Invite/DiscordInviteTargetType.cs b/DSharpPlus/Entities/Invite/DiscordInviteTargetType.cs index 268f2f2093..538491a1fc 100644 --- a/DSharpPlus/Entities/Invite/DiscordInviteTargetType.cs +++ b/DSharpPlus/Entities/Invite/DiscordInviteTargetType.cs @@ -1,17 +1,17 @@ -namespace DSharpPlus.Entities; - - -/// -/// Represents the application an invite is for. -/// -public enum DiscordInviteTargetType -{ - /// - /// Represents an invite to a user streaming. - /// - Stream = 1, - /// - /// Represents an invite to an embedded application. - /// - EmbeddedApplication = 2 -} +namespace DSharpPlus.Entities; + + +/// +/// Represents the application an invite is for. +/// +public enum DiscordInviteTargetType +{ + /// + /// Represents an invite to a user streaming. + /// + Stream = 1, + /// + /// Represents an invite to an embedded application. + /// + EmbeddedApplication = 2 +} diff --git a/DSharpPlus/Entities/Invite/DiscordStageInvite.cs b/DSharpPlus/Entities/Invite/DiscordStageInvite.cs index 16df5898e1..52fb4b192f 100644 --- a/DSharpPlus/Entities/Invite/DiscordStageInvite.cs +++ b/DSharpPlus/Entities/Invite/DiscordStageInvite.cs @@ -1,34 +1,34 @@ -using System.Collections.Generic; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents an invite to a stage channel. -/// -public class DiscordStageInvite -{ - /// - /// Gets the members that are currently speaking in the stage channel. - /// - [JsonProperty("members")] - public IReadOnlyList Members { get; internal set; } - - /// - /// Gets the number of participants in the stage channel. - /// - [JsonProperty("participant_count")] - public int ParticipantCount { get; internal set; } - - /// - /// Gets the number of speakers in the stage channel. - /// - [JsonProperty("speaker_count")] - public int SpeakerCount { get; internal set; } - - /// - /// Gets the topic of the stage channel. - /// - [JsonProperty("topic")] - public string Topic { get; internal set; } -} +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Represents an invite to a stage channel. +/// +public class DiscordStageInvite +{ + /// + /// Gets the members that are currently speaking in the stage channel. + /// + [JsonProperty("members")] + public IReadOnlyList Members { get; internal set; } + + /// + /// Gets the number of participants in the stage channel. + /// + [JsonProperty("participant_count")] + public int ParticipantCount { get; internal set; } + + /// + /// Gets the number of speakers in the stage channel. + /// + [JsonProperty("speaker_count")] + public int SpeakerCount { get; internal set; } + + /// + /// Gets the topic of the stage channel. + /// + [JsonProperty("topic")] + public string Topic { get; internal set; } +} diff --git a/DSharpPlus/Entities/Optional.cs b/DSharpPlus/Entities/Optional.cs index 142012bd98..bcaf2b729e 100644 --- a/DSharpPlus/Entities/Optional.cs +++ b/DSharpPlus/Entities/Optional.cs @@ -1,251 +1,251 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Reflection; -using DSharpPlus.Net.Serialization; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using Newtonsoft.Json.Serialization; - -namespace DSharpPlus.Entities; - -/// -/// Helper methods for instantiating an . -/// -/// -/// This class only serves to allow type parameter inference on calls to or -/// . -/// -public static class Optional -{ - /// - /// Creates a new with specified value and valid state. - /// - /// Value to populate the optional with. - /// Type of the value. - /// Created optional. - public static Optional FromValue(T value) - => new(value); - - /// - /// Creates a new empty with no value and invalid state. - /// - /// The type that the created instance is wrapping around. - /// Created optional. - public static Optional FromNoValue() - => default; -} - -// used internally to make serialization more convenient, do NOT change this, do NOT implement this yourself -public interface IOptional -{ - public bool HasValue { get; } - public object RawValue { get; } // must NOT throw InvalidOperationException -} - -/// -/// Represents a wrapper which may or may not have a value. -/// -/// Type of the value. -[JsonConverter(typeof(OptionalJsonConverter))] -public readonly struct Optional : IEquatable>, IEquatable, IOptional -{ - /// - /// Gets whether this has a value. - /// - public bool HasValue { get; } - - /// - /// Gets the value of this . - /// - /// If this has no value. - public T Value => this.HasValue ? this.val : throw new InvalidOperationException("Value is not set."); - object IOptional.RawValue => this.val; - - private readonly T val; - - /// - /// Creates a new with specified value. - /// - /// Value of this option. - public Optional(T value) - { - this.val = value; - this.HasValue = true; - } - - /// - /// Determines whether the optional has a value, and the value is non-null. - /// - /// The value contained within the optional. - /// True if the value is set, and is not null, otherwise false. - public bool IsDefined([NotNullWhen(true)] out T? value) - => (value = this.val) != null; - - /// - /// Returns a string representation of this optional value. - /// - /// String representation of this optional value. - public override string ToString() => $"Optional<{typeof(T)}> ({(this.HasValue ? this.Value.ToString() : "")})"; - - /// - /// Checks whether this (or its value) are equal to another object. - /// - /// Object to compare to. - /// Whether the object is equal to this or its value. - public override bool Equals(object obj) - { - return obj switch - { - T t => Equals(t), - Optional opt => Equals(opt), - _ => false, - }; - } - - /// - /// Checks whether this is equal to another . - /// - /// to compare to. - /// Whether the is equal to this . - public bool Equals(Optional e) - => (!this.HasValue && !e.HasValue) || (this.HasValue == e.HasValue && this.Value.Equals(e.Value)); - - /// - /// Checks whether the value of this is equal to specified object. - /// - /// Object to compare to. - /// Whether the object is equal to the value of this . - public bool Equals(T e) - => this.HasValue && ReferenceEquals(this.Value, e); - - /// - /// Gets the hash code for this . - /// - /// The hash code for this . - [SuppressMessage("Formatting", "IDE0046", Justification = "Do not fall into the ternary trap")] - public override int GetHashCode() - { - if (this.HasValue) - { - if (this.val is not null) - { - return this.val.GetHashCode(); - } - - return 0; - } - - return -1; - } - - public static implicit operator Optional(T val) - => new(val); - - public static explicit operator T(Optional opt) - => opt.Value; - - public static bool operator ==(Optional opt1, Optional opt2) - => opt1.Equals(opt2); - - public static bool operator !=(Optional opt1, Optional opt2) - => !opt1.Equals(opt2); - - public static bool operator ==(Optional opt, T t) - => opt.Equals(t); - - public static bool operator !=(Optional opt, T t) - => !opt.Equals(t); - - /// - /// Performs a mapping operation on the current , turning it into an Optional holding a - /// instance if the source optional contains a value; otherwise, returns an - /// of that same type with no value. - /// - /// The mapping function to apply on the current value if it exists - /// The type of the target value returned by - /// - /// An containing a value denoted by calling if the current - /// contains a value; otherwise, an empty of the target - /// type. - /// - public Optional IfPresent(Func mapper) => this.HasValue ? new Optional(mapper(this.Value)) : default; -} - -/// -internal sealed class OptionalJsonContractResolver : DefaultContractResolver -{ - protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) - { - JsonProperty property = base.CreateProperty(member, memberSerialization); - - Type? type = property.PropertyType; - - if (!type.GetTypeInfo().ImplementedInterfaces.Contains(typeof(IOptional))) - { - return property; - } - - // we cache the PropertyInfo object here (it's captured in closure). we don't have direct - // access to the property value so we have to reflect into it from the parent instance - // we use UnderlyingName instead of PropertyName in case the C# name is different from the Json name. - MemberInfo? declaringMember = property.DeclaringType.GetTypeInfo().DeclaredMembers - .FirstOrDefault(e => e.Name == property.UnderlyingName); - - switch (declaringMember) - { - case PropertyInfo declaringProp: - property.ShouldSerialize = instance => // instance here is the declaring (parent) type - { - object? optionalValue = declaringProp.GetValue(instance); - return (optionalValue as IOptional).HasValue; - }; - return property; - case FieldInfo declaringField: - property.ShouldSerialize = instance => // instance here is the declaring (parent) type - { - object? optionalValue = declaringField.GetValue(instance); - return (optionalValue as IOptional).HasValue; - }; - return property; - default: - throw new InvalidOperationException( - "Can only serialize Optional members that are fields or properties"); - } - } -} - -internal sealed class OptionalJsonConverter : JsonConverter -{ - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - // we don't check for HasValue here since it's checked in OptionalJsonContractResolver - object val = (value as IOptional).RawValue; - // JToken.FromObject will throw if `null` so we manually write a null value. - if (val == null) - { - // you can read serializer.NullValueHandling here, but unfortunately you can **not** skip serialization - // here, or else you will get a nasty JsonWriterException, so we just ignore its value and manually - // write the null. - writer.WriteToken(JsonToken.Null); - } - else - { - // convert the value to a JSON object and write it to the property value. - JToken.FromObject(val).WriteTo(writer); - } - } - - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, - JsonSerializer serializer) - { - Type genericType = objectType.GenericTypeArguments[0]; - - ConstructorInfo? constructor = objectType.GetTypeInfo().DeclaredConstructors - .FirstOrDefault(e => e.GetParameters()[0].ParameterType == genericType); - - return constructor.Invoke([serializer.Deserialize(reader, genericType)]); - } - - public override bool CanConvert(Type objectType) => objectType.GetTypeInfo().ImplementedInterfaces.Contains(typeof(IOptional)); -} +using System; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Reflection; +using DSharpPlus.Net.Serialization; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Newtonsoft.Json.Serialization; + +namespace DSharpPlus.Entities; + +/// +/// Helper methods for instantiating an . +/// +/// +/// This class only serves to allow type parameter inference on calls to or +/// . +/// +public static class Optional +{ + /// + /// Creates a new with specified value and valid state. + /// + /// Value to populate the optional with. + /// Type of the value. + /// Created optional. + public static Optional FromValue(T value) + => new(value); + + /// + /// Creates a new empty with no value and invalid state. + /// + /// The type that the created instance is wrapping around. + /// Created optional. + public static Optional FromNoValue() + => default; +} + +// used internally to make serialization more convenient, do NOT change this, do NOT implement this yourself +public interface IOptional +{ + public bool HasValue { get; } + public object RawValue { get; } // must NOT throw InvalidOperationException +} + +/// +/// Represents a wrapper which may or may not have a value. +/// +/// Type of the value. +[JsonConverter(typeof(OptionalJsonConverter))] +public readonly struct Optional : IEquatable>, IEquatable, IOptional +{ + /// + /// Gets whether this has a value. + /// + public bool HasValue { get; } + + /// + /// Gets the value of this . + /// + /// If this has no value. + public T Value => this.HasValue ? this.val : throw new InvalidOperationException("Value is not set."); + object IOptional.RawValue => this.val; + + private readonly T val; + + /// + /// Creates a new with specified value. + /// + /// Value of this option. + public Optional(T value) + { + this.val = value; + this.HasValue = true; + } + + /// + /// Determines whether the optional has a value, and the value is non-null. + /// + /// The value contained within the optional. + /// True if the value is set, and is not null, otherwise false. + public bool IsDefined([NotNullWhen(true)] out T? value) + => (value = this.val) != null; + + /// + /// Returns a string representation of this optional value. + /// + /// String representation of this optional value. + public override string ToString() => $"Optional<{typeof(T)}> ({(this.HasValue ? this.Value.ToString() : "")})"; + + /// + /// Checks whether this (or its value) are equal to another object. + /// + /// Object to compare to. + /// Whether the object is equal to this or its value. + public override bool Equals(object obj) + { + return obj switch + { + T t => Equals(t), + Optional opt => Equals(opt), + _ => false, + }; + } + + /// + /// Checks whether this is equal to another . + /// + /// to compare to. + /// Whether the is equal to this . + public bool Equals(Optional e) + => (!this.HasValue && !e.HasValue) || (this.HasValue == e.HasValue && this.Value.Equals(e.Value)); + + /// + /// Checks whether the value of this is equal to specified object. + /// + /// Object to compare to. + /// Whether the object is equal to the value of this . + public bool Equals(T e) + => this.HasValue && ReferenceEquals(this.Value, e); + + /// + /// Gets the hash code for this . + /// + /// The hash code for this . + [SuppressMessage("Formatting", "IDE0046", Justification = "Do not fall into the ternary trap")] + public override int GetHashCode() + { + if (this.HasValue) + { + if (this.val is not null) + { + return this.val.GetHashCode(); + } + + return 0; + } + + return -1; + } + + public static implicit operator Optional(T val) + => new(val); + + public static explicit operator T(Optional opt) + => opt.Value; + + public static bool operator ==(Optional opt1, Optional opt2) + => opt1.Equals(opt2); + + public static bool operator !=(Optional opt1, Optional opt2) + => !opt1.Equals(opt2); + + public static bool operator ==(Optional opt, T t) + => opt.Equals(t); + + public static bool operator !=(Optional opt, T t) + => !opt.Equals(t); + + /// + /// Performs a mapping operation on the current , turning it into an Optional holding a + /// instance if the source optional contains a value; otherwise, returns an + /// of that same type with no value. + /// + /// The mapping function to apply on the current value if it exists + /// The type of the target value returned by + /// + /// An containing a value denoted by calling if the current + /// contains a value; otherwise, an empty of the target + /// type. + /// + public Optional IfPresent(Func mapper) => this.HasValue ? new Optional(mapper(this.Value)) : default; +} + +/// +internal sealed class OptionalJsonContractResolver : DefaultContractResolver +{ + protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) + { + JsonProperty property = base.CreateProperty(member, memberSerialization); + + Type? type = property.PropertyType; + + if (!type.GetTypeInfo().ImplementedInterfaces.Contains(typeof(IOptional))) + { + return property; + } + + // we cache the PropertyInfo object here (it's captured in closure). we don't have direct + // access to the property value so we have to reflect into it from the parent instance + // we use UnderlyingName instead of PropertyName in case the C# name is different from the Json name. + MemberInfo? declaringMember = property.DeclaringType.GetTypeInfo().DeclaredMembers + .FirstOrDefault(e => e.Name == property.UnderlyingName); + + switch (declaringMember) + { + case PropertyInfo declaringProp: + property.ShouldSerialize = instance => // instance here is the declaring (parent) type + { + object? optionalValue = declaringProp.GetValue(instance); + return (optionalValue as IOptional).HasValue; + }; + return property; + case FieldInfo declaringField: + property.ShouldSerialize = instance => // instance here is the declaring (parent) type + { + object? optionalValue = declaringField.GetValue(instance); + return (optionalValue as IOptional).HasValue; + }; + return property; + default: + throw new InvalidOperationException( + "Can only serialize Optional members that are fields or properties"); + } + } +} + +internal sealed class OptionalJsonConverter : JsonConverter +{ + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + // we don't check for HasValue here since it's checked in OptionalJsonContractResolver + object val = (value as IOptional).RawValue; + // JToken.FromObject will throw if `null` so we manually write a null value. + if (val == null) + { + // you can read serializer.NullValueHandling here, but unfortunately you can **not** skip serialization + // here, or else you will get a nasty JsonWriterException, so we just ignore its value and manually + // write the null. + writer.WriteToken(JsonToken.Null); + } + else + { + // convert the value to a JSON object and write it to the property value. + JToken.FromObject(val).WriteTo(writer); + } + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, + JsonSerializer serializer) + { + Type genericType = objectType.GenericTypeArguments[0]; + + ConstructorInfo? constructor = objectType.GetTypeInfo().DeclaredConstructors + .FirstOrDefault(e => e.GetParameters()[0].ParameterType == genericType); + + return constructor.Invoke([serializer.Deserialize(reader, genericType)]); + } + + public override bool CanConvert(Type objectType) => objectType.GetTypeInfo().ImplementedInterfaces.Contains(typeof(IOptional)); +} diff --git a/DSharpPlus/Entities/SnowflakeObject.cs b/DSharpPlus/Entities/SnowflakeObject.cs index 51045c0715..2354b85b50 100644 --- a/DSharpPlus/Entities/SnowflakeObject.cs +++ b/DSharpPlus/Entities/SnowflakeObject.cs @@ -1,31 +1,31 @@ -using System; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents an object in Discord API. -/// -public abstract class SnowflakeObject -{ - /// - /// Gets the ID of this object. - /// - [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)] - public ulong Id { get; internal set; } - - /// - /// Gets the date and time this object was created. - /// - [JsonIgnore] - public DateTimeOffset CreationTimestamp - => this.Id.GetSnowflakeTime(); - - /// - /// Gets the client instance this object is tied to. - /// - [JsonIgnore] - internal BaseDiscordClient Discord { get; set; } - - internal SnowflakeObject() { } -} +using System; +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Represents an object in Discord API. +/// +public abstract class SnowflakeObject +{ + /// + /// Gets the ID of this object. + /// + [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)] + public ulong Id { get; internal set; } + + /// + /// Gets the date and time this object was created. + /// + [JsonIgnore] + public DateTimeOffset CreationTimestamp + => this.Id.GetSnowflakeTime(); + + /// + /// Gets the client instance this object is tied to. + /// + [JsonIgnore] + internal BaseDiscordClient Discord { get; set; } + + internal SnowflakeObject() { } +} diff --git a/DSharpPlus/Entities/User/DiscordActivity.cs b/DSharpPlus/Entities/User/DiscordActivity.cs index 30d06b5f18..5f733d6119 100644 --- a/DSharpPlus/Entities/User/DiscordActivity.cs +++ b/DSharpPlus/Entities/User/DiscordActivity.cs @@ -1,447 +1,447 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using DSharpPlus.Net.Abstractions; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents user status. -/// -[JsonConverter(typeof(UserStatusConverter))] -public enum DiscordUserStatus -{ - /// - /// User is offline. - /// - Offline = 0, - - /// - /// User is online. - /// - Online = 1, - - /// - /// User is idle. - /// - Idle = 2, - - /// - /// User asked not to be disturbed. - /// - DoNotDisturb = 4, - - /// - /// User is invisible. They will appear as Offline to anyone but themselves. - /// - Invisible = 5 -} - -internal sealed class UserStatusConverter : JsonConverter -{ - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - if (value is DiscordUserStatus status) - { - switch (status) // reader.Value can be a string, DateTime or DateTimeOffset (yes, it's weird) - { - case DiscordUserStatus.Online: - writer.WriteValue("online"); - return; - - case DiscordUserStatus.Idle: - writer.WriteValue("idle"); - return; - - case DiscordUserStatus.DoNotDisturb: - writer.WriteValue("dnd"); - return; - - case DiscordUserStatus.Invisible: - writer.WriteValue("invisible"); - return; - - case DiscordUserStatus.Offline: - default: - writer.WriteValue("offline"); - return; - } - } - } - - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) => - // Active sessions are indicated with an "online", "idle", or "dnd" string per platform. If a user is - // offline or invisible, the corresponding field is not present. - reader.Value?.ToString().ToLowerInvariant() switch // reader.Value can be a string, DateTime or DateTimeOffset (yes, it's weird) - { - "online" => DiscordUserStatus.Online, - "idle" => DiscordUserStatus.Idle, - "dnd" => DiscordUserStatus.DoNotDisturb, - "invisible" => DiscordUserStatus.Invisible, - _ => DiscordUserStatus.Offline, - }; - - public override bool CanConvert(Type objectType) => objectType == typeof(DiscordUserStatus); -} - -/// -/// Represents a game that a user is playing. -/// -public sealed class DiscordActivity -{ - /// - /// Gets or sets the name of user's activity. - /// - public string Name { get; set; } - - /// - /// Gets or sets the stream URL, if applicable. - /// - public string StreamUrl { get; set; } - - /// - /// Gets or sets the activity type. - /// - public DiscordActivityType ActivityType { get; set; } - - /// - /// Gets the rich presence details, if present. - /// - public DiscordRichPresence RichPresence { get; internal set; } - - /// - /// Gets the custom status of this activity, if present. - /// - public DiscordCustomStatus CustomStatus { get; internal set; } - - /// - /// Creates a new, empty instance of a . - /// - public DiscordActivity() => this.ActivityType = DiscordActivityType.Playing; - - /// - /// Creates a new instance of a with specified name. - /// - /// Name of the activity. - public DiscordActivity(string name) - { - this.Name = name; - this.ActivityType = DiscordActivityType.Playing; - } - - /// - /// Creates a new instance of a with specified name. - /// - /// Name of the activity. - /// Type of the activity. - public DiscordActivity(string name, DiscordActivityType type) - { - if (type == DiscordActivityType.Custom) - { - this.Name = "Custom Status"; - this.CustomStatus = new DiscordCustomStatus() { Name = name }; - this.ActivityType = DiscordActivityType.Custom; - } - else - { - this.Name = name; - this.ActivityType = type; - } - - this.ActivityType = type; - } - - internal DiscordActivity(TransportActivity rawActivity) => UpdateWith(rawActivity); - - internal DiscordActivity(DiscordActivity other) - { - this.Name = other.Name; - this.ActivityType = other.ActivityType; - this.StreamUrl = other.StreamUrl; - if (other.RichPresence != null) - { - this.RichPresence = new DiscordRichPresence(other.RichPresence); - } - - if (other.CustomStatus != null) - { - this.CustomStatus = new DiscordCustomStatus(other.CustomStatus); - } - } - - internal void UpdateWith(TransportActivity rawActivity) - { - this.Name = rawActivity?.Name; - this.ActivityType = rawActivity != null ? rawActivity.ActivityType : DiscordActivityType.Playing; - this.StreamUrl = rawActivity?.StreamUrl; - - if (rawActivity?.IsRichPresence() == true && this.RichPresence != null) - { - this.RichPresence.UpdateWith(rawActivity); - } - else - { - this.RichPresence = rawActivity?.IsRichPresence() == true ? new DiscordRichPresence(rawActivity) : null; - } - - if (rawActivity?.IsCustomStatus() == true && this.CustomStatus != null) - { - this.CustomStatus.UpdateWith(rawActivity.State, rawActivity.Emoji); - } - else - { - this.CustomStatus = rawActivity?.IsCustomStatus() == true - ? new DiscordCustomStatus - { - Name = rawActivity.State, - Emoji = rawActivity.Emoji - } - : null; - } - } -} - -/// -/// Represents details for a custom status activity, attached to a . -/// -public sealed class DiscordCustomStatus -{ - /// - /// Gets the name of this custom status. - /// - public string Name { get; internal set; } - - /// - /// Gets the emoji of this custom status, if any. - /// - public DiscordEmoji Emoji { get; internal set; } - - internal DiscordCustomStatus() { } - - internal DiscordCustomStatus(DiscordCustomStatus other) - { - this.Name = other.Name; - this.Emoji = other.Emoji; - } - - internal void UpdateWith(string state, DiscordEmoji emoji) - { - this.Name = state; - this.Emoji = emoji; - } -} - -/// -/// Represents details for Discord rich presence, attached to a . -/// -public sealed class DiscordRichPresence -{ - /// - /// Gets the details of this presence. - /// - public string Details { get; internal set; } - - /// - /// Gets the game state. - /// - public string State { get; internal set; } - - /// - /// Gets the application for which the rich presence is for. - /// - public DiscordApplication Application { get; internal set; } - - /// - /// Gets the instance status. - /// - public bool? Instance { get; internal set; } - - /// - /// Gets the large image for the rich presence. - /// - public DiscordAsset LargeImage { get; internal set; } - - /// - /// Gets the hovertext for large image. - /// - public string LargeImageText { get; internal set; } - - /// - /// Gets the small image for the rich presence. - /// - public DiscordAsset SmallImage { get; internal set; } - - /// - /// Gets the hovertext for small image. - /// - public string SmallImageText { get; internal set; } - - /// - /// Gets the current party size. - /// - public long? CurrentPartySize { get; internal set; } - - /// - /// Gets the maximum party size. - /// - public long? MaximumPartySize { get; internal set; } - - /// - /// Gets the party ID. - /// - public ulong? PartyId { get; internal set; } - - /// - /// Gets the game start timestamp. - /// - public DateTimeOffset? StartTimestamp { get; internal set; } - - /// - /// Gets the game end timestamp. - /// - public DateTimeOffset? EndTimestamp { get; internal set; } - - /// - /// Gets the secret value enabling users to join your game. - /// - public string JoinSecret { get; internal set; } - - /// - /// Gets the secret value enabling users to receive notifications whenever your game state changes. - /// - public string MatchSecret { get; internal set; } - - /// - /// Gets the secret value enabling users to spectate your game. - /// - public string SpectateSecret { get; internal set; } - - /// - /// Gets the buttons for the rich presence. - /// - public IReadOnlyList Buttons { get; internal set; } - - internal DiscordRichPresence() { } - - internal DiscordRichPresence(TransportActivity rawGame) => UpdateWith(rawGame); - - internal DiscordRichPresence(DiscordRichPresence other) - { - this.Details = other.Details; - this.State = other.State; - this.Application = other.Application; - this.Instance = other.Instance; - this.LargeImageText = other.LargeImageText; - this.SmallImageText = other.SmallImageText; - this.LargeImage = other.LargeImage; - this.SmallImage = other.SmallImage; - this.CurrentPartySize = other.CurrentPartySize; - this.MaximumPartySize = other.MaximumPartySize; - this.PartyId = other.PartyId; - this.StartTimestamp = other.StartTimestamp; - this.EndTimestamp = other.EndTimestamp; - this.JoinSecret = other.JoinSecret; - this.MatchSecret = other.MatchSecret; - this.SpectateSecret = other.SpectateSecret; - this.Buttons = other.Buttons; - } - - internal void UpdateWith(TransportActivity rawGame) - { - this.Details = rawGame?.Details; - this.State = rawGame?.State; - this.Application = rawGame?.ApplicationId != null ? new DiscordApplication { Id = rawGame.ApplicationId.Value } : null; - this.Instance = rawGame?.Instance; - this.LargeImageText = rawGame?.Assets?.LargeImageText; - this.SmallImageText = rawGame?.Assets?.SmallImageText; - //this.LargeImage = rawGame?.Assets?.LargeImage != null ? new DiscordApplicationAsset { Application = this.Application, Id = rawGame.Assets.LargeImage.Value, Type = ApplicationAssetType.LargeImage } : null; - //this.SmallImage = rawGame?.Assets?.SmallImage != null ? new DiscordApplicationAsset { Application = this.Application, Id = rawGame.Assets.SmallImage.Value, Type = ApplicationAssetType.SmallImage } : null; - this.CurrentPartySize = rawGame?.Party?.Size?.Current; - this.MaximumPartySize = rawGame?.Party?.Size?.Maximum; - if (rawGame?.Party != null && ulong.TryParse(rawGame.Party.Id, NumberStyles.Number, CultureInfo.InvariantCulture, out ulong partyId)) - { - this.PartyId = partyId; - } - - this.StartTimestamp = rawGame?.Timestamps?.Start; - this.EndTimestamp = rawGame?.Timestamps?.End; - this.JoinSecret = rawGame?.Secrets?.Join; - this.MatchSecret = rawGame?.Secrets?.Match; - this.SpectateSecret = rawGame?.Secrets?.Spectate; - this.Buttons = rawGame?.Buttons; - - string? lid = rawGame?.Assets?.LargeImage; - if (lid != null) - { - if (lid.StartsWith("spotify:")) - { - this.LargeImage = new DiscordSpotifyAsset(lid); - } - else if (ulong.TryParse(lid, NumberStyles.Number, CultureInfo.InvariantCulture, out _)) - { - this.LargeImage = new DiscordApplicationAsset - { - Id = lid, - Application = this.Application, - Type = DiscordApplicationAssetType.LargeImage - }; - } - } - - string? sid = rawGame?.Assets?.SmallImage; - if (sid != null) - { - if (sid.StartsWith("spotify:")) - { - this.SmallImage = new DiscordSpotifyAsset(sid); - } - else if (ulong.TryParse(sid, NumberStyles.Number, CultureInfo.InvariantCulture, out _)) - { - this.SmallImage = - new DiscordApplicationAsset - { - Id = sid, - Application = this.Application, - Type = DiscordApplicationAssetType.SmallImage - }; - } - } - } -} - -/// -/// Determines the type of a user activity. -/// -public enum DiscordActivityType -{ - /// - /// Indicates the user is playing a game. - /// - Playing = 0, - - /// - /// Indicates the user is streaming a game. - /// - Streaming = 1, - - /// - /// Indicates the user is listening to something. - /// - ListeningTo = 2, - - /// - /// Indicates the user is watching something. - /// - Watching = 3, - - /// - /// Indicates the current activity is a custom status. - /// - Custom = 4, - - /// - /// Indicates the user is competing in something. - /// - Competing = 5 -} +using System; +using System.Collections.Generic; +using System.Globalization; +using DSharpPlus.Net.Abstractions; +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Represents user status. +/// +[JsonConverter(typeof(UserStatusConverter))] +public enum DiscordUserStatus +{ + /// + /// User is offline. + /// + Offline = 0, + + /// + /// User is online. + /// + Online = 1, + + /// + /// User is idle. + /// + Idle = 2, + + /// + /// User asked not to be disturbed. + /// + DoNotDisturb = 4, + + /// + /// User is invisible. They will appear as Offline to anyone but themselves. + /// + Invisible = 5 +} + +internal sealed class UserStatusConverter : JsonConverter +{ + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + if (value is DiscordUserStatus status) + { + switch (status) // reader.Value can be a string, DateTime or DateTimeOffset (yes, it's weird) + { + case DiscordUserStatus.Online: + writer.WriteValue("online"); + return; + + case DiscordUserStatus.Idle: + writer.WriteValue("idle"); + return; + + case DiscordUserStatus.DoNotDisturb: + writer.WriteValue("dnd"); + return; + + case DiscordUserStatus.Invisible: + writer.WriteValue("invisible"); + return; + + case DiscordUserStatus.Offline: + default: + writer.WriteValue("offline"); + return; + } + } + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) => + // Active sessions are indicated with an "online", "idle", or "dnd" string per platform. If a user is + // offline or invisible, the corresponding field is not present. + reader.Value?.ToString().ToLowerInvariant() switch // reader.Value can be a string, DateTime or DateTimeOffset (yes, it's weird) + { + "online" => DiscordUserStatus.Online, + "idle" => DiscordUserStatus.Idle, + "dnd" => DiscordUserStatus.DoNotDisturb, + "invisible" => DiscordUserStatus.Invisible, + _ => DiscordUserStatus.Offline, + }; + + public override bool CanConvert(Type objectType) => objectType == typeof(DiscordUserStatus); +} + +/// +/// Represents a game that a user is playing. +/// +public sealed class DiscordActivity +{ + /// + /// Gets or sets the name of user's activity. + /// + public string Name { get; set; } + + /// + /// Gets or sets the stream URL, if applicable. + /// + public string StreamUrl { get; set; } + + /// + /// Gets or sets the activity type. + /// + public DiscordActivityType ActivityType { get; set; } + + /// + /// Gets the rich presence details, if present. + /// + public DiscordRichPresence RichPresence { get; internal set; } + + /// + /// Gets the custom status of this activity, if present. + /// + public DiscordCustomStatus CustomStatus { get; internal set; } + + /// + /// Creates a new, empty instance of a . + /// + public DiscordActivity() => this.ActivityType = DiscordActivityType.Playing; + + /// + /// Creates a new instance of a with specified name. + /// + /// Name of the activity. + public DiscordActivity(string name) + { + this.Name = name; + this.ActivityType = DiscordActivityType.Playing; + } + + /// + /// Creates a new instance of a with specified name. + /// + /// Name of the activity. + /// Type of the activity. + public DiscordActivity(string name, DiscordActivityType type) + { + if (type == DiscordActivityType.Custom) + { + this.Name = "Custom Status"; + this.CustomStatus = new DiscordCustomStatus() { Name = name }; + this.ActivityType = DiscordActivityType.Custom; + } + else + { + this.Name = name; + this.ActivityType = type; + } + + this.ActivityType = type; + } + + internal DiscordActivity(TransportActivity rawActivity) => UpdateWith(rawActivity); + + internal DiscordActivity(DiscordActivity other) + { + this.Name = other.Name; + this.ActivityType = other.ActivityType; + this.StreamUrl = other.StreamUrl; + if (other.RichPresence != null) + { + this.RichPresence = new DiscordRichPresence(other.RichPresence); + } + + if (other.CustomStatus != null) + { + this.CustomStatus = new DiscordCustomStatus(other.CustomStatus); + } + } + + internal void UpdateWith(TransportActivity rawActivity) + { + this.Name = rawActivity?.Name; + this.ActivityType = rawActivity != null ? rawActivity.ActivityType : DiscordActivityType.Playing; + this.StreamUrl = rawActivity?.StreamUrl; + + if (rawActivity?.IsRichPresence() == true && this.RichPresence != null) + { + this.RichPresence.UpdateWith(rawActivity); + } + else + { + this.RichPresence = rawActivity?.IsRichPresence() == true ? new DiscordRichPresence(rawActivity) : null; + } + + if (rawActivity?.IsCustomStatus() == true && this.CustomStatus != null) + { + this.CustomStatus.UpdateWith(rawActivity.State, rawActivity.Emoji); + } + else + { + this.CustomStatus = rawActivity?.IsCustomStatus() == true + ? new DiscordCustomStatus + { + Name = rawActivity.State, + Emoji = rawActivity.Emoji + } + : null; + } + } +} + +/// +/// Represents details for a custom status activity, attached to a . +/// +public sealed class DiscordCustomStatus +{ + /// + /// Gets the name of this custom status. + /// + public string Name { get; internal set; } + + /// + /// Gets the emoji of this custom status, if any. + /// + public DiscordEmoji Emoji { get; internal set; } + + internal DiscordCustomStatus() { } + + internal DiscordCustomStatus(DiscordCustomStatus other) + { + this.Name = other.Name; + this.Emoji = other.Emoji; + } + + internal void UpdateWith(string state, DiscordEmoji emoji) + { + this.Name = state; + this.Emoji = emoji; + } +} + +/// +/// Represents details for Discord rich presence, attached to a . +/// +public sealed class DiscordRichPresence +{ + /// + /// Gets the details of this presence. + /// + public string Details { get; internal set; } + + /// + /// Gets the game state. + /// + public string State { get; internal set; } + + /// + /// Gets the application for which the rich presence is for. + /// + public DiscordApplication Application { get; internal set; } + + /// + /// Gets the instance status. + /// + public bool? Instance { get; internal set; } + + /// + /// Gets the large image for the rich presence. + /// + public DiscordAsset LargeImage { get; internal set; } + + /// + /// Gets the hovertext for large image. + /// + public string LargeImageText { get; internal set; } + + /// + /// Gets the small image for the rich presence. + /// + public DiscordAsset SmallImage { get; internal set; } + + /// + /// Gets the hovertext for small image. + /// + public string SmallImageText { get; internal set; } + + /// + /// Gets the current party size. + /// + public long? CurrentPartySize { get; internal set; } + + /// + /// Gets the maximum party size. + /// + public long? MaximumPartySize { get; internal set; } + + /// + /// Gets the party ID. + /// + public ulong? PartyId { get; internal set; } + + /// + /// Gets the game start timestamp. + /// + public DateTimeOffset? StartTimestamp { get; internal set; } + + /// + /// Gets the game end timestamp. + /// + public DateTimeOffset? EndTimestamp { get; internal set; } + + /// + /// Gets the secret value enabling users to join your game. + /// + public string JoinSecret { get; internal set; } + + /// + /// Gets the secret value enabling users to receive notifications whenever your game state changes. + /// + public string MatchSecret { get; internal set; } + + /// + /// Gets the secret value enabling users to spectate your game. + /// + public string SpectateSecret { get; internal set; } + + /// + /// Gets the buttons for the rich presence. + /// + public IReadOnlyList Buttons { get; internal set; } + + internal DiscordRichPresence() { } + + internal DiscordRichPresence(TransportActivity rawGame) => UpdateWith(rawGame); + + internal DiscordRichPresence(DiscordRichPresence other) + { + this.Details = other.Details; + this.State = other.State; + this.Application = other.Application; + this.Instance = other.Instance; + this.LargeImageText = other.LargeImageText; + this.SmallImageText = other.SmallImageText; + this.LargeImage = other.LargeImage; + this.SmallImage = other.SmallImage; + this.CurrentPartySize = other.CurrentPartySize; + this.MaximumPartySize = other.MaximumPartySize; + this.PartyId = other.PartyId; + this.StartTimestamp = other.StartTimestamp; + this.EndTimestamp = other.EndTimestamp; + this.JoinSecret = other.JoinSecret; + this.MatchSecret = other.MatchSecret; + this.SpectateSecret = other.SpectateSecret; + this.Buttons = other.Buttons; + } + + internal void UpdateWith(TransportActivity rawGame) + { + this.Details = rawGame?.Details; + this.State = rawGame?.State; + this.Application = rawGame?.ApplicationId != null ? new DiscordApplication { Id = rawGame.ApplicationId.Value } : null; + this.Instance = rawGame?.Instance; + this.LargeImageText = rawGame?.Assets?.LargeImageText; + this.SmallImageText = rawGame?.Assets?.SmallImageText; + //this.LargeImage = rawGame?.Assets?.LargeImage != null ? new DiscordApplicationAsset { Application = this.Application, Id = rawGame.Assets.LargeImage.Value, Type = ApplicationAssetType.LargeImage } : null; + //this.SmallImage = rawGame?.Assets?.SmallImage != null ? new DiscordApplicationAsset { Application = this.Application, Id = rawGame.Assets.SmallImage.Value, Type = ApplicationAssetType.SmallImage } : null; + this.CurrentPartySize = rawGame?.Party?.Size?.Current; + this.MaximumPartySize = rawGame?.Party?.Size?.Maximum; + if (rawGame?.Party != null && ulong.TryParse(rawGame.Party.Id, NumberStyles.Number, CultureInfo.InvariantCulture, out ulong partyId)) + { + this.PartyId = partyId; + } + + this.StartTimestamp = rawGame?.Timestamps?.Start; + this.EndTimestamp = rawGame?.Timestamps?.End; + this.JoinSecret = rawGame?.Secrets?.Join; + this.MatchSecret = rawGame?.Secrets?.Match; + this.SpectateSecret = rawGame?.Secrets?.Spectate; + this.Buttons = rawGame?.Buttons; + + string? lid = rawGame?.Assets?.LargeImage; + if (lid != null) + { + if (lid.StartsWith("spotify:")) + { + this.LargeImage = new DiscordSpotifyAsset(lid); + } + else if (ulong.TryParse(lid, NumberStyles.Number, CultureInfo.InvariantCulture, out _)) + { + this.LargeImage = new DiscordApplicationAsset + { + Id = lid, + Application = this.Application, + Type = DiscordApplicationAssetType.LargeImage + }; + } + } + + string? sid = rawGame?.Assets?.SmallImage; + if (sid != null) + { + if (sid.StartsWith("spotify:")) + { + this.SmallImage = new DiscordSpotifyAsset(sid); + } + else if (ulong.TryParse(sid, NumberStyles.Number, CultureInfo.InvariantCulture, out _)) + { + this.SmallImage = + new DiscordApplicationAsset + { + Id = sid, + Application = this.Application, + Type = DiscordApplicationAssetType.SmallImage + }; + } + } + } +} + +/// +/// Determines the type of a user activity. +/// +public enum DiscordActivityType +{ + /// + /// Indicates the user is playing a game. + /// + Playing = 0, + + /// + /// Indicates the user is streaming a game. + /// + Streaming = 1, + + /// + /// Indicates the user is listening to something. + /// + ListeningTo = 2, + + /// + /// Indicates the user is watching something. + /// + Watching = 3, + + /// + /// Indicates the current activity is a custom status. + /// + Custom = 4, + + /// + /// Indicates the user is competing in something. + /// + Competing = 5 +} diff --git a/DSharpPlus/Entities/User/DiscordPremiumType.cs b/DSharpPlus/Entities/User/DiscordPremiumType.cs index bb228697ca..cb21b5bea8 100644 --- a/DSharpPlus/Entities/User/DiscordPremiumType.cs +++ b/DSharpPlus/Entities/User/DiscordPremiumType.cs @@ -1,17 +1,17 @@ -namespace DSharpPlus.Entities; - - -/// -/// The type of Nitro subscription on a user's account. -/// -public enum DiscordPremiumType -{ - /// - /// Includes app perks like animated emojis and avatars, but not games. - /// - NitroClassic = 1, - /// - /// Includes app perks as well as the games subscription service. - /// - Nitro = 2 -} +namespace DSharpPlus.Entities; + + +/// +/// The type of Nitro subscription on a user's account. +/// +public enum DiscordPremiumType +{ + /// + /// Includes app perks like animated emojis and avatars, but not games. + /// + NitroClassic = 1, + /// + /// Includes app perks as well as the games subscription service. + /// + Nitro = 2 +} diff --git a/DSharpPlus/Entities/User/DiscordPresence.cs b/DSharpPlus/Entities/User/DiscordPresence.cs index 5e109b66ae..fbb1c2dd97 100644 --- a/DSharpPlus/Entities/User/DiscordPresence.cs +++ b/DSharpPlus/Entities/User/DiscordPresence.cs @@ -1,109 +1,109 @@ -using System.Collections.Generic; -using DSharpPlus.Net.Abstractions; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents a user presence. -/// -public sealed class DiscordPresence -{ - [JsonIgnore] - internal DiscordClient Discord { get; set; } - - // "The user object within this event can be partial, the only field which must be sent is the id field, everything else is optional." - [JsonProperty("user", NullValueHandling = NullValueHandling.Ignore)] - internal TransportUser InternalUser { get; set; } - - /// - /// Gets the user that owns this presence. - /// - [JsonIgnore] - public DiscordUser User - => this.Discord.GetCachedOrEmptyUserInternal(this.InternalUser.Id); - - /// - /// Gets the user's current activity. - /// - [JsonIgnore] - public DiscordActivity Activity { get; internal set; } - - internal TransportActivity RawActivity { get; set; } - - /// - /// Gets the user's current activities. - /// - [JsonIgnore] - public IReadOnlyList Activities => this.internalActivities; - - [JsonIgnore] - internal DiscordActivity[] internalActivities; - - [JsonProperty("activities", NullValueHandling = NullValueHandling.Ignore)] - internal TransportActivity[] RawActivities { get; set; } - - /// - /// Gets this user's status. - /// - [JsonProperty("status", NullValueHandling = NullValueHandling.Ignore)] - public DiscordUserStatus Status { get; internal set; } - - [JsonProperty("guild_id", NullValueHandling = NullValueHandling.Ignore)] - internal ulong GuildId { get; set; } - - /// - /// Gets the guild for which this presence was set. - /// - [JsonIgnore] - public DiscordGuild Guild - => this.GuildId != 0 ? this.Discord.guilds[this.GuildId] : null; - - /// - /// Gets this user's platform-dependent status. - /// - [JsonProperty("client_status", NullValueHandling = NullValueHandling.Ignore)] - public DiscordClientStatus ClientStatus { get; internal set; } - - internal DiscordPresence() { } - - internal DiscordPresence(DiscordPresence other) - { - this.Discord = other.Discord; - if (other.Activity != null) - { - this.Activity = new DiscordActivity(other.Activity); - } - - if (other.Activity != null) - { - this.RawActivity = new TransportActivity(this.Activity); - } - - this.internalActivities = (DiscordActivity[])other.internalActivities?.Clone(); - this.RawActivities = (TransportActivity[])other.RawActivities?.Clone(); - this.Status = other.Status; - this.InternalUser = new TransportUser(other.InternalUser); - } -} - -public sealed class DiscordClientStatus -{ - /// - /// Gets the user's status set for an active desktop (Windows, Linux, Mac) application session. - /// - [JsonProperty("desktop", NullValueHandling = NullValueHandling.Ignore)] - public Optional Desktop { get; internal set; } - - /// - /// Gets the user's status set for an active mobile (iOS, Android) application session. - /// - [JsonProperty("mobile", NullValueHandling = NullValueHandling.Ignore)] - public Optional Mobile { get; internal set; } - - /// - /// Gets the user's status set for an active web (browser, bot account) application session. - /// - [JsonProperty("web", NullValueHandling = NullValueHandling.Ignore)] - public Optional Web { get; internal set; } -} +using System.Collections.Generic; +using DSharpPlus.Net.Abstractions; +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Represents a user presence. +/// +public sealed class DiscordPresence +{ + [JsonIgnore] + internal DiscordClient Discord { get; set; } + + // "The user object within this event can be partial, the only field which must be sent is the id field, everything else is optional." + [JsonProperty("user", NullValueHandling = NullValueHandling.Ignore)] + internal TransportUser InternalUser { get; set; } + + /// + /// Gets the user that owns this presence. + /// + [JsonIgnore] + public DiscordUser User + => this.Discord.GetCachedOrEmptyUserInternal(this.InternalUser.Id); + + /// + /// Gets the user's current activity. + /// + [JsonIgnore] + public DiscordActivity Activity { get; internal set; } + + internal TransportActivity RawActivity { get; set; } + + /// + /// Gets the user's current activities. + /// + [JsonIgnore] + public IReadOnlyList Activities => this.internalActivities; + + [JsonIgnore] + internal DiscordActivity[] internalActivities; + + [JsonProperty("activities", NullValueHandling = NullValueHandling.Ignore)] + internal TransportActivity[] RawActivities { get; set; } + + /// + /// Gets this user's status. + /// + [JsonProperty("status", NullValueHandling = NullValueHandling.Ignore)] + public DiscordUserStatus Status { get; internal set; } + + [JsonProperty("guild_id", NullValueHandling = NullValueHandling.Ignore)] + internal ulong GuildId { get; set; } + + /// + /// Gets the guild for which this presence was set. + /// + [JsonIgnore] + public DiscordGuild Guild + => this.GuildId != 0 ? this.Discord.guilds[this.GuildId] : null; + + /// + /// Gets this user's platform-dependent status. + /// + [JsonProperty("client_status", NullValueHandling = NullValueHandling.Ignore)] + public DiscordClientStatus ClientStatus { get; internal set; } + + internal DiscordPresence() { } + + internal DiscordPresence(DiscordPresence other) + { + this.Discord = other.Discord; + if (other.Activity != null) + { + this.Activity = new DiscordActivity(other.Activity); + } + + if (other.Activity != null) + { + this.RawActivity = new TransportActivity(this.Activity); + } + + this.internalActivities = (DiscordActivity[])other.internalActivities?.Clone(); + this.RawActivities = (TransportActivity[])other.RawActivities?.Clone(); + this.Status = other.Status; + this.InternalUser = new TransportUser(other.InternalUser); + } +} + +public sealed class DiscordClientStatus +{ + /// + /// Gets the user's status set for an active desktop (Windows, Linux, Mac) application session. + /// + [JsonProperty("desktop", NullValueHandling = NullValueHandling.Ignore)] + public Optional Desktop { get; internal set; } + + /// + /// Gets the user's status set for an active mobile (iOS, Android) application session. + /// + [JsonProperty("mobile", NullValueHandling = NullValueHandling.Ignore)] + public Optional Mobile { get; internal set; } + + /// + /// Gets the user's status set for an active web (browser, bot account) application session. + /// + [JsonProperty("web", NullValueHandling = NullValueHandling.Ignore)] + public Optional Web { get; internal set; } +} diff --git a/DSharpPlus/Entities/User/DiscordUser.cs b/DSharpPlus/Entities/User/DiscordUser.cs index 6b736e86f9..25e6294432 100644 --- a/DSharpPlus/Entities/User/DiscordUser.cs +++ b/DSharpPlus/Entities/User/DiscordUser.cs @@ -1,423 +1,423 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Threading.Tasks; -using DSharpPlus.Net; -using DSharpPlus.Net.Abstractions; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents a Discord user. -/// -public class DiscordUser : SnowflakeObject, IEquatable -{ - internal DiscordUser() { } - internal DiscordUser(TransportUser transport) - { - this.Id = transport.Id; - this.Username = transport.Username; - this.Discriminator = transport.Discriminator; - this.GlobalName = transport.GlobalDisplayName; - this.AvatarHash = transport.AvatarHash; - this.bannerColor = transport.BannerColor; - this.BannerHash = transport.BannerHash; - this.IsBot = transport.IsBot; - this.MfaEnabled = transport.MfaEnabled; - this.Verified = transport.Verified; - this.Email = transport.Email; - this.PremiumType = transport.PremiumType; - this.Locale = transport.Locale; - this.Flags = transport.Flags; - this.OAuthFlags = transport.OAuthFlags; - } - - /// - /// Gets this user's username. - /// - [JsonProperty("username", NullValueHandling = NullValueHandling.Ignore)] - public virtual string Username { get; internal set; } - - /// - /// Gets this user's global display name. - /// - /// - /// A global display name differs from a username in that it acts like a nickname, but is not specific to a single guild. - /// Nicknames in servers however still take precedence over global names, which take precedence over usernames. - /// - [JsonProperty("global_name", NullValueHandling = NullValueHandling.Ignore)] - public virtual string GlobalName { get; internal set; } - - /// - /// Gets the user's 4-digit discriminator. - /// - /// - /// As of May 15th, 2023, Discord has begun phasing out discriminators in favor of handles (@username); this property will return "0" for migrated accounts. - /// - [JsonProperty("discriminator", NullValueHandling = NullValueHandling.Ignore)] - public virtual string Discriminator { get; internal set; } - - [JsonIgnore] - internal int DiscriminatorInt - => int.Parse(this.Discriminator, NumberStyles.Integer, CultureInfo.InvariantCulture); - - /// - /// Gets the user's banner color, if set. Mutually exclusive with . - /// - public virtual DiscordColor? BannerColor - => !this.bannerColor.HasValue ? null : new DiscordColor(this.bannerColor.Value); - - [JsonProperty("accent_color")] - internal int? bannerColor; - - /// - /// Gets the user's banner url. - /// - [JsonIgnore] - public string BannerUrl - => string.IsNullOrEmpty(this.BannerHash) ? null : $"https://cdn.discordapp.com/banners/{this.Id}/{this.BannerHash}.{(this.BannerHash.StartsWith('a') ? "gif" : "png")}?size=4096"; - - /// - /// Gets the user's profile banner hash. Mutually exclusive with . - /// - [JsonProperty("banner", NullValueHandling = NullValueHandling.Ignore)] - public virtual string BannerHash { get; internal set; } - - /// - /// Gets the user's avatar hash. - /// - [JsonProperty("avatar", NullValueHandling = NullValueHandling.Ignore)] - public virtual string AvatarHash { get; internal set; } - - /// - /// Gets the user's avatar URL. - /// - [JsonIgnore] - public string AvatarUrl - => !string.IsNullOrWhiteSpace(this.AvatarHash) ? this.AvatarHash.StartsWith("a_") ? $"https://cdn.discordapp.com/avatars/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.AvatarHash}.gif?size=1024" : $"https://cdn.discordapp.com/avatars/{this.Id}/{this.AvatarHash}.png?size=1024" : this.DefaultAvatarUrl; - - /// - /// Gets the URL of default avatar for this user. - /// - [JsonIgnore] - public string DefaultAvatarUrl - => $"https://cdn.discordapp.com/embed/avatars/{(this.DiscriminatorInt is 0 ? (this.Id >> 22) % 6 : (ulong)this.DiscriminatorInt % 5).ToString(CultureInfo.InvariantCulture)}.png?size=1024"; - - /// - /// Gets whether the user is a bot. - /// - [JsonProperty("bot", NullValueHandling = NullValueHandling.Ignore)] - public virtual bool IsBot { get; internal set; } - - /// - /// Gets whether the user has multi-factor authentication enabled. - /// - [JsonProperty("mfa_enabled", NullValueHandling = NullValueHandling.Ignore)] - public virtual bool? MfaEnabled { get; internal set; } - - /// - /// Gets whether the user is an official Discord system user. - /// - [JsonProperty("system", NullValueHandling = NullValueHandling.Ignore)] - public bool? IsSystem { get; internal set; } - - /// - /// Gets whether the user is verified. - /// This is only present in OAuth. - /// - [JsonProperty("verified", NullValueHandling = NullValueHandling.Ignore)] - public virtual bool? Verified { get; internal set; } - - /// - /// Gets the user's email address. - /// This is only present in OAuth. - /// - [JsonProperty("email", NullValueHandling = NullValueHandling.Ignore)] - public virtual string Email { get; internal set; } - - /// - /// Gets the user's premium type. - /// - [JsonProperty("premium_type", NullValueHandling = NullValueHandling.Ignore)] - public virtual DiscordPremiumType? PremiumType { get; internal set; } - - /// - /// Gets the user's chosen language - /// - [JsonProperty("locale", NullValueHandling = NullValueHandling.Ignore)] - public virtual string Locale { get; internal set; } - - /// - /// Gets the user's flags for OAuth. - /// - [JsonProperty("flags", NullValueHandling = NullValueHandling.Ignore)] - public virtual DiscordUserFlags? OAuthFlags { get; internal set; } - - /// - /// Gets the user's flags. - /// - [JsonProperty("public_flags", NullValueHandling = NullValueHandling.Ignore)] - public virtual DiscordUserFlags? Flags { get; internal set; } - - /// - /// Gets the user's mention string. - /// - [JsonIgnore] - public string Mention - => Formatter.Mention(this, this is DiscordMember); - - /// - /// Gets whether this user is the Client which created this object. - /// - [JsonIgnore] - public bool IsCurrent - => this.Id == this.Discord.CurrentUser.Id; - - /// - /// Unbans this user from a guild. - /// - /// Guild to unban this user from. - /// Reason for audit logs. - /// - /// Thrown when the client does not have the permission. - /// Thrown when the user does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public Task UnbanAsync(DiscordGuild guild, string reason = null) - => guild.UnbanMemberAsync(this, reason); - - /// - /// Gets this user's presence. - /// - [JsonIgnore] - public DiscordPresence Presence - => this.Discord is DiscordClient dc ? dc.Presences.TryGetValue(this.Id, out DiscordPresence? presence) ? presence : null : null; - - /// - /// Gets the user's avatar URL, in requested format and size. - /// - /// The image format of the avatar to get. - /// The maximum size of the avatar. Must be a power of two, minimum 16, maximum 4096. - /// The URL of the user's avatar. - public string GetAvatarUrl(MediaFormat imageFormat, ushort imageSize = 1024) - { - if (imageFormat == MediaFormat.Unknown) - { - throw new ArgumentException("You must specify valid image format.", nameof(imageFormat)); - } - - // Makes sure the image size is in between Discord's allowed range. - if (imageSize is < 16 or > 4096) - { - throw new ArgumentOutOfRangeException(nameof(imageSize), "Image Size is not in between 16 and 4096: "); - } - - // Checks to see if the image size is not a power of two. - if (!(imageSize is not 0 && (imageSize & (imageSize - 1)) is 0)) - { - throw new ArgumentOutOfRangeException(nameof(imageSize), "Image size is not a power of two: "); - } - - // Get the string variants of the method parameters to use in the urls. - string stringImageFormat = imageFormat switch - { - MediaFormat.Gif => "gif", - MediaFormat.Jpeg => "jpg", - MediaFormat.Png => "png", - MediaFormat.WebP => "webp", - MediaFormat.Auto => !string.IsNullOrWhiteSpace(this.AvatarHash) ? (this.AvatarHash.StartsWith("a_") ? "gif" : "png") : "png", - _ => throw new ArgumentOutOfRangeException(nameof(imageFormat)), - }; - string stringImageSize = imageSize.ToString(CultureInfo.InvariantCulture); - - // If the avatar hash is set, get their avatar. If it isn't set, grab the default avatar calculated from their discriminator. - if (!string.IsNullOrWhiteSpace(this.AvatarHash)) - { - string userId = this.Id.ToString(CultureInfo.InvariantCulture); - return $"https://cdn.discordapp.com/{Endpoints.AVATARS}/{userId}/{this.AvatarHash}.{stringImageFormat}?size={stringImageSize}"; - } - else - { - // https://discord.com/developers/docs/reference#image-formatting-cdn-endpoints: In the case of the Default User Avatar endpoint, the value for `user_discriminator` in the path should be the user's discriminator `modulo 5—Test#1337` would be `1337 % 5`, which evaluates to 2. - string defaultAvatarType = (this.DiscriminatorInt % 5).ToString(CultureInfo.InvariantCulture); - return $"https://cdn.discordapp.com/embed/{Endpoints.AVATARS}/{defaultAvatarType}.{stringImageFormat}?size={stringImageSize}"; - } - } - - /// - /// Creates a direct message channel to this member. - /// - /// Direct message channel to this member. - /// - /// Thrown when the member has the bot blocked, - /// the member does not share a guild with the bot and does not have the user app installed, - /// or if the member has Allow DM from server members off. - /// - /// 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 ValueTask CreateDmChannelAsync(bool skipCache = false) - { - if (skipCache) - { - return await this.Discord.ApiClient.CreateDmAsync(this.Id); - } - - DiscordDmChannel? dm = default; - - if (this.Discord is DiscordClient dc) - { - dm = dc.privateChannels.Values.FirstOrDefault(x => x.Recipients.FirstOrDefault(y => y is not null && y.Id == this.Id) is not null); - } - - return dm ?? await this.Discord.ApiClient.CreateDmAsync(this.Id); - } - - /// - /// Sends a direct message to this member. Creates a direct message channel if one does not exist already. - /// - /// Content of the message to send. - /// The sent message. - /// - /// Thrown when the member has the bot blocked, - /// the member does not share a guild with the bot and does not have the user app installed, - /// or if the member has Allow DM from server members off. - /// - /// 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 SendMessageAsync(string content) - { - if (this.IsBot && this.Discord.CurrentUser.IsBot) - { - throw new ArgumentException("Bots cannot DM each other."); - } - - DiscordDmChannel chn = await CreateDmChannelAsync(); - return await chn.SendMessageAsync(content); - } - - /// - /// Sends a direct message to this member. Creates a direct message channel if one does not exist already. - /// - /// Embed to attach to the message. - /// The sent message. - /// - /// Thrown when the member has the bot blocked, - /// the member does not share a guild with the bot and does not have the user app installed, - /// or if the member has Allow DM from server members off. - /// - /// 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 SendMessageAsync(DiscordEmbed embed) - { - if (this.IsBot && this.Discord.CurrentUser.IsBot) - { - throw new ArgumentException("Bots cannot DM each other."); - } - - DiscordDmChannel chn = await CreateDmChannelAsync(); - return await chn.SendMessageAsync(embed); - } - - /// - /// Sends a direct message to this member. Creates a direct message channel if one does not exist already. - /// - /// Content of the message to send. - /// Embed to attach to the message. - /// The sent message. - /// - /// Thrown when the member has the bot blocked, - /// the member does not share a guild with the bot and does not have the user app installed, - /// or if the member has Allow DM from server members off. - /// - /// 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 SendMessageAsync(string content, DiscordEmbed embed) - { - if (this.IsBot && this.Discord.CurrentUser.IsBot) - { - throw new ArgumentException("Bots cannot DM each other."); - } - - DiscordDmChannel chn = await CreateDmChannelAsync(); - return await chn.SendMessageAsync(content, embed); - } - - /// - /// Sends a direct message to this member. Creates a direct message channel if one does not exist already. - /// - /// Builder to with the message. - /// The sent message. - /// - /// Thrown when the member has the bot blocked, - /// the member does not share a guild with the bot and does not have the user app installed, - /// or if the member has Allow DM from server members off. - /// - /// 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 SendMessageAsync(DiscordMessageBuilder message) - { - if (this.IsBot && this.Discord.CurrentUser.IsBot) - { - throw new ArgumentException("Bots cannot DM each other."); - } - - DiscordDmChannel chn = await CreateDmChannelAsync(); - return await chn.SendMessageAsync(message); - } - - /// - /// Returns a string representation of this user. - /// - /// String representation of this user. - public override string ToString() => $"User {this.Id}; {this.Username}#{this.Discriminator}"; - - /// - /// Gets the hash code for this . - /// - /// The hash code for this . - public override int GetHashCode() => this.Id.GetHashCode(); - - /// - /// 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) => Equals(obj as DiscordUser); - - /// - /// Checks whether this is equal to another . - /// - /// to compare to. - /// Whether the is equal to this . - public bool Equals(DiscordUser? other) => this.Id == other?.Id; - - /// - /// Gets whether the two objects are equal. - /// - /// First user to compare. - /// Second user to compare. - /// Whether the two users are equal. - public static bool operator ==(DiscordUser? obj, DiscordUser? other) => obj?.Equals(other) ?? other is null; - - /// - /// Gets whether the two objects are not equal. - /// - /// First user to compare. - /// Second user to compare. - /// Whether the two users are not equal. - public static bool operator !=(DiscordUser? obj, DiscordUser? other) => !(obj == other); -} - -internal class DiscordUserComparer : IEqualityComparer -{ - public bool Equals(DiscordUser x, DiscordUser y) => x.Equals(y); - - public int GetHashCode(DiscordUser obj) => obj.Id.GetHashCode(); -} +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; +using DSharpPlus.Net; +using DSharpPlus.Net.Abstractions; +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Represents a Discord user. +/// +public class DiscordUser : SnowflakeObject, IEquatable +{ + internal DiscordUser() { } + internal DiscordUser(TransportUser transport) + { + this.Id = transport.Id; + this.Username = transport.Username; + this.Discriminator = transport.Discriminator; + this.GlobalName = transport.GlobalDisplayName; + this.AvatarHash = transport.AvatarHash; + this.bannerColor = transport.BannerColor; + this.BannerHash = transport.BannerHash; + this.IsBot = transport.IsBot; + this.MfaEnabled = transport.MfaEnabled; + this.Verified = transport.Verified; + this.Email = transport.Email; + this.PremiumType = transport.PremiumType; + this.Locale = transport.Locale; + this.Flags = transport.Flags; + this.OAuthFlags = transport.OAuthFlags; + } + + /// + /// Gets this user's username. + /// + [JsonProperty("username", NullValueHandling = NullValueHandling.Ignore)] + public virtual string Username { get; internal set; } + + /// + /// Gets this user's global display name. + /// + /// + /// A global display name differs from a username in that it acts like a nickname, but is not specific to a single guild. + /// Nicknames in servers however still take precedence over global names, which take precedence over usernames. + /// + [JsonProperty("global_name", NullValueHandling = NullValueHandling.Ignore)] + public virtual string GlobalName { get; internal set; } + + /// + /// Gets the user's 4-digit discriminator. + /// + /// + /// As of May 15th, 2023, Discord has begun phasing out discriminators in favor of handles (@username); this property will return "0" for migrated accounts. + /// + [JsonProperty("discriminator", NullValueHandling = NullValueHandling.Ignore)] + public virtual string Discriminator { get; internal set; } + + [JsonIgnore] + internal int DiscriminatorInt + => int.Parse(this.Discriminator, NumberStyles.Integer, CultureInfo.InvariantCulture); + + /// + /// Gets the user's banner color, if set. Mutually exclusive with . + /// + public virtual DiscordColor? BannerColor + => !this.bannerColor.HasValue ? null : new DiscordColor(this.bannerColor.Value); + + [JsonProperty("accent_color")] + internal int? bannerColor; + + /// + /// Gets the user's banner url. + /// + [JsonIgnore] + public string BannerUrl + => string.IsNullOrEmpty(this.BannerHash) ? null : $"https://cdn.discordapp.com/banners/{this.Id}/{this.BannerHash}.{(this.BannerHash.StartsWith('a') ? "gif" : "png")}?size=4096"; + + /// + /// Gets the user's profile banner hash. Mutually exclusive with . + /// + [JsonProperty("banner", NullValueHandling = NullValueHandling.Ignore)] + public virtual string BannerHash { get; internal set; } + + /// + /// Gets the user's avatar hash. + /// + [JsonProperty("avatar", NullValueHandling = NullValueHandling.Ignore)] + public virtual string AvatarHash { get; internal set; } + + /// + /// Gets the user's avatar URL. + /// + [JsonIgnore] + public string AvatarUrl + => !string.IsNullOrWhiteSpace(this.AvatarHash) ? this.AvatarHash.StartsWith("a_") ? $"https://cdn.discordapp.com/avatars/{this.Id.ToString(CultureInfo.InvariantCulture)}/{this.AvatarHash}.gif?size=1024" : $"https://cdn.discordapp.com/avatars/{this.Id}/{this.AvatarHash}.png?size=1024" : this.DefaultAvatarUrl; + + /// + /// Gets the URL of default avatar for this user. + /// + [JsonIgnore] + public string DefaultAvatarUrl + => $"https://cdn.discordapp.com/embed/avatars/{(this.DiscriminatorInt is 0 ? (this.Id >> 22) % 6 : (ulong)this.DiscriminatorInt % 5).ToString(CultureInfo.InvariantCulture)}.png?size=1024"; + + /// + /// Gets whether the user is a bot. + /// + [JsonProperty("bot", NullValueHandling = NullValueHandling.Ignore)] + public virtual bool IsBot { get; internal set; } + + /// + /// Gets whether the user has multi-factor authentication enabled. + /// + [JsonProperty("mfa_enabled", NullValueHandling = NullValueHandling.Ignore)] + public virtual bool? MfaEnabled { get; internal set; } + + /// + /// Gets whether the user is an official Discord system user. + /// + [JsonProperty("system", NullValueHandling = NullValueHandling.Ignore)] + public bool? IsSystem { get; internal set; } + + /// + /// Gets whether the user is verified. + /// This is only present in OAuth. + /// + [JsonProperty("verified", NullValueHandling = NullValueHandling.Ignore)] + public virtual bool? Verified { get; internal set; } + + /// + /// Gets the user's email address. + /// This is only present in OAuth. + /// + [JsonProperty("email", NullValueHandling = NullValueHandling.Ignore)] + public virtual string Email { get; internal set; } + + /// + /// Gets the user's premium type. + /// + [JsonProperty("premium_type", NullValueHandling = NullValueHandling.Ignore)] + public virtual DiscordPremiumType? PremiumType { get; internal set; } + + /// + /// Gets the user's chosen language + /// + [JsonProperty("locale", NullValueHandling = NullValueHandling.Ignore)] + public virtual string Locale { get; internal set; } + + /// + /// Gets the user's flags for OAuth. + /// + [JsonProperty("flags", NullValueHandling = NullValueHandling.Ignore)] + public virtual DiscordUserFlags? OAuthFlags { get; internal set; } + + /// + /// Gets the user's flags. + /// + [JsonProperty("public_flags", NullValueHandling = NullValueHandling.Ignore)] + public virtual DiscordUserFlags? Flags { get; internal set; } + + /// + /// Gets the user's mention string. + /// + [JsonIgnore] + public string Mention + => Formatter.Mention(this, this is DiscordMember); + + /// + /// Gets whether this user is the Client which created this object. + /// + [JsonIgnore] + public bool IsCurrent + => this.Id == this.Discord.CurrentUser.Id; + + /// + /// Unbans this user from a guild. + /// + /// Guild to unban this user from. + /// Reason for audit logs. + /// + /// Thrown when the client does not have the permission. + /// Thrown when the user does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public Task UnbanAsync(DiscordGuild guild, string reason = null) + => guild.UnbanMemberAsync(this, reason); + + /// + /// Gets this user's presence. + /// + [JsonIgnore] + public DiscordPresence Presence + => this.Discord is DiscordClient dc ? dc.Presences.TryGetValue(this.Id, out DiscordPresence? presence) ? presence : null : null; + + /// + /// Gets the user's avatar URL, in requested format and size. + /// + /// The image format of the avatar to get. + /// The maximum size of the avatar. Must be a power of two, minimum 16, maximum 4096. + /// The URL of the user's avatar. + public string GetAvatarUrl(MediaFormat imageFormat, ushort imageSize = 1024) + { + if (imageFormat == MediaFormat.Unknown) + { + throw new ArgumentException("You must specify valid image format.", nameof(imageFormat)); + } + + // Makes sure the image size is in between Discord's allowed range. + if (imageSize is < 16 or > 4096) + { + throw new ArgumentOutOfRangeException(nameof(imageSize), "Image Size is not in between 16 and 4096: "); + } + + // Checks to see if the image size is not a power of two. + if (!(imageSize is not 0 && (imageSize & (imageSize - 1)) is 0)) + { + throw new ArgumentOutOfRangeException(nameof(imageSize), "Image size is not a power of two: "); + } + + // Get the string variants of the method parameters to use in the urls. + string stringImageFormat = imageFormat switch + { + MediaFormat.Gif => "gif", + MediaFormat.Jpeg => "jpg", + MediaFormat.Png => "png", + MediaFormat.WebP => "webp", + MediaFormat.Auto => !string.IsNullOrWhiteSpace(this.AvatarHash) ? (this.AvatarHash.StartsWith("a_") ? "gif" : "png") : "png", + _ => throw new ArgumentOutOfRangeException(nameof(imageFormat)), + }; + string stringImageSize = imageSize.ToString(CultureInfo.InvariantCulture); + + // If the avatar hash is set, get their avatar. If it isn't set, grab the default avatar calculated from their discriminator. + if (!string.IsNullOrWhiteSpace(this.AvatarHash)) + { + string userId = this.Id.ToString(CultureInfo.InvariantCulture); + return $"https://cdn.discordapp.com/{Endpoints.AVATARS}/{userId}/{this.AvatarHash}.{stringImageFormat}?size={stringImageSize}"; + } + else + { + // https://discord.com/developers/docs/reference#image-formatting-cdn-endpoints: In the case of the Default User Avatar endpoint, the value for `user_discriminator` in the path should be the user's discriminator `modulo 5—Test#1337` would be `1337 % 5`, which evaluates to 2. + string defaultAvatarType = (this.DiscriminatorInt % 5).ToString(CultureInfo.InvariantCulture); + return $"https://cdn.discordapp.com/embed/{Endpoints.AVATARS}/{defaultAvatarType}.{stringImageFormat}?size={stringImageSize}"; + } + } + + /// + /// Creates a direct message channel to this member. + /// + /// Direct message channel to this member. + /// + /// Thrown when the member has the bot blocked, + /// the member does not share a guild with the bot and does not have the user app installed, + /// or if the member has Allow DM from server members off. + /// + /// 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 ValueTask CreateDmChannelAsync(bool skipCache = false) + { + if (skipCache) + { + return await this.Discord.ApiClient.CreateDmAsync(this.Id); + } + + DiscordDmChannel? dm = default; + + if (this.Discord is DiscordClient dc) + { + dm = dc.privateChannels.Values.FirstOrDefault(x => x.Recipients.FirstOrDefault(y => y is not null && y.Id == this.Id) is not null); + } + + return dm ?? await this.Discord.ApiClient.CreateDmAsync(this.Id); + } + + /// + /// Sends a direct message to this member. Creates a direct message channel if one does not exist already. + /// + /// Content of the message to send. + /// The sent message. + /// + /// Thrown when the member has the bot blocked, + /// the member does not share a guild with the bot and does not have the user app installed, + /// or if the member has Allow DM from server members off. + /// + /// 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 SendMessageAsync(string content) + { + if (this.IsBot && this.Discord.CurrentUser.IsBot) + { + throw new ArgumentException("Bots cannot DM each other."); + } + + DiscordDmChannel chn = await CreateDmChannelAsync(); + return await chn.SendMessageAsync(content); + } + + /// + /// Sends a direct message to this member. Creates a direct message channel if one does not exist already. + /// + /// Embed to attach to the message. + /// The sent message. + /// + /// Thrown when the member has the bot blocked, + /// the member does not share a guild with the bot and does not have the user app installed, + /// or if the member has Allow DM from server members off. + /// + /// 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 SendMessageAsync(DiscordEmbed embed) + { + if (this.IsBot && this.Discord.CurrentUser.IsBot) + { + throw new ArgumentException("Bots cannot DM each other."); + } + + DiscordDmChannel chn = await CreateDmChannelAsync(); + return await chn.SendMessageAsync(embed); + } + + /// + /// Sends a direct message to this member. Creates a direct message channel if one does not exist already. + /// + /// Content of the message to send. + /// Embed to attach to the message. + /// The sent message. + /// + /// Thrown when the member has the bot blocked, + /// the member does not share a guild with the bot and does not have the user app installed, + /// or if the member has Allow DM from server members off. + /// + /// 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 SendMessageAsync(string content, DiscordEmbed embed) + { + if (this.IsBot && this.Discord.CurrentUser.IsBot) + { + throw new ArgumentException("Bots cannot DM each other."); + } + + DiscordDmChannel chn = await CreateDmChannelAsync(); + return await chn.SendMessageAsync(content, embed); + } + + /// + /// Sends a direct message to this member. Creates a direct message channel if one does not exist already. + /// + /// Builder to with the message. + /// The sent message. + /// + /// Thrown when the member has the bot blocked, + /// the member does not share a guild with the bot and does not have the user app installed, + /// or if the member has Allow DM from server members off. + /// + /// 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 SendMessageAsync(DiscordMessageBuilder message) + { + if (this.IsBot && this.Discord.CurrentUser.IsBot) + { + throw new ArgumentException("Bots cannot DM each other."); + } + + DiscordDmChannel chn = await CreateDmChannelAsync(); + return await chn.SendMessageAsync(message); + } + + /// + /// Returns a string representation of this user. + /// + /// String representation of this user. + public override string ToString() => $"User {this.Id}; {this.Username}#{this.Discriminator}"; + + /// + /// Gets the hash code for this . + /// + /// The hash code for this . + public override int GetHashCode() => this.Id.GetHashCode(); + + /// + /// 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) => Equals(obj as DiscordUser); + + /// + /// Checks whether this is equal to another . + /// + /// to compare to. + /// Whether the is equal to this . + public bool Equals(DiscordUser? other) => this.Id == other?.Id; + + /// + /// Gets whether the two objects are equal. + /// + /// First user to compare. + /// Second user to compare. + /// Whether the two users are equal. + public static bool operator ==(DiscordUser? obj, DiscordUser? other) => obj?.Equals(other) ?? other is null; + + /// + /// Gets whether the two objects are not equal. + /// + /// First user to compare. + /// Second user to compare. + /// Whether the two users are not equal. + public static bool operator !=(DiscordUser? obj, DiscordUser? other) => !(obj == other); +} + +internal class DiscordUserComparer : IEqualityComparer +{ + public bool Equals(DiscordUser x, DiscordUser y) => x.Equals(y); + + public int GetHashCode(DiscordUser obj) => obj.Id.GetHashCode(); +} diff --git a/DSharpPlus/Entities/User/DiscordUserFlags.cs b/DSharpPlus/Entities/User/DiscordUserFlags.cs index 1a2d371a79..ccf640c3c6 100644 --- a/DSharpPlus/Entities/User/DiscordUserFlags.cs +++ b/DSharpPlus/Entities/User/DiscordUserFlags.cs @@ -1,95 +1,95 @@ -using System; - -namespace DSharpPlus.Entities; - -/// -/// Represents additional details of a users account. -/// -[Flags] -public enum DiscordUserFlags -{ - /// - /// The user has no flags. - /// - None = 0, - - /// - /// The user is a Discord employee. - /// - DiscordEmployee = 1 << 0, - - /// - /// The user is a Discord partner. - /// - DiscordPartner = 1 << 1, - - /// - /// The user has the HypeSquad badge. - /// - HypeSquadEvents = 1 << 2, - - /// - /// The user reached the first bug hunter tier. - /// - BugHunterLevelOne = 1 << 3, - - /// - /// The user is a member of house bravery. - /// - HouseBravery = 1 << 6, - - /// - /// The user is a member of house brilliance. - /// - HouseBrilliance = 1 << 7, - - /// - /// The user is a member of house balance. - /// - HouseBalance = 1 << 8, - - /// - /// The user has the early supporter badge. - /// - EarlySupporter = 1 << 9, - - /// - /// Whether the user is apart of a Discord developer team. - /// - TeamUser = 1 << 10, - - /// - /// The user reached the second bug hunter tier. - /// - BugHunterLevelTwo = 1 << 14, - - /// - /// Whether the user is an official system user. - /// - System = 1 << 12, - - /// - /// The user is a verified bot. - /// - VerifiedBot = 1 << 16, - - /// - /// The user is a verified bot developer. - /// - VerifiedBotDeveloper = 1 << 17, - - /// - /// The user is a discord certified moderator. - /// - DiscordCertifiedModerator = 1 << 18, - - /// - /// The bot receives interactions via HTTP. - /// - HttpInteractionsBot = 1 << 19, - - /// - /// The user is an active bot developer. - /// - ActiveDeveloper = 1 << 22 -} +using System; + +namespace DSharpPlus.Entities; + +/// +/// Represents additional details of a users account. +/// +[Flags] +public enum DiscordUserFlags +{ + /// + /// The user has no flags. + /// + None = 0, + + /// + /// The user is a Discord employee. + /// + DiscordEmployee = 1 << 0, + + /// + /// The user is a Discord partner. + /// + DiscordPartner = 1 << 1, + + /// + /// The user has the HypeSquad badge. + /// + HypeSquadEvents = 1 << 2, + + /// + /// The user reached the first bug hunter tier. + /// + BugHunterLevelOne = 1 << 3, + + /// + /// The user is a member of house bravery. + /// + HouseBravery = 1 << 6, + + /// + /// The user is a member of house brilliance. + /// + HouseBrilliance = 1 << 7, + + /// + /// The user is a member of house balance. + /// + HouseBalance = 1 << 8, + + /// + /// The user has the early supporter badge. + /// + EarlySupporter = 1 << 9, + + /// + /// Whether the user is apart of a Discord developer team. + /// + TeamUser = 1 << 10, + + /// + /// The user reached the second bug hunter tier. + /// + BugHunterLevelTwo = 1 << 14, + + /// + /// Whether the user is an official system user. + /// + System = 1 << 12, + + /// + /// The user is a verified bot. + /// + VerifiedBot = 1 << 16, + + /// + /// The user is a verified bot developer. + /// + VerifiedBotDeveloper = 1 << 17, + + /// + /// The user is a discord certified moderator. + /// + DiscordCertifiedModerator = 1 << 18, + + /// + /// The bot receives interactions via HTTP. + /// + HttpInteractionsBot = 1 << 19, + + /// + /// The user is an active bot developer. + /// + ActiveDeveloper = 1 << 22 +} diff --git a/DSharpPlus/Entities/Voice/DiscordVoiceRegion.cs b/DSharpPlus/Entities/Voice/DiscordVoiceRegion.cs index 76e7aada4f..5e64af871e 100644 --- a/DSharpPlus/Entities/Voice/DiscordVoiceRegion.cs +++ b/DSharpPlus/Entities/Voice/DiscordVoiceRegion.cs @@ -1,94 +1,94 @@ -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents information about a Discord voice server region. -/// -public class DiscordVoiceRegion -{ - /// - /// Gets the unique ID for the region. - /// - [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)] - public string Id { get; internal set; } - - /// - /// Gets the name of the region. - /// - [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] - public string Name { get; internal set; } - - /// - /// Gets an example server hostname for this region. - /// - [JsonProperty("sample_hostname", NullValueHandling = NullValueHandling.Ignore)] - public string SampleHostname { get; internal set; } - - /// - /// Gets an example server port for this region. - /// - [JsonProperty("sample_port", NullValueHandling = NullValueHandling.Ignore)] - public int SamplePort { get; internal set; } - - /// - /// Gets whether this is a VIP-only region. - /// - [JsonProperty("vip", NullValueHandling = NullValueHandling.Ignore)] - public bool IsVIP { get; internal set; } - - /// - /// Gets whether this region is the most optimal for the current user. - /// - [JsonProperty("optimal", NullValueHandling = NullValueHandling.Ignore)] - public bool IsOptimal { get; internal set; } - - /// - /// Gets whether this voice region is deprecated. - /// - [JsonProperty("deprecated", NullValueHandling = NullValueHandling.Ignore)] - public bool IsDeprecated { get; internal set; } - - /// - /// Gets whether this is a custom voice region. - /// - [JsonProperty("custom", NullValueHandling = NullValueHandling.Ignore)] - public bool IsCustom { get; internal set; } - - /// - /// Gets whether two s are equal. - /// - /// The region to compare with. - /// - public bool Equals(DiscordVoiceRegion region) - => this == region; - - public override bool Equals(object obj) => Equals(obj as DiscordVoiceRegion); - - public override int GetHashCode() => this.Id.GetHashCode(); - - /// - /// Gets whether the two objects are equal. - /// - /// First voice region to compare. - /// Second voice region to compare. - /// Whether the two voice regions are equal. - public static bool operator ==(DiscordVoiceRegion left, DiscordVoiceRegion right) - { - object? o1 = left; - object? o2 = right; - - return (o1 != null || o2 == null) && (o1 == null || o2 != null) && ((o1 == null && o2 == null) || left.Id == right.Id); - } - - /// - /// Gets whether the two objects are not equal. - /// - /// First voice region to compare. - /// Second voice region to compare. - /// Whether the two voice regions are not equal. - public static bool operator !=(DiscordVoiceRegion left, DiscordVoiceRegion right) - => !(left == right); - - internal DiscordVoiceRegion() { } -} +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Represents information about a Discord voice server region. +/// +public class DiscordVoiceRegion +{ + /// + /// Gets the unique ID for the region. + /// + [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)] + public string Id { get; internal set; } + + /// + /// Gets the name of the region. + /// + [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] + public string Name { get; internal set; } + + /// + /// Gets an example server hostname for this region. + /// + [JsonProperty("sample_hostname", NullValueHandling = NullValueHandling.Ignore)] + public string SampleHostname { get; internal set; } + + /// + /// Gets an example server port for this region. + /// + [JsonProperty("sample_port", NullValueHandling = NullValueHandling.Ignore)] + public int SamplePort { get; internal set; } + + /// + /// Gets whether this is a VIP-only region. + /// + [JsonProperty("vip", NullValueHandling = NullValueHandling.Ignore)] + public bool IsVIP { get; internal set; } + + /// + /// Gets whether this region is the most optimal for the current user. + /// + [JsonProperty("optimal", NullValueHandling = NullValueHandling.Ignore)] + public bool IsOptimal { get; internal set; } + + /// + /// Gets whether this voice region is deprecated. + /// + [JsonProperty("deprecated", NullValueHandling = NullValueHandling.Ignore)] + public bool IsDeprecated { get; internal set; } + + /// + /// Gets whether this is a custom voice region. + /// + [JsonProperty("custom", NullValueHandling = NullValueHandling.Ignore)] + public bool IsCustom { get; internal set; } + + /// + /// Gets whether two s are equal. + /// + /// The region to compare with. + /// + public bool Equals(DiscordVoiceRegion region) + => this == region; + + public override bool Equals(object obj) => Equals(obj as DiscordVoiceRegion); + + public override int GetHashCode() => this.Id.GetHashCode(); + + /// + /// Gets whether the two objects are equal. + /// + /// First voice region to compare. + /// Second voice region to compare. + /// Whether the two voice regions are equal. + public static bool operator ==(DiscordVoiceRegion left, DiscordVoiceRegion right) + { + object? o1 = left; + object? o2 = right; + + return (o1 != null || o2 == null) && (o1 == null || o2 != null) && ((o1 == null && o2 == null) || left.Id == right.Id); + } + + /// + /// Gets whether the two objects are not equal. + /// + /// First voice region to compare. + /// Second voice region to compare. + /// Whether the two voice regions are not equal. + public static bool operator !=(DiscordVoiceRegion left, DiscordVoiceRegion right) + => !(left == right); + + internal DiscordVoiceRegion() { } +} diff --git a/DSharpPlus/Entities/Voice/DiscordVoiceState.cs b/DSharpPlus/Entities/Voice/DiscordVoiceState.cs index e890dad2fb..6f10c26e1b 100644 --- a/DSharpPlus/Entities/Voice/DiscordVoiceState.cs +++ b/DSharpPlus/Entities/Voice/DiscordVoiceState.cs @@ -1,217 +1,216 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using System.Threading.Tasks; -using DSharpPlus.Net.Abstractions; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents a Discord voice state. -/// -public class DiscordVoiceState -{ - [JsonIgnore] - internal BaseDiscordClient Discord { get; set; } - - /// - /// Gets ID of the guild this voice state is associated with. - /// - [JsonProperty("guild_id", NullValueHandling = NullValueHandling.Ignore)] - public ulong? GuildId { get; init; } - - /// - /// Gets ID of the channel this user is connected to. - /// - [JsonProperty("channel_id", NullValueHandling = NullValueHandling.Include)] - public ulong? ChannelId { get; init; } - - /// - /// Gets ID of the user to which this voice state belongs. - /// - [JsonProperty("user_id", NullValueHandling = NullValueHandling.Ignore)] - internal ulong UserId { get; init; } - - /// - /// Gets ID of the session of this voice state. - /// - [JsonProperty("session_id", NullValueHandling = NullValueHandling.Ignore)] - public string SessionId { get; internal init; } - - /// - /// Gets whether this user is deafened. - /// - [JsonProperty("deaf", NullValueHandling = NullValueHandling.Ignore)] - public bool IsServerDeafened { get; internal init; } - - /// - /// Gets whether this user is muted. - /// - [JsonProperty("mute", NullValueHandling = NullValueHandling.Ignore)] - public bool IsServerMuted { get; internal init; } - - /// - /// Gets whether this user is locally deafened. - /// - [JsonProperty("self_deaf", NullValueHandling = NullValueHandling.Ignore)] - public bool IsSelfDeafened { get; internal init; } - - /// - /// Gets whether this user is locally muted. - /// - [JsonProperty("self_mute", NullValueHandling = NullValueHandling.Ignore)] - public bool IsSelfMuted { get; internal init; } - - /// - /// Gets whether this user's camera is enabled. - /// - [JsonProperty("self_video", NullValueHandling = NullValueHandling.Ignore)] - public bool IsSelfVideo { get; internal init; } - - /// - /// Gets whether this user is using the Go Live feature. - /// - [JsonProperty("self_stream", NullValueHandling = NullValueHandling.Ignore)] - public bool IsSelfStream { get; internal init; } - - /// - /// Gets whether the current user has suppressed this user. - /// - [JsonProperty("suppress", NullValueHandling = NullValueHandling.Ignore)] - public bool IsSuppressed { get; internal init; } - - /// - /// Gets the time at which this user requested to speak. - /// - [JsonProperty("request_to_speak_timestamp", NullValueHandling = NullValueHandling.Ignore)] - public DateTimeOffset? RequestToSpeakTimestamp { get; internal init; } - - [JsonProperty("member", NullValueHandling = NullValueHandling.Ignore)] - internal TransportMember TransportMember { get; init; } - - /// - /// Gets the guild associated with this voice state. - /// - /// - /// - public async ValueTask GetGuildAsync(bool skipCache = false) - { - if (this.GuildId is null) - { - return null; - } - - if (skipCache) - { - return await this.Discord.ApiClient.GetGuildAsync(this.GuildId.Value, false); - } - - if (this.Discord.Guilds.TryGetValue(this.GuildId.Value, out DiscordGuild? guild)) - { - return guild; - } - - guild = await this.Discord.ApiClient.GetGuildAsync(this.GuildId.Value, false); - - if (this.Discord is DiscordClient dc) - { - dc.guilds.TryAdd(this.GuildId.Value, guild); - } - - return guild; - } - - /// - /// Gets the member associated with this voice state. - /// - /// Whether to skip the cache and always fetch the member from the API. - /// Returns the member associated with this voice state. Null if the voice state is not associated with a guild. - public async ValueTask GetUserAsync(bool skipCache = false) - { - if (this.GuildId is null) - { - return null; - } - - if (skipCache) - { - return await this.Discord.ApiClient.GetGuildMemberAsync(this.GuildId.Value, this.UserId); - } - - DiscordGuild? guild = await GetGuildAsync(skipCache); - - if (guild is null) - { - return null; - } - - if (guild.Members.TryGetValue(this.UserId, out DiscordMember? member)) - { - return member; - } - - member = new DiscordMember(this.TransportMember) { Discord = this.Discord }; - - if (this.Discord is DiscordClient dc) - { - dc.guilds.TryAdd(this.GuildId.Value, guild); - } - - return member; - } - - /// - /// Gets the channel associated with this voice state. - /// - /// Whether to skip the cache and always fetch the channel from the API. - /// Returns the channel associated with this voice state. Null if the voice state is not associated with a guild. - public async ValueTask GetChannelAsync(bool skipCache = false) - { - if (this.ChannelId is null || this.GuildId is null) - { - return null; - } - - if (skipCache) - { - return await this.Discord.ApiClient.GetChannelAsync(this.ChannelId.Value); - } - - DiscordGuild? guild = await GetGuildAsync(skipCache); - - if (guild is null) - { - return null; - } - - if (guild.Channels.TryGetValue(this.ChannelId.Value, out DiscordChannel? channel)) - { - return channel; - } - - channel = await this.Discord.ApiClient.GetChannelAsync(this.ChannelId.Value); - - if (this.Discord is DiscordClient dc) - { - dc.guilds.TryAdd(this.GuildId.Value, guild); - } - - return channel; - } - - internal DiscordVoiceState() { } - internal DiscordVoiceState(DiscordMember member) - { - this.Discord = (DiscordClient)member.Discord; - this.UserId = member.Id; - this.ChannelId = 0; - this.GuildId = member.guild_id; - this.IsServerDeafened = member.IsDeafened; - this.IsServerMuted = member.IsMuted; - - // Values not filled out are values that are not known from a DiscordMember - } - - public override string ToString() => $"{this.UserId.ToString(CultureInfo.InvariantCulture)} in {this.GuildId?.ToString(CultureInfo.InvariantCulture)}"; -} +using System; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Threading.Tasks; +using DSharpPlus.Net.Abstractions; +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Represents a Discord voice state. +/// +public class DiscordVoiceState +{ + [JsonIgnore] + internal BaseDiscordClient Discord { get; set; } + + /// + /// Gets ID of the guild this voice state is associated with. + /// + [JsonProperty("guild_id", NullValueHandling = NullValueHandling.Ignore)] + public ulong? GuildId { get; init; } + + /// + /// Gets ID of the channel this user is connected to. + /// + [JsonProperty("channel_id", NullValueHandling = NullValueHandling.Include)] + public ulong? ChannelId { get; init; } + + /// + /// Gets ID of the user to which this voice state belongs. + /// + [JsonProperty("user_id", NullValueHandling = NullValueHandling.Ignore)] + internal ulong UserId { get; init; } + + /// + /// Gets ID of the session of this voice state. + /// + [JsonProperty("session_id", NullValueHandling = NullValueHandling.Ignore)] + public string SessionId { get; internal init; } + + /// + /// Gets whether this user is deafened. + /// + [JsonProperty("deaf", NullValueHandling = NullValueHandling.Ignore)] + public bool IsServerDeafened { get; internal init; } + + /// + /// Gets whether this user is muted. + /// + [JsonProperty("mute", NullValueHandling = NullValueHandling.Ignore)] + public bool IsServerMuted { get; internal init; } + + /// + /// Gets whether this user is locally deafened. + /// + [JsonProperty("self_deaf", NullValueHandling = NullValueHandling.Ignore)] + public bool IsSelfDeafened { get; internal init; } + + /// + /// Gets whether this user is locally muted. + /// + [JsonProperty("self_mute", NullValueHandling = NullValueHandling.Ignore)] + public bool IsSelfMuted { get; internal init; } + + /// + /// Gets whether this user's camera is enabled. + /// + [JsonProperty("self_video", NullValueHandling = NullValueHandling.Ignore)] + public bool IsSelfVideo { get; internal init; } + + /// + /// Gets whether this user is using the Go Live feature. + /// + [JsonProperty("self_stream", NullValueHandling = NullValueHandling.Ignore)] + public bool IsSelfStream { get; internal init; } + + /// + /// Gets whether the current user has suppressed this user. + /// + [JsonProperty("suppress", NullValueHandling = NullValueHandling.Ignore)] + public bool IsSuppressed { get; internal init; } + + /// + /// Gets the time at which this user requested to speak. + /// + [JsonProperty("request_to_speak_timestamp", NullValueHandling = NullValueHandling.Ignore)] + public DateTimeOffset? RequestToSpeakTimestamp { get; internal init; } + + [JsonProperty("member", NullValueHandling = NullValueHandling.Ignore)] + internal TransportMember TransportMember { get; init; } + + /// + /// Gets the guild associated with this voice state. + /// + /// Returns the guild associated with this voicestate + public async ValueTask GetGuildAsync(bool skipCache = false) + { + if (this.GuildId is null) + { + return null; + } + + if (skipCache) + { + return await this.Discord.ApiClient.GetGuildAsync(this.GuildId.Value, false); + } + + if (this.Discord.Guilds.TryGetValue(this.GuildId.Value, out DiscordGuild? guild)) + { + return guild; + } + + guild = await this.Discord.ApiClient.GetGuildAsync(this.GuildId.Value, false); + + if (this.Discord is DiscordClient dc) + { + dc.guilds.TryAdd(this.GuildId.Value, guild); + } + + return guild; + } + + /// + /// Gets the member associated with this voice state. + /// + /// Whether to skip the cache and always fetch the member from the API. + /// Returns the member associated with this voice state. Null if the voice state is not associated with a guild. + public async ValueTask GetUserAsync(bool skipCache = false) + { + if (this.GuildId is null) + { + return null; + } + + if (skipCache) + { + return await this.Discord.ApiClient.GetGuildMemberAsync(this.GuildId.Value, this.UserId); + } + + DiscordGuild? guild = await GetGuildAsync(skipCache); + + if (guild is null) + { + return null; + } + + if (guild.Members.TryGetValue(this.UserId, out DiscordMember? member)) + { + return member; + } + + member = new DiscordMember(this.TransportMember) { Discord = this.Discord }; + + if (this.Discord is DiscordClient dc) + { + dc.guilds.TryAdd(this.GuildId.Value, guild); + } + + return member; + } + + /// + /// Gets the channel associated with this voice state. + /// + /// Whether to skip the cache and always fetch the channel from the API. + /// Returns the channel associated with this voice state. Null if the voice state is not associated with a guild. + public async ValueTask GetChannelAsync(bool skipCache = false) + { + if (this.ChannelId is null || this.GuildId is null) + { + return null; + } + + if (skipCache) + { + return await this.Discord.ApiClient.GetChannelAsync(this.ChannelId.Value); + } + + DiscordGuild? guild = await GetGuildAsync(skipCache); + + if (guild is null) + { + return null; + } + + if (guild.Channels.TryGetValue(this.ChannelId.Value, out DiscordChannel? channel)) + { + return channel; + } + + channel = await this.Discord.ApiClient.GetChannelAsync(this.ChannelId.Value); + + if (this.Discord is DiscordClient dc) + { + dc.guilds.TryAdd(this.GuildId.Value, guild); + } + + return channel; + } + + internal DiscordVoiceState() { } + internal DiscordVoiceState(DiscordMember member) + { + this.Discord = (DiscordClient)member.Discord; + this.UserId = member.Id; + this.ChannelId = 0; + this.GuildId = member.guild_id; + this.IsServerDeafened = member.IsDeafened; + this.IsServerMuted = member.IsMuted; + + // Values not filled out are values that are not known from a DiscordMember + } + + public override string ToString() => $"{this.UserId.ToString(CultureInfo.InvariantCulture)} in {this.GuildId?.ToString(CultureInfo.InvariantCulture)}"; +} diff --git a/DSharpPlus/Entities/Webhook/DiscordWebhook.cs b/DSharpPlus/Entities/Webhook/DiscordWebhook.cs index b06da54482..4b81024091 100644 --- a/DSharpPlus/Entities/Webhook/DiscordWebhook.cs +++ b/DSharpPlus/Entities/Webhook/DiscordWebhook.cs @@ -1,231 +1,231 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading.Tasks; -using DSharpPlus.Net; -using Newtonsoft.Json; - -namespace DSharpPlus.Entities; - -/// -/// Represents information about a Discord webhook. -/// -public class DiscordWebhook : SnowflakeObject, IEquatable -{ - internal DiscordApiClient ApiClient { get; set; } - - /// - /// Gets the ID of the guild this webhook belongs to. - /// - [JsonProperty("guild_id", NullValueHandling = NullValueHandling.Ignore)] - public ulong GuildId { get; internal set; } - - /// - /// Gets the ID of the channel this webhook belongs to. - /// - [JsonProperty("channel_id", NullValueHandling = NullValueHandling.Ignore)] - public ulong ChannelId { get; internal set; } - - /// - /// Gets the user this webhook was created by. - /// - [JsonProperty("user", NullValueHandling = NullValueHandling.Ignore)] - public DiscordUser User { get; internal set; } - - /// - /// Gets the default name of this webhook. - /// - [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] - public string Name { get; internal set; } - - /// - /// Gets hash of the default avatar for this webhook. - /// - [JsonProperty("avatar", NullValueHandling = NullValueHandling.Ignore)] - internal string AvatarHash { get; set; } - - /// - /// Gets the default avatar url for this webhook. - /// - public string AvatarUrl - => !string.IsNullOrWhiteSpace(this.AvatarHash) ? $"https://cdn.discordapp.com/avatars/{this.Id}/{this.AvatarHash}.png?size=1024" : null; - - /// - /// Gets the secure token of this webhook. - /// - [JsonProperty("token", NullValueHandling = NullValueHandling.Ignore)] - public string Token { get; internal set; } - - /// - /// A partial guild object for the guild of the channel this channel follower webhook is following. - /// - [JsonProperty("source_guild", NullValueHandling = NullValueHandling.Ignore)] - public DiscordGuild SourceGuild { get; internal set; } - - /// - /// A partial channel object for the channel this channel follower webhook is following. - /// - [JsonProperty("source_channel", NullValueHandling = NullValueHandling.Ignore)] - public DiscordPartialChannel SourceChannel { get; internal set; } - - /// - /// Gets the webhook's url. Only returned when using the webhook.incoming OAuth2 scope. - /// - [JsonProperty("url", NullValueHandling = NullValueHandling.Ignore)] - public string Url { get; internal set; } - - internal DiscordWebhook() { } - - /// - /// Modifies this webhook. - /// - /// New default name for this webhook. - /// New avatar for this webhook. - /// The new channel id to move the webhook to. - /// Reason for audit logs. - /// The modified webhook. - /// Thrown when the client does not have the permission. - /// Thrown when the webhook does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public async Task ModifyAsync(string name = null, Optional avatar = default, ulong? channelId = null, string reason = null) - { - Optional avatarb64 = Optional.FromNoValue(); - if (avatar.HasValue && avatar.Value != null) - { - using InlineMediaTool imgtool = new(avatar.Value); - avatarb64 = imgtool.GetBase64(); - } - else if (avatar.HasValue) - { - avatarb64 = null; - } - - ulong newChannelId = channelId ?? this.ChannelId; - - return await this.Discord.ApiClient.ModifyWebhookAsync(this.Id, newChannelId, name, avatarb64, reason); - } - - /// - /// Permanently deletes this webhook. - /// - /// - /// Thrown when the client does not have the permission. - /// Thrown when the webhook does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public async Task DeleteAsync() - => await this.Discord.ApiClient.DeleteWebhookAsync(this.Id, this.Token); - - /// - /// Executes this webhook with the given . - /// - /// Webhook builder filled with data to send. - /// Thrown when the webhook does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public async Task ExecuteAsync(DiscordWebhookBuilder builder) - => await (this.Discord?.ApiClient ?? this.ApiClient).ExecuteWebhookAsync(this.Id, this.Token, builder); - - /// - /// Executes this webhook in Slack compatibility mode. - /// - /// JSON containing Slack-compatible payload for this 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 async Task ExecuteSlackAsync(string json) - => await (this.Discord?.ApiClient ?? this.ApiClient).ExecuteWebhookSlackAsync(this.Id, this.Token, json); - - /// - /// Executes this webhook in GitHub compatibility mode. - /// - /// JSON containing GitHub-compatible payload for this 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 async Task ExecuteGithubAsync(string json) - => await (this.Discord?.ApiClient ?? this.ApiClient).ExecuteWebhookGithubAsync(this.Id, this.Token, json); - - /// - /// Gets a previously-sent webhook message. - /// - /// Thrown when the webhook or message does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public async Task GetMessageAsync(ulong messageId) - => await (this.Discord?.ApiClient ?? this.ApiClient).GetWebhookMessageAsync(this.Id, this.Token, messageId); - - /// - /// Edits a previously-sent webhook message. - /// - /// The id of the message to edit. - /// The builder of the message to edit. - /// Attached files to keep. - /// The modified - /// Thrown when the webhook does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public async Task EditMessageAsync(ulong messageId, DiscordWebhookBuilder builder, IEnumerable attachments = default) - { - builder.Validate(true); - - return await (this.Discord?.ApiClient ?? this.ApiClient).EditWebhookMessageAsync(this.Id, this.Token, messageId, builder, attachments); - } - - /// - /// Deletes a message that was created by the webhook. - /// - /// The id of the message to delete - /// - /// Thrown when the webhook does not exist. - /// Thrown when an invalid parameter was provided. - /// Thrown when Discord is unable to process the request. - public async Task DeleteMessageAsync(ulong messageId) - => await (this.Discord?.ApiClient ?? this.ApiClient).DeleteWebhookMessageAsync(this.Id, this.Token, messageId); - - /// - /// 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) => Equals(obj as DiscordWebhook); - - /// - /// Checks whether this is equal to another . - /// - /// to compare to. - /// Whether the is equal to this . - public bool Equals(DiscordWebhook 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 webhook to compare. - /// Second webhook to compare. - /// Whether the two webhooks are equal. - public static bool operator ==(DiscordWebhook e1, DiscordWebhook e2) - { - object? o1 = e1; - object? o2 = e2; - - 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 webhook to compare. - /// Second webhook to compare. - /// Whether the two webhooks are not equal. - public static bool operator !=(DiscordWebhook e1, DiscordWebhook e2) - => !(e1 == e2); -} +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using DSharpPlus.Net; +using Newtonsoft.Json; + +namespace DSharpPlus.Entities; + +/// +/// Represents information about a Discord webhook. +/// +public class DiscordWebhook : SnowflakeObject, IEquatable +{ + internal DiscordApiClient ApiClient { get; set; } + + /// + /// Gets the ID of the guild this webhook belongs to. + /// + [JsonProperty("guild_id", NullValueHandling = NullValueHandling.Ignore)] + public ulong GuildId { get; internal set; } + + /// + /// Gets the ID of the channel this webhook belongs to. + /// + [JsonProperty("channel_id", NullValueHandling = NullValueHandling.Ignore)] + public ulong ChannelId { get; internal set; } + + /// + /// Gets the user this webhook was created by. + /// + [JsonProperty("user", NullValueHandling = NullValueHandling.Ignore)] + public DiscordUser User { get; internal set; } + + /// + /// Gets the default name of this webhook. + /// + [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] + public string Name { get; internal set; } + + /// + /// Gets hash of the default avatar for this webhook. + /// + [JsonProperty("avatar", NullValueHandling = NullValueHandling.Ignore)] + internal string AvatarHash { get; set; } + + /// + /// Gets the default avatar url for this webhook. + /// + public string AvatarUrl + => !string.IsNullOrWhiteSpace(this.AvatarHash) ? $"https://cdn.discordapp.com/avatars/{this.Id}/{this.AvatarHash}.png?size=1024" : null; + + /// + /// Gets the secure token of this webhook. + /// + [JsonProperty("token", NullValueHandling = NullValueHandling.Ignore)] + public string Token { get; internal set; } + + /// + /// A partial guild object for the guild of the channel this channel follower webhook is following. + /// + [JsonProperty("source_guild", NullValueHandling = NullValueHandling.Ignore)] + public DiscordGuild SourceGuild { get; internal set; } + + /// + /// A partial channel object for the channel this channel follower webhook is following. + /// + [JsonProperty("source_channel", NullValueHandling = NullValueHandling.Ignore)] + public DiscordPartialChannel SourceChannel { get; internal set; } + + /// + /// Gets the webhook's url. Only returned when using the webhook.incoming OAuth2 scope. + /// + [JsonProperty("url", NullValueHandling = NullValueHandling.Ignore)] + public string Url { get; internal set; } + + internal DiscordWebhook() { } + + /// + /// Modifies this webhook. + /// + /// New default name for this webhook. + /// New avatar for this webhook. + /// The new channel id to move the webhook to. + /// Reason for audit logs. + /// The modified webhook. + /// Thrown when the client does not have the permission. + /// Thrown when the webhook does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task ModifyAsync(string name = null, Optional avatar = default, ulong? channelId = null, string reason = null) + { + Optional avatarb64 = Optional.FromNoValue(); + if (avatar.HasValue && avatar.Value != null) + { + using InlineMediaTool imgtool = new(avatar.Value); + avatarb64 = imgtool.GetBase64(); + } + else if (avatar.HasValue) + { + avatarb64 = null; + } + + ulong newChannelId = channelId ?? this.ChannelId; + + return await this.Discord.ApiClient.ModifyWebhookAsync(this.Id, newChannelId, name, avatarb64, reason); + } + + /// + /// Permanently deletes this webhook. + /// + /// + /// Thrown when the client does not have the permission. + /// Thrown when the webhook does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task DeleteAsync() + => await this.Discord.ApiClient.DeleteWebhookAsync(this.Id, this.Token); + + /// + /// Executes this webhook with the given . + /// + /// Webhook builder filled with data to send. + /// Thrown when the webhook does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task ExecuteAsync(DiscordWebhookBuilder builder) + => await (this.Discord?.ApiClient ?? this.ApiClient).ExecuteWebhookAsync(this.Id, this.Token, builder); + + /// + /// Executes this webhook in Slack compatibility mode. + /// + /// JSON containing Slack-compatible payload for this 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 async Task ExecuteSlackAsync(string json) + => await (this.Discord?.ApiClient ?? this.ApiClient).ExecuteWebhookSlackAsync(this.Id, this.Token, json); + + /// + /// Executes this webhook in GitHub compatibility mode. + /// + /// JSON containing GitHub-compatible payload for this 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 async Task ExecuteGithubAsync(string json) + => await (this.Discord?.ApiClient ?? this.ApiClient).ExecuteWebhookGithubAsync(this.Id, this.Token, json); + + /// + /// Gets a previously-sent webhook message. + /// + /// Thrown when the webhook or message does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task GetMessageAsync(ulong messageId) + => await (this.Discord?.ApiClient ?? this.ApiClient).GetWebhookMessageAsync(this.Id, this.Token, messageId); + + /// + /// Edits a previously-sent webhook message. + /// + /// The id of the message to edit. + /// The builder of the message to edit. + /// Attached files to keep. + /// The modified + /// Thrown when the webhook does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task EditMessageAsync(ulong messageId, DiscordWebhookBuilder builder, IEnumerable attachments = default) + { + builder.Validate(true); + + return await (this.Discord?.ApiClient ?? this.ApiClient).EditWebhookMessageAsync(this.Id, this.Token, messageId, builder, attachments); + } + + /// + /// Deletes a message that was created by the webhook. + /// + /// The id of the message to delete + /// + /// Thrown when the webhook does not exist. + /// Thrown when an invalid parameter was provided. + /// Thrown when Discord is unable to process the request. + public async Task DeleteMessageAsync(ulong messageId) + => await (this.Discord?.ApiClient ?? this.ApiClient).DeleteWebhookMessageAsync(this.Id, this.Token, messageId); + + /// + /// 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) => Equals(obj as DiscordWebhook); + + /// + /// Checks whether this is equal to another . + /// + /// to compare to. + /// Whether the is equal to this . + public bool Equals(DiscordWebhook 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 webhook to compare. + /// Second webhook to compare. + /// Whether the two webhooks are equal. + public static bool operator ==(DiscordWebhook e1, DiscordWebhook e2) + { + object? o1 = e1; + object? o2 = e2; + + 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 webhook to compare. + /// Second webhook to compare. + /// Whether the two webhooks are not equal. + public static bool operator !=(DiscordWebhook e1, DiscordWebhook e2) + => !(e1 == e2); +} diff --git a/DSharpPlus/Entities/Webhook/DiscordWebhookBuilder.cs b/DSharpPlus/Entities/Webhook/DiscordWebhookBuilder.cs index 7bec86cc82..bd8ab626b8 100644 --- a/DSharpPlus/Entities/Webhook/DiscordWebhookBuilder.cs +++ b/DSharpPlus/Entities/Webhook/DiscordWebhookBuilder.cs @@ -1,161 +1,161 @@ -using System; -using System.Linq; -using System.Threading.Tasks; - -namespace DSharpPlus.Entities; - -/// -/// Constructs ready-to-send webhook requests. -/// -public sealed class DiscordWebhookBuilder : BaseDiscordMessageBuilder -{ - /// - /// Username to use for this webhook request. - /// - public Optional Username { get; set; } - - /// - /// Avatar url to use for this webhook request. - /// - public Optional AvatarUrl { get; set; } - - /// - /// Id of the thread to send the webhook request to. - /// - public ulong? ThreadId { get; set; } - - /// - /// Constructs a new empty webhook request builder. - /// - public DiscordWebhookBuilder() { } // I still see no point in initializing collections with empty collections. // - - /// - /// Constructs a new webhook request builder based on a previous message builder - /// - /// The builder to copy. - public DiscordWebhookBuilder(DiscordWebhookBuilder builder) : base(builder) - { - this.Username = builder.Username; - this.AvatarUrl = builder.AvatarUrl; - this.ThreadId = builder.ThreadId; - } - - /// - /// Copies the common properties from the passed builder. - /// - /// The builder to copy. - public DiscordWebhookBuilder(IDiscordMessageBuilder builder) : base(builder) { } - - /// - /// Sets the username for this webhook builder. - /// - /// Username of the webhook - public DiscordWebhookBuilder WithUsername(string username) - { - this.Username = username; - return this; - } - - /// - /// Sets the avatar of this webhook builder from its url. - /// - /// Avatar url of the webhook - public DiscordWebhookBuilder WithAvatarUrl(string avatarUrl) - { - this.AvatarUrl = avatarUrl; - return this; - } - - /// - /// Sets the id of the thread to execute the webhook on. - /// - /// The id of the thread - public DiscordWebhookBuilder WithThreadId(ulong? threadId) - { - this.ThreadId = threadId; - return this; - } - - public override void Clear() - { - this.Username = default; - this.AvatarUrl = default; - this.ThreadId = default; - base.Clear(); - } - - /// - /// Executes a webhook. - /// - /// The webhook that should be executed. - /// The message sent - public async Task SendAsync(DiscordWebhook webhook) => await webhook.ExecuteAsync(this); - - /// - /// Sends the modified webhook message. - /// - /// The webhook that should be executed. - /// The message to modify. - /// The modified message - public async Task ModifyAsync(DiscordWebhook webhook, DiscordMessage message) => await ModifyAsync(webhook, message.Id); - /// - /// Sends the modified webhook message. - /// - /// The webhook that should be executed. - /// The id of the message to modify. - /// The modified message - public async Task ModifyAsync(DiscordWebhook webhook, ulong messageId) => await webhook.EditMessageAsync(messageId, this); - - /// - /// Does the validation before we send a the Create/Modify request. - /// - /// Tells the method to perform the Modify Validation or Create Validation. - /// Tells the method to perform the follow up message validation. - /// Tells the method to perform the interaction response validation. - internal void Validate(bool isModify = false, bool isFollowup = false, bool isInteractionResponse = false) - { - if (isModify) - { - if (this.Username.HasValue) - { - throw new ArgumentException("You cannot change the username of a message."); - } - - if (this.AvatarUrl.HasValue) - { - throw new ArgumentException("You cannot change the avatar of a message."); - } - } - else if (isFollowup) - { - if (this.Username.HasValue) - { - throw new ArgumentException("You cannot change the username of a follow up message."); - } - - if (this.AvatarUrl.HasValue) - { - throw new ArgumentException("You cannot change the avatar of a follow up message."); - } - } - else if (isInteractionResponse) - { - if (this.Username.HasValue) - { - throw new ArgumentException("You cannot change the username of an interaction response."); - } - - if (this.AvatarUrl.HasValue) - { - throw new ArgumentException("You cannot change the avatar of an interaction response."); - } - } - else - { - if (!this.Flags.HasFlag(DiscordMessageFlags.IsComponentsV2) && this.Files?.Count == 0 && string.IsNullOrEmpty(this.Content) && !this.Embeds.Any()) - { - throw new ArgumentException("You must specify content, an embed, or at least one file."); - } - } - } -} +using System; +using System.Linq; +using System.Threading.Tasks; + +namespace DSharpPlus.Entities; + +/// +/// Constructs ready-to-send webhook requests. +/// +public sealed class DiscordWebhookBuilder : BaseDiscordMessageBuilder +{ + /// + /// Username to use for this webhook request. + /// + public Optional Username { get; set; } + + /// + /// Avatar url to use for this webhook request. + /// + public Optional AvatarUrl { get; set; } + + /// + /// Id of the thread to send the webhook request to. + /// + public ulong? ThreadId { get; set; } + + /// + /// Constructs a new empty webhook request builder. + /// + public DiscordWebhookBuilder() { } // I still see no point in initializing collections with empty collections. // + + /// + /// Constructs a new webhook request builder based on a previous message builder + /// + /// The builder to copy. + public DiscordWebhookBuilder(DiscordWebhookBuilder builder) : base(builder) + { + this.Username = builder.Username; + this.AvatarUrl = builder.AvatarUrl; + this.ThreadId = builder.ThreadId; + } + + /// + /// Copies the common properties from the passed builder. + /// + /// The builder to copy. + public DiscordWebhookBuilder(IDiscordMessageBuilder builder) : base(builder) { } + + /// + /// Sets the username for this webhook builder. + /// + /// Username of the webhook + public DiscordWebhookBuilder WithUsername(string username) + { + this.Username = username; + return this; + } + + /// + /// Sets the avatar of this webhook builder from its url. + /// + /// Avatar url of the webhook + public DiscordWebhookBuilder WithAvatarUrl(string avatarUrl) + { + this.AvatarUrl = avatarUrl; + return this; + } + + /// + /// Sets the id of the thread to execute the webhook on. + /// + /// The id of the thread + public DiscordWebhookBuilder WithThreadId(ulong? threadId) + { + this.ThreadId = threadId; + return this; + } + + public override void Clear() + { + this.Username = default; + this.AvatarUrl = default; + this.ThreadId = default; + base.Clear(); + } + + /// + /// Executes a webhook. + /// + /// The webhook that should be executed. + /// The message sent + public async Task SendAsync(DiscordWebhook webhook) => await webhook.ExecuteAsync(this); + + /// + /// Sends the modified webhook message. + /// + /// The webhook that should be executed. + /// The message to modify. + /// The modified message + public async Task ModifyAsync(DiscordWebhook webhook, DiscordMessage message) => await ModifyAsync(webhook, message.Id); + /// + /// Sends the modified webhook message. + /// + /// The webhook that should be executed. + /// The id of the message to modify. + /// The modified message + public async Task ModifyAsync(DiscordWebhook webhook, ulong messageId) => await webhook.EditMessageAsync(messageId, this); + + /// + /// Does the validation before we send a the Create/Modify request. + /// + /// Tells the method to perform the Modify Validation or Create Validation. + /// Tells the method to perform the follow up message validation. + /// Tells the method to perform the interaction response validation. + internal void Validate(bool isModify = false, bool isFollowup = false, bool isInteractionResponse = false) + { + if (isModify) + { + if (this.Username.HasValue) + { + throw new ArgumentException("You cannot change the username of a message."); + } + + if (this.AvatarUrl.HasValue) + { + throw new ArgumentException("You cannot change the avatar of a message."); + } + } + else if (isFollowup) + { + if (this.Username.HasValue) + { + throw new ArgumentException("You cannot change the username of a follow up message."); + } + + if (this.AvatarUrl.HasValue) + { + throw new ArgumentException("You cannot change the avatar of a follow up message."); + } + } + else if (isInteractionResponse) + { + if (this.Username.HasValue) + { + throw new ArgumentException("You cannot change the username of an interaction response."); + } + + if (this.AvatarUrl.HasValue) + { + throw new ArgumentException("You cannot change the avatar of an interaction response."); + } + } + else + { + if (!this.Flags.HasFlag(DiscordMessageFlags.IsComponentsV2) && this.Files?.Count == 0 && string.IsNullOrEmpty(this.Content) && !this.Embeds.Any()) + { + throw new ArgumentException("You must specify content, an embed, or at least one file."); + } + } + } +} diff --git a/DSharpPlus/EventArgs/Guild/GuildAuditLogCreatedEventArgs.cs b/DSharpPlus/EventArgs/Guild/GuildAuditLogCreatedEventArgs.cs index 136f9f4389..52b08becec 100644 --- a/DSharpPlus/EventArgs/Guild/GuildAuditLogCreatedEventArgs.cs +++ b/DSharpPlus/EventArgs/Guild/GuildAuditLogCreatedEventArgs.cs @@ -1,19 +1,19 @@ -using DSharpPlus.Entities; -using DSharpPlus.Entities.AuditLogs; - -namespace DSharpPlus.EventArgs; - -public class GuildAuditLogCreatedEventArgs : DiscordEventArgs -{ - /// - /// Created audit log entry. - /// - public DiscordAuditLogEntry AuditLogEntry { get; internal set; } - - /// - /// Guild where audit log entry was created. - /// - public DiscordGuild Guild { get; internal set; } - - internal GuildAuditLogCreatedEventArgs() : base() { } -} +using DSharpPlus.Entities; +using DSharpPlus.Entities.AuditLogs; + +namespace DSharpPlus.EventArgs; + +public class GuildAuditLogCreatedEventArgs : DiscordEventArgs +{ + /// + /// Created audit log entry. + /// + public DiscordAuditLogEntry AuditLogEntry { get; internal set; } + + /// + /// Guild where audit log entry was created. + /// + public DiscordGuild Guild { get; internal set; } + + internal GuildAuditLogCreatedEventArgs() : base() { } +} diff --git a/DSharpPlus/EventArgs/Guild/GuildDownloadCompletedEventArgs.cs b/DSharpPlus/EventArgs/Guild/GuildDownloadCompletedEventArgs.cs index e9b392ac8a..b5d4ad10e7 100644 --- a/DSharpPlus/EventArgs/Guild/GuildDownloadCompletedEventArgs.cs +++ b/DSharpPlus/EventArgs/Guild/GuildDownloadCompletedEventArgs.cs @@ -1,18 +1,18 @@ -using System.Collections.Generic; -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Represents arguments for GuildDownloadCompleted event. -/// -public class GuildDownloadCompletedEventArgs : DiscordEventArgs -{ - /// - /// Gets the dictionary of guilds that just finished downloading. - /// - public IReadOnlyDictionary Guilds { get; } - - internal GuildDownloadCompletedEventArgs(IReadOnlyDictionary guilds) - : base() => this.Guilds = guilds; -} +using System.Collections.Generic; +using DSharpPlus.Entities; + +namespace DSharpPlus.EventArgs; + +/// +/// Represents arguments for GuildDownloadCompleted event. +/// +public class GuildDownloadCompletedEventArgs : DiscordEventArgs +{ + /// + /// Gets the dictionary of guilds that just finished downloading. + /// + public IReadOnlyDictionary Guilds { get; } + + internal GuildDownloadCompletedEventArgs(IReadOnlyDictionary guilds) + : base() => this.Guilds = guilds; +} diff --git a/DSharpPlus/EventArgs/Guild/ScheduledEvents/ScheduledGuildEventCompletedEventArgs.cs b/DSharpPlus/EventArgs/Guild/ScheduledEvents/ScheduledGuildEventCompletedEventArgs.cs index 19563e48f5..245ff2f490 100644 --- a/DSharpPlus/EventArgs/Guild/ScheduledEvents/ScheduledGuildEventCompletedEventArgs.cs +++ b/DSharpPlus/EventArgs/Guild/ScheduledEvents/ScheduledGuildEventCompletedEventArgs.cs @@ -1,16 +1,16 @@ -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Fired when an event is completed. -/// -public class ScheduledGuildEventCompletedEventArgs : DiscordEventArgs -{ - /// - /// The event that finished. - /// - public DiscordScheduledGuildEvent Event { get; internal set; } - - internal ScheduledGuildEventCompletedEventArgs() : base() { } -} +using DSharpPlus.Entities; + +namespace DSharpPlus.EventArgs; + +/// +/// Fired when an event is completed. +/// +public class ScheduledGuildEventCompletedEventArgs : DiscordEventArgs +{ + /// + /// The event that finished. + /// + public DiscordScheduledGuildEvent Event { get; internal set; } + + internal ScheduledGuildEventCompletedEventArgs() : base() { } +} diff --git a/DSharpPlus/EventArgs/Socket/SocketEventArgs.cs b/DSharpPlus/EventArgs/Socket/SocketEventArgs.cs index 06c73ac6b2..b8d7e62745 100644 --- a/DSharpPlus/EventArgs/Socket/SocketEventArgs.cs +++ b/DSharpPlus/EventArgs/Socket/SocketEventArgs.cs @@ -1,12 +1,12 @@ -namespace DSharpPlus.EventArgs; - -/// -/// Represents basic socket event arguments. -/// -public class SocketEventArgs : DiscordEventArgs -{ - /// - /// Creates a new event argument container. - /// - public SocketEventArgs() : base() { } -} +namespace DSharpPlus.EventArgs; + +/// +/// Represents basic socket event arguments. +/// +public class SocketEventArgs : DiscordEventArgs +{ + /// + /// Creates a new event argument container. + /// + public SocketEventArgs() : base() { } +} diff --git a/DSharpPlus/EventArgs/Socket/WebSocketMessageEventArgs.cs b/DSharpPlus/EventArgs/Socket/WebSocketMessageEventArgs.cs index 343e60f4e8..0891bc3755 100644 --- a/DSharpPlus/EventArgs/Socket/WebSocketMessageEventArgs.cs +++ b/DSharpPlus/EventArgs/Socket/WebSocketMessageEventArgs.cs @@ -1,43 +1,43 @@ -using DSharpPlus.AsyncEvents; - -namespace DSharpPlus.EventArgs; - -/// -/// Represents base class for raw socket message event arguments. -/// -public abstract class SocketMessageEventArgs : AsyncEventArgs -{ } - -/// -/// Represents arguments for text message websocket event. -/// -public sealed class SocketTextMessageEventArgs : SocketMessageEventArgs -{ - /// - /// Gets the received message string. - /// - public string Message { get; } - - /// - /// Creates a new instance of text message event arguments. - /// - /// Received message string. - public SocketTextMessageEventArgs(string message) => this.Message = message; -} - -/// -/// Represents arguments for binary message websocket event. -/// -public sealed class SocketBinaryMessageEventArgs : SocketMessageEventArgs -{ - /// - /// Gets the received message bytes. - /// - public byte[] Message { get; } - - /// - /// Creates a new instance of binary message event arguments. - /// - /// Received message bytes. - public SocketBinaryMessageEventArgs(byte[] message) => this.Message = message; -} +using DSharpPlus.AsyncEvents; + +namespace DSharpPlus.EventArgs; + +/// +/// Represents base class for raw socket message event arguments. +/// +public abstract class SocketMessageEventArgs : AsyncEventArgs +{ } + +/// +/// Represents arguments for text message websocket event. +/// +public sealed class SocketTextMessageEventArgs : SocketMessageEventArgs +{ + /// + /// Gets the received message string. + /// + public string Message { get; } + + /// + /// Creates a new instance of text message event arguments. + /// + /// Received message string. + public SocketTextMessageEventArgs(string message) => this.Message = message; +} + +/// +/// Represents arguments for binary message websocket event. +/// +public sealed class SocketBinaryMessageEventArgs : SocketMessageEventArgs +{ + /// + /// Gets the received message bytes. + /// + public byte[] Message { get; } + + /// + /// Creates a new instance of binary message event arguments. + /// + /// Received message bytes. + public SocketBinaryMessageEventArgs(byte[] message) => this.Message = message; +} diff --git a/DSharpPlus/EventArgs/User/UserSpeakingEventArgs.cs b/DSharpPlus/EventArgs/User/UserSpeakingEventArgs.cs index d6e5eeb2e7..0fc310f0a0 100644 --- a/DSharpPlus/EventArgs/User/UserSpeakingEventArgs.cs +++ b/DSharpPlus/EventArgs/User/UserSpeakingEventArgs.cs @@ -1,26 +1,26 @@ -using DSharpPlus.Entities; - -namespace DSharpPlus.EventArgs; - -/// -/// Represents arguments for UserSpeaking event. -/// -public class UserSpeakingEventArgs : DiscordEventArgs -{ - /// - /// Gets the users whose speaking state changed. - /// - public DiscordUser User { get; internal set; } - - /// - /// Gets the SSRC of the audio source. - /// - public uint SSRC { get; internal set; } - - /// - /// Gets whether this user is speaking. - /// - public bool Speaking { get; internal set; } - - internal UserSpeakingEventArgs() : base() { } -} +using DSharpPlus.Entities; + +namespace DSharpPlus.EventArgs; + +/// +/// Represents arguments for UserSpeaking event. +/// +public class UserSpeakingEventArgs : DiscordEventArgs +{ + /// + /// Gets the users whose speaking state changed. + /// + public DiscordUser User { get; internal set; } + + /// + /// Gets the SSRC of the audio source. + /// + public uint SSRC { get; internal set; } + + /// + /// Gets whether this user is speaking. + /// + public bool Speaking { get; internal set; } + + internal UserSpeakingEventArgs() : base() { } +} diff --git a/DSharpPlus/EventArgs/Voice/VoiceStateUpdatedEventArgs.cs b/DSharpPlus/EventArgs/Voice/VoiceStateUpdatedEventArgs.cs index 7e6a921b88..59168a0d62 100644 --- a/DSharpPlus/EventArgs/Voice/VoiceStateUpdatedEventArgs.cs +++ b/DSharpPlus/EventArgs/Voice/VoiceStateUpdatedEventArgs.cs @@ -1,3 +1,4 @@ +using System.Threading.Tasks; using DSharpPlus.Entities; namespace DSharpPlus.EventArgs; @@ -7,6 +8,29 @@ namespace DSharpPlus.EventArgs; /// public class VoiceStateUpdatedEventArgs : DiscordEventArgs { + /// + /// Gets the member associated with this voice state. + /// + /// Whether to skip the cache and always fetch the member from the API. + /// Returns the member associated with this voice state. Null if the voice state is not associated with a guild. + public async ValueTask GetUserAsync(bool skipCache = false) + => await this.After.GetUserAsync(skipCache); + + /// + /// Gets the guild associated with this voice state. + /// + /// Returns the guild associated with this voicestate + public async ValueTask GetGuildAsync(bool skipCache = false) + => await this.After.GetGuildAsync(skipCache); + + /// + /// Gets the channel associated with this voice state. + /// + /// Whether to skip the cache and always fetch the channel from the API. + /// Returns the channel associated with this voice state. Null if the voice state is not associated with a guild. + public async ValueTask GetChannelAsync(bool skipCache = false) + => await this.After.GetChannelAsync(skipCache); + /// /// Gets the voice state pre-update. /// diff --git a/DSharpPlus/Exceptions/BadRequestException.cs b/DSharpPlus/Exceptions/BadRequestException.cs index dd5d189286..7645f9b32f 100644 --- a/DSharpPlus/Exceptions/BadRequestException.cs +++ b/DSharpPlus/Exceptions/BadRequestException.cs @@ -1,61 +1,61 @@ -using System.Net.Http; -using System.Text.Json; - -namespace DSharpPlus.Exceptions; - -/// -/// Represents an exception thrown when a malformed request is sent. -/// -public class BadRequestException : DiscordException -{ - - /// - /// Gets the error code for this exception. - /// - public int Code { get; internal set; } - - /// - /// Gets the form error responses in JSON format. - /// - public string? Errors { get; internal set; } - - internal BadRequestException(HttpRequestMessage request, HttpResponseMessage response, string content) - : base("Bad request: " + response.StatusCode) - { - this.Request = request; - this.Response = response; - - try - { - using JsonDocument document = JsonDocument.Parse(content); - JsonElement responseModel = document.RootElement; - - if - ( - responseModel.TryGetProperty("code", out JsonElement code) - && code.ValueKind == JsonValueKind.Number - ) - { - this.Code = code.GetInt32(); - } - - if - ( - responseModel.TryGetProperty("message", out JsonElement message) - && message.ValueKind == JsonValueKind.String - ) - { - this.JsonMessage = message.GetString(); - } - - if - ( - responseModel.TryGetProperty("errors", out JsonElement errors) - ) - { - this.Errors = JsonSerializer.Serialize(errors); - } - } - catch { } - } -} +using System.Net.Http; +using System.Text.Json; + +namespace DSharpPlus.Exceptions; + +/// +/// Represents an exception thrown when a malformed request is sent. +/// +public class BadRequestException : DiscordException +{ + + /// + /// Gets the error code for this exception. + /// + public int Code { get; internal set; } + + /// + /// Gets the form error responses in JSON format. + /// + public string? Errors { get; internal set; } + + internal BadRequestException(HttpRequestMessage request, HttpResponseMessage response, string content) + : base("Bad request: " + response.StatusCode) + { + this.Request = request; + this.Response = response; + + try + { + using JsonDocument document = JsonDocument.Parse(content); + JsonElement responseModel = document.RootElement; + + if + ( + responseModel.TryGetProperty("code", out JsonElement code) + && code.ValueKind == JsonValueKind.Number + ) + { + this.Code = code.GetInt32(); + } + + if + ( + responseModel.TryGetProperty("message", out JsonElement message) + && message.ValueKind == JsonValueKind.String + ) + { + this.JsonMessage = message.GetString(); + } + + if + ( + responseModel.TryGetProperty("errors", out JsonElement errors) + ) + { + this.Errors = JsonSerializer.Serialize(errors); + } + } + catch { } + } +} diff --git a/DSharpPlus/Exceptions/BulkDeleteFailedException.cs b/DSharpPlus/Exceptions/BulkDeleteFailedException.cs index f1cca60da2..a5f189d201 100644 --- a/DSharpPlus/Exceptions/BulkDeleteFailedException.cs +++ b/DSharpPlus/Exceptions/BulkDeleteFailedException.cs @@ -1,15 +1,15 @@ -using System; - -namespace DSharpPlus.Exceptions; - -public class BulkDeleteFailedException : Exception -{ - public BulkDeleteFailedException(int messagesDeleted, Exception innerException) - : base("Failed to delete all messages. See inner exception", innerException: innerException) => - this.MessagesDeleted = messagesDeleted; - - /// - /// Number of messages that were deleted successfully. - /// - public int MessagesDeleted { get; init; } -} +using System; + +namespace DSharpPlus.Exceptions; + +public class BulkDeleteFailedException : Exception +{ + public BulkDeleteFailedException(int messagesDeleted, Exception innerException) + : base("Failed to delete all messages. See inner exception", innerException: innerException) => + this.MessagesDeleted = messagesDeleted; + + /// + /// Number of messages that were deleted successfully. + /// + public int MessagesDeleted { get; init; } +} diff --git a/DSharpPlus/Exceptions/DiscordException.cs b/DSharpPlus/Exceptions/DiscordException.cs index 792081d097..242af91deb 100644 --- a/DSharpPlus/Exceptions/DiscordException.cs +++ b/DSharpPlus/Exceptions/DiscordException.cs @@ -1,26 +1,26 @@ -using System; -using System.Net.Http; - -namespace DSharpPlus.Exceptions; - -public abstract class DiscordException : Exception -{ - /// - /// Gets the request that caused the exception. - /// - public virtual HttpRequestMessage? Request { get; internal set; } - - /// - /// Gets the response to the request. - /// - public virtual HttpResponseMessage? Response { get; internal set; } - - /// - /// Gets the JSON message received. - /// - public virtual string? JsonMessage { get; internal set; } - - public DiscordException() : base() { } - public DiscordException(string message) : base(message) { } - public DiscordException(string message, Exception innerException) : base(message, innerException) { } -} +using System; +using System.Net.Http; + +namespace DSharpPlus.Exceptions; + +public abstract class DiscordException : Exception +{ + /// + /// Gets the request that caused the exception. + /// + public virtual HttpRequestMessage? Request { get; internal set; } + + /// + /// Gets the response to the request. + /// + public virtual HttpResponseMessage? Response { get; internal set; } + + /// + /// Gets the JSON message received. + /// + public virtual string? JsonMessage { get; internal set; } + + public DiscordException() : base() { } + public DiscordException(string message) : base(message) { } + public DiscordException(string message, Exception innerException) : base(message, innerException) { } +} diff --git a/DSharpPlus/Exceptions/NotFoundException.cs b/DSharpPlus/Exceptions/NotFoundException.cs index 4552a690f1..69ada79914 100644 --- a/DSharpPlus/Exceptions/NotFoundException.cs +++ b/DSharpPlus/Exceptions/NotFoundException.cs @@ -1,33 +1,33 @@ -using System.Net.Http; -using System.Text.Json; - -namespace DSharpPlus.Exceptions; - -/// -/// Represents an exception thrown when a requested resource is not found. -/// -public class NotFoundException : DiscordException -{ - internal NotFoundException(HttpRequestMessage request, HttpResponseMessage response, string content) - : base("Not found: " + response.StatusCode) - { - this.Request = request; - this.Response = response; - - try - { - using JsonDocument document = JsonDocument.Parse(content); - JsonElement responseModel = document.RootElement; - - if - ( - responseModel.TryGetProperty("message", out JsonElement message) - && message.ValueKind == JsonValueKind.String - ) - { - this.JsonMessage = message.GetString(); - } - } - catch { } - } -} +using System.Net.Http; +using System.Text.Json; + +namespace DSharpPlus.Exceptions; + +/// +/// Represents an exception thrown when a requested resource is not found. +/// +public class NotFoundException : DiscordException +{ + internal NotFoundException(HttpRequestMessage request, HttpResponseMessage response, string content) + : base("Not found: " + response.StatusCode) + { + this.Request = request; + this.Response = response; + + try + { + using JsonDocument document = JsonDocument.Parse(content); + JsonElement responseModel = document.RootElement; + + if + ( + responseModel.TryGetProperty("message", out JsonElement message) + && message.ValueKind == JsonValueKind.String + ) + { + this.JsonMessage = message.GetString(); + } + } + catch { } + } +} diff --git a/DSharpPlus/Exceptions/RateLimitException.cs b/DSharpPlus/Exceptions/RateLimitException.cs index 901a8a6934..956b58d4ea 100644 --- a/DSharpPlus/Exceptions/RateLimitException.cs +++ b/DSharpPlus/Exceptions/RateLimitException.cs @@ -1,33 +1,33 @@ -using System.Net.Http; -using System.Text.Json; - -namespace DSharpPlus.Exceptions; - -/// -/// Represents an exception thrown when too many requests are sent. -/// -public class RateLimitException : DiscordException -{ - internal RateLimitException(HttpRequestMessage request, HttpResponseMessage response, string content) - : base("Rate limited: " + response.StatusCode) - { - this.Request = request; - this.Response = response; - - try - { - using JsonDocument document = JsonDocument.Parse(content); - JsonElement responseModel = document.RootElement; - - if - ( - responseModel.TryGetProperty("message", out JsonElement message) - && message.ValueKind == JsonValueKind.String - ) - { - this.JsonMessage = message.GetString(); - } - } - catch { } - } -} +using System.Net.Http; +using System.Text.Json; + +namespace DSharpPlus.Exceptions; + +/// +/// Represents an exception thrown when too many requests are sent. +/// +public class RateLimitException : DiscordException +{ + internal RateLimitException(HttpRequestMessage request, HttpResponseMessage response, string content) + : base("Rate limited: " + response.StatusCode) + { + this.Request = request; + this.Response = response; + + try + { + using JsonDocument document = JsonDocument.Parse(content); + JsonElement responseModel = document.RootElement; + + if + ( + responseModel.TryGetProperty("message", out JsonElement message) + && message.ValueKind == JsonValueKind.String + ) + { + this.JsonMessage = message.GetString(); + } + } + catch { } + } +} diff --git a/DSharpPlus/Exceptions/RequestSizeException.cs b/DSharpPlus/Exceptions/RequestSizeException.cs index 3b39ec3986..cb35b83671 100644 --- a/DSharpPlus/Exceptions/RequestSizeException.cs +++ b/DSharpPlus/Exceptions/RequestSizeException.cs @@ -1,33 +1,33 @@ -using System.Net.Http; -using System.Text.Json; - -namespace DSharpPlus.Exceptions; - -/// -/// Represents an exception thrown when the request sent to Discord is too large. -/// -public class RequestSizeException : DiscordException -{ - internal RequestSizeException(HttpRequestMessage request, HttpResponseMessage response, string content) - : base($"Request entity too large: {response.StatusCode}. Make sure the data sent is within Discord's upload limit.") - { - this.Request = request; - this.Response = response; - - try - { - using JsonDocument document = JsonDocument.Parse(content); - JsonElement responseModel = document.RootElement; - - if - ( - responseModel.TryGetProperty("message", out JsonElement message) - && message.ValueKind == JsonValueKind.String - ) - { - this.JsonMessage = message.GetString(); - } - } - catch { } - } -} +using System.Net.Http; +using System.Text.Json; + +namespace DSharpPlus.Exceptions; + +/// +/// Represents an exception thrown when the request sent to Discord is too large. +/// +public class RequestSizeException : DiscordException +{ + internal RequestSizeException(HttpRequestMessage request, HttpResponseMessage response, string content) + : base($"Request entity too large: {response.StatusCode}. Make sure the data sent is within Discord's upload limit.") + { + this.Request = request; + this.Response = response; + + try + { + using JsonDocument document = JsonDocument.Parse(content); + JsonElement responseModel = document.RootElement; + + if + ( + responseModel.TryGetProperty("message", out JsonElement message) + && message.ValueKind == JsonValueKind.String + ) + { + this.JsonMessage = message.GetString(); + } + } + catch { } + } +} diff --git a/DSharpPlus/Exceptions/ServerErrorException.cs b/DSharpPlus/Exceptions/ServerErrorException.cs index a5a41f6391..9e6ca1452a 100644 --- a/DSharpPlus/Exceptions/ServerErrorException.cs +++ b/DSharpPlus/Exceptions/ServerErrorException.cs @@ -1,33 +1,33 @@ -using System.Net.Http; -using System.Text.Json; - -namespace DSharpPlus.Exceptions; - -/// -/// Represents an exception thrown when Discord returns an Internal Server Error. -/// -public class ServerErrorException : DiscordException -{ - internal ServerErrorException(HttpRequestMessage request, HttpResponseMessage response, string content) - : base("Internal Server Error: " + response.StatusCode) - { - this.Request = request; - this.Response = response; - - try - { - using JsonDocument document = JsonDocument.Parse(content); - JsonElement responseModel = document.RootElement; - - if - ( - responseModel.TryGetProperty("message", out JsonElement message) - && message.ValueKind == JsonValueKind.String - ) - { - this.JsonMessage = message.GetString(); - } - } - catch { } - } -} +using System.Net.Http; +using System.Text.Json; + +namespace DSharpPlus.Exceptions; + +/// +/// Represents an exception thrown when Discord returns an Internal Server Error. +/// +public class ServerErrorException : DiscordException +{ + internal ServerErrorException(HttpRequestMessage request, HttpResponseMessage response, string content) + : base("Internal Server Error: " + response.StatusCode) + { + this.Request = request; + this.Response = response; + + try + { + using JsonDocument document = JsonDocument.Parse(content); + JsonElement responseModel = document.RootElement; + + if + ( + responseModel.TryGetProperty("message", out JsonElement message) + && message.ValueKind == JsonValueKind.String + ) + { + this.JsonMessage = message.GetString(); + } + } + catch { } + } +} diff --git a/DSharpPlus/Exceptions/UnauthorizedException.cs b/DSharpPlus/Exceptions/UnauthorizedException.cs index f4158d6a13..e8418433af 100644 --- a/DSharpPlus/Exceptions/UnauthorizedException.cs +++ b/DSharpPlus/Exceptions/UnauthorizedException.cs @@ -1,33 +1,33 @@ -using System.Net.Http; -using System.Text.Json; - -namespace DSharpPlus.Exceptions; - -/// -/// Represents an exception thrown when requester doesn't have necessary permissions to complete the request. -/// -public class UnauthorizedException : DiscordException -{ - internal UnauthorizedException(HttpRequestMessage request, HttpResponseMessage response, string content) - : base("Unauthorized: " + response.StatusCode) - { - this.Request = request; - this.Response = response; - - try - { - using JsonDocument document = JsonDocument.Parse(content); - JsonElement responseModel = document.RootElement; - - if - ( - responseModel.TryGetProperty("message", out JsonElement message) - && message.ValueKind == JsonValueKind.String - ) - { - this.JsonMessage = message.GetString(); - } - } - catch { } - } -} +using System.Net.Http; +using System.Text.Json; + +namespace DSharpPlus.Exceptions; + +/// +/// Represents an exception thrown when requester doesn't have necessary permissions to complete the request. +/// +public class UnauthorizedException : DiscordException +{ + internal UnauthorizedException(HttpRequestMessage request, HttpResponseMessage response, string content) + : base("Unauthorized: " + response.StatusCode) + { + this.Request = request; + this.Response = response; + + try + { + using JsonDocument document = JsonDocument.Parse(content); + JsonElement responseModel = document.RootElement; + + if + ( + responseModel.TryGetProperty("message", out JsonElement message) + && message.ValueKind == JsonValueKind.String + ) + { + this.JsonMessage = message.GetString(); + } + } + catch { } + } +} diff --git a/DSharpPlus/Extensions/ServiceCollectionExtensions.cs b/DSharpPlus/Extensions/ServiceCollectionExtensions.cs index cdb01b0d13..b9415734b3 100644 --- a/DSharpPlus/Extensions/ServiceCollectionExtensions.cs +++ b/DSharpPlus/Extensions/ServiceCollectionExtensions.cs @@ -1,235 +1,235 @@ -using System; -using System.Linq; -using DSharpPlus.Clients; -using DSharpPlus.Net.Gateway.Compression; -using DSharpPlus.Net.Gateway.Compression.Zlib; -using DSharpPlus.Net.Gateway.Compression.Zstd; - -using Microsoft.Extensions.DependencyInjection; - -namespace DSharpPlus.Extensions; - -/// -/// Provides extension methods on . -/// -public static partial class ServiceCollectionExtensions -{ - /// - /// Adds DSharpPlus' DiscordClient and all its dependent services to the service collection. - /// - /// The service collection to add the DiscordClient to. - /// The bot token to use to connect to Discord. - /// The intents to use to connect to Discord. - /// The current instance for chaining. - public static IServiceCollection AddDiscordClient - ( - this IServiceCollection services, - string token, - DiscordIntents intents - ) - { - services.Configure(c => c.GetToken = () => token); - services.AddDSharpPlusDefaultsSingleShard(intents); - return services; - } - - /// - /// Adds DSharpPlus' DiscordClient and all its dependent services to the service collection, initialized - /// for running multiple shards. - /// - /// - /// This requires specifying shard information using Configure<ShardingOptions>(...); - /// /// - /// The service collection to add the DiscordClient to. - /// The bot token to use to connect to Discord. - /// The intents to use to connect to Discord. - /// The current instance for chaining. - public static IServiceCollection AddShardedDiscordClient - ( - this IServiceCollection services, - string token, - DiscordIntents intents - ) - { - services.Configure(c => c.GetToken = () => token); - services.AddDSharpPlusDefaultsSharded(intents); - return services; - } - - /// - /// Forces DSharpPlus to use zlib compression for the gateway. - /// - /// The service collection to configure this for. - /// The current instance for chaining. - public static IServiceCollection UseZlibCompression(this IServiceCollection services) - => services.Replace(); - - /// - /// Forces DSharpPlus to use zstd compression for the gateway. - /// - /// The service collection to configure this for. - /// The current instance for chaining. - public static IServiceCollection UseZstdCompression(this IServiceCollection services) - => services.Replace(); - - /// - /// Forces DSharpPlus to disable gateway compression entirely. - /// - /// The service collection to configure this for. - /// The current instance for chaining. - public static IServiceCollection DisableGatewayCompression(this IServiceCollection services) - => services.Replace(); - - /// - /// Disables connecting to the gateway. This is useful for Http - /// Interaction only bots, or bot that only make REST requests. - /// - /// The service collection to configure this for. - /// The current instance for chaining. - public static IServiceCollection DisableGateway(this IServiceCollection services) - => services.Replace(); - - /// - /// Decorates a given with a decorator of type . - /// - /// - /// The interface type to be decorated. The newly registered decorator can be decorated again if needed. - /// - /// The decorator type. This type may be decorated again. - /// The service collection for chaining. - /// - /// Thrown if this method is called before a service of type was registered. - /// - public static IServiceCollection Decorate(this IServiceCollection services) - where TInterface : class - where TDecorator : class, TInterface - { - ServiceDescriptor? previousRegistration = services.LastOrDefault(xm => xm.ServiceType == typeof(TInterface)) - ?? throw new InvalidOperationException - ( - $"Tried to register a decorator for {typeof(TInterface).Name}, but there was no underlying service to decorate." - ); - - Func? previousFactory = previousRegistration.ImplementationFactory; - - if (previousFactory is null && previousRegistration.ImplementationInstance is not null) - { - previousFactory = _ => previousRegistration.ImplementationInstance; - } - else if (previousFactory is null && previousRegistration.ImplementationType is not null) - { - previousFactory = provider => ActivatorUtilities.CreateInstance - ( - provider, - previousRegistration.ImplementationType - ); - } - - services.Add(new ServiceDescriptor(typeof(TInterface), CreateDecorator, previousRegistration.Lifetime)); - - return services; - - TDecorator CreateDecorator(IServiceProvider provider) - { - TInterface previousInstance = (TInterface)previousFactory!(provider); - - TDecorator decorator = (TDecorator)ActivatorUtilities.CreateFactory(typeof(TDecorator), [typeof(TInterface)]) - .Invoke(provider, [previousInstance]); - - return decorator; - } - } - - /// - /// Replaces an existing implementation for the specified service, retaining its lifetime. - /// - /// The service type. - /// The new implementation type. - /// The service collection to perform this operation on. - /// The service collection for chaining. - public static IServiceCollection Replace(this IServiceCollection services) - where TImplementation : class, TInterface - { - ServiceDescriptor old = services.Single(x => x.ServiceType == typeof(TInterface)); - - services.Remove(old); - services.Add(new ServiceDescriptor(typeof(TInterface), typeof(TImplementation), old.Lifetime)); - - return services; - } - - /// - /// Replaces an existing implementation for the specified service, retaining its lifetime. - /// - /// The service type. - /// The service collection to perform this operation on. - /// A factory for creating implementations of the service. - /// The service collection for chaining. - public static IServiceCollection Replace - ( - this IServiceCollection services, - Func factory - ) - { - ServiceDescriptor old = services.Single(x => x.ServiceType == typeof(TInterface)); - - services.Remove(old); - services.Add(new ServiceDescriptor(typeof(TInterface), factory, old.Lifetime)); - - return services; - } - - /// - /// Adds a service to the service collection, replacing an existing predecessor. - /// - /// The service type. - /// The new implementation type. - /// The service collection to perform this operation on. - /// The new service lifetime. - /// The service collection for chaining. - public static IServiceCollection AddOrReplace - ( - this IServiceCollection services, - ServiceLifetime lifetime - ) - where TImplementation : class, TInterface - { - ServiceDescriptor? old = services.SingleOrDefault(x => x.ServiceType == typeof(TInterface)); - - if (old is not null) - { - services.Remove(old); - } - - services.Add(new ServiceDescriptor(typeof(TInterface), typeof(TImplementation), lifetime)); - - return services; - } - - /// - /// Adds a service to the service collection, replacing an existing predecessor. - /// - /// The service type. - /// The service collection to perform this operation on. - /// A factory for creating implementations of the service. - /// The new service lifetime. - /// The service collection for chaining. - public static IServiceCollection AddOrReplace - ( - this IServiceCollection services, - Func factory, - ServiceLifetime lifetime - ) - { - ServiceDescriptor? old = services.SingleOrDefault(x => x.ServiceType == typeof(TInterface)); - - if (old is not null) - { - services.Remove(old); - } - - services.Add(new ServiceDescriptor(typeof(TInterface), factory, lifetime)); - - return services; - } -} +using System; +using System.Linq; +using DSharpPlus.Clients; +using DSharpPlus.Net.Gateway.Compression; +using DSharpPlus.Net.Gateway.Compression.Zlib; +using DSharpPlus.Net.Gateway.Compression.Zstd; + +using Microsoft.Extensions.DependencyInjection; + +namespace DSharpPlus.Extensions; + +/// +/// Provides extension methods on . +/// +public static partial class ServiceCollectionExtensions +{ + /// + /// Adds DSharpPlus' DiscordClient and all its dependent services to the service collection. + /// + /// The service collection to add the DiscordClient to. + /// The bot token to use to connect to Discord. + /// The intents to use to connect to Discord. + /// The current instance for chaining. + public static IServiceCollection AddDiscordClient + ( + this IServiceCollection services, + string token, + DiscordIntents intents + ) + { + services.Configure(c => c.GetToken = () => token); + services.AddDSharpPlusDefaultsSingleShard(intents); + return services; + } + + /// + /// Adds DSharpPlus' DiscordClient and all its dependent services to the service collection, initialized + /// for running multiple shards. + /// + /// + /// This requires specifying shard information using Configure<ShardingOptions>(...); + /// /// + /// The service collection to add the DiscordClient to. + /// The bot token to use to connect to Discord. + /// The intents to use to connect to Discord. + /// The current instance for chaining. + public static IServiceCollection AddShardedDiscordClient + ( + this IServiceCollection services, + string token, + DiscordIntents intents + ) + { + services.Configure(c => c.GetToken = () => token); + services.AddDSharpPlusDefaultsSharded(intents); + return services; + } + + /// + /// Forces DSharpPlus to use zlib compression for the gateway. + /// + /// The service collection to configure this for. + /// The current instance for chaining. + public static IServiceCollection UseZlibCompression(this IServiceCollection services) + => services.Replace(); + + /// + /// Forces DSharpPlus to use zstd compression for the gateway. + /// + /// The service collection to configure this for. + /// The current instance for chaining. + public static IServiceCollection UseZstdCompression(this IServiceCollection services) + => services.Replace(); + + /// + /// Forces DSharpPlus to disable gateway compression entirely. + /// + /// The service collection to configure this for. + /// The current instance for chaining. + public static IServiceCollection DisableGatewayCompression(this IServiceCollection services) + => services.Replace(); + + /// + /// Disables connecting to the gateway. This is useful for Http + /// Interaction only bots, or bot that only make REST requests. + /// + /// The service collection to configure this for. + /// The current instance for chaining. + public static IServiceCollection DisableGateway(this IServiceCollection services) + => services.Replace(); + + /// + /// Decorates a given with a decorator of type . + /// + /// + /// The interface type to be decorated. The newly registered decorator can be decorated again if needed. + /// + /// The decorator type. This type may be decorated again. + /// The service collection for chaining. + /// + /// Thrown if this method is called before a service of type was registered. + /// + public static IServiceCollection Decorate(this IServiceCollection services) + where TInterface : class + where TDecorator : class, TInterface + { + ServiceDescriptor? previousRegistration = services.LastOrDefault(xm => xm.ServiceType == typeof(TInterface)) + ?? throw new InvalidOperationException + ( + $"Tried to register a decorator for {typeof(TInterface).Name}, but there was no underlying service to decorate." + ); + + Func? previousFactory = previousRegistration.ImplementationFactory; + + if (previousFactory is null && previousRegistration.ImplementationInstance is not null) + { + previousFactory = _ => previousRegistration.ImplementationInstance; + } + else if (previousFactory is null && previousRegistration.ImplementationType is not null) + { + previousFactory = provider => ActivatorUtilities.CreateInstance + ( + provider, + previousRegistration.ImplementationType + ); + } + + services.Add(new ServiceDescriptor(typeof(TInterface), CreateDecorator, previousRegistration.Lifetime)); + + return services; + + TDecorator CreateDecorator(IServiceProvider provider) + { + TInterface previousInstance = (TInterface)previousFactory!(provider); + + TDecorator decorator = (TDecorator)ActivatorUtilities.CreateFactory(typeof(TDecorator), [typeof(TInterface)]) + .Invoke(provider, [previousInstance]); + + return decorator; + } + } + + /// + /// Replaces an existing implementation for the specified service, retaining its lifetime. + /// + /// The service type. + /// The new implementation type. + /// The service collection to perform this operation on. + /// The service collection for chaining. + public static IServiceCollection Replace(this IServiceCollection services) + where TImplementation : class, TInterface + { + ServiceDescriptor old = services.Single(x => x.ServiceType == typeof(TInterface)); + + services.Remove(old); + services.Add(new ServiceDescriptor(typeof(TInterface), typeof(TImplementation), old.Lifetime)); + + return services; + } + + /// + /// Replaces an existing implementation for the specified service, retaining its lifetime. + /// + /// The service type. + /// The service collection to perform this operation on. + /// A factory for creating implementations of the service. + /// The service collection for chaining. + public static IServiceCollection Replace + ( + this IServiceCollection services, + Func factory + ) + { + ServiceDescriptor old = services.Single(x => x.ServiceType == typeof(TInterface)); + + services.Remove(old); + services.Add(new ServiceDescriptor(typeof(TInterface), factory, old.Lifetime)); + + return services; + } + + /// + /// Adds a service to the service collection, replacing an existing predecessor. + /// + /// The service type. + /// The new implementation type. + /// The service collection to perform this operation on. + /// The new service lifetime. + /// The service collection for chaining. + public static IServiceCollection AddOrReplace + ( + this IServiceCollection services, + ServiceLifetime lifetime + ) + where TImplementation : class, TInterface + { + ServiceDescriptor? old = services.SingleOrDefault(x => x.ServiceType == typeof(TInterface)); + + if (old is not null) + { + services.Remove(old); + } + + services.Add(new ServiceDescriptor(typeof(TInterface), typeof(TImplementation), lifetime)); + + return services; + } + + /// + /// Adds a service to the service collection, replacing an existing predecessor. + /// + /// The service type. + /// The service collection to perform this operation on. + /// A factory for creating implementations of the service. + /// The new service lifetime. + /// The service collection for chaining. + public static IServiceCollection AddOrReplace + ( + this IServiceCollection services, + Func factory, + ServiceLifetime lifetime + ) + { + ServiceDescriptor? old = services.SingleOrDefault(x => x.ServiceType == typeof(TInterface)); + + if (old is not null) + { + services.Remove(old); + } + + services.Add(new ServiceDescriptor(typeof(TInterface), factory, lifetime)); + + return services; + } +} diff --git a/DSharpPlus/Formatter.cs b/DSharpPlus/Formatter.cs index 44a7865f26..08e01abae0 100644 --- a/DSharpPlus/Formatter.cs +++ b/DSharpPlus/Formatter.cs @@ -1,225 +1,225 @@ -using System; -using System.Globalization; -using System.Linq; -using System.Text.RegularExpressions; -using DSharpPlus.Entities; - -namespace DSharpPlus; - -/// -/// Contains markdown formatting helpers. -/// -public static partial class Formatter -{ - private const string AnsiEscapeStarter = "\u001b["; - - /// - /// Colorizes text based using ANSI escape codes. Escape codes are only properly rendered in code blocks. Resets are inserted automatically. - /// - /// The text to colorize. - /// - /// - public static string Colorize(string text, params AnsiColor[] styles) - { - string joined = styles.Select(s => ((int)s).ToString()).Aggregate((a, b) => $"{a};{b}"); - - return $"{AnsiEscapeStarter}{joined}m{text}{AnsiEscapeStarter}{(int)AnsiColor.Reset}m"; - } - - /// - /// Creates a block of code. - /// - /// Contents of the block. - /// Language to use for highlighting. - /// Formatted block of code. - public static string BlockCode(string content, string language = "") - => $"```{language}\n{content}\n```"; - - /// - /// Creates inline code snippet. - /// - /// Contents of the snippet. - /// Formatted inline code snippet. - public static string InlineCode(string content) - => $"`{content}`"; - - /// - /// Creates a rendered timestamp. - /// - /// The time from now. - /// The format to render the timestamp in. Defaults to relative. - /// A formatted timestamp. - public static string Timestamp(TimeSpan time, TimestampFormat format = TimestampFormat.RelativeTime) - => Timestamp(DateTimeOffset.UtcNow + time, format); - - /// - /// Creates a rendered timestamp. - /// - /// The time from now. - /// The format to render the timestamp in. Defaults to relative. - /// A formatted timestamp. - public static string Timestamp(DateTime time, TimestampFormat format = TimestampFormat.RelativeTime) - => Timestamp(new DateTimeOffset(time.ToUniversalTime()), format); - - /// - /// Creates a rendered timestamp. - /// - /// Timestamp to format. - /// The format to render the timestamp in. Defaults to relative. - /// A formatted timestamp. - public static string Timestamp(DateTimeOffset time, TimestampFormat format = TimestampFormat.RelativeTime) - => $""; - - /// - /// Creates bold text. - /// - /// Text to bolden. - /// Formatted text. - public static string Bold(string content) - => $"**{content}**"; - - /// - /// Creates italicized text. - /// - /// Text to italicize. - /// Formatted text. - public static string Italic(string content) - => $"*{content}*"; - - /// - /// Creates spoiler from text. - /// - /// Text to spoilerize. - /// Formatted text. - public static string Spoiler(string content) - => $"||{content}||"; - - /// - /// Creates underlined text. - /// - /// Text to underline. - /// Formatted text. - public static string Underline(string content) - => $"__{content}__"; - - /// - /// Creates strikethrough text. - /// - /// Text to strikethrough. - /// Formatted text. - public static string Strike(string content) - => $"~~{content}~~"; - - /// - /// Creates a URL that won't create a link preview. - /// - /// Url to prevent from being previewed. - /// Formatted url. - public static string EmbedlessUrl(Uri url) - => $"<{url}>"; - - /// - /// Creates a masked link. This link will display as specified text, and alternatively provided alt text. This can only be used in embeds. - /// - /// Text to display the link as. - /// Url that the link will lead to. - /// Alt text to display on hover. - /// Formatted url. - public static string MaskedUrl(string text, Uri url, string alt_text = "") - => $"[{text}]({url}{(!string.IsNullOrWhiteSpace(alt_text) ? $" \"{alt_text}\"" : "")})"; - - /// - /// Escapes all markdown formatting from specified text. - /// - /// Text to sanitize. - /// Sanitized text. - public static string Sanitize(string text) - => GetMarkdownSanitizationRegex().Replace(text, m => $"\\{m.Groups[1].Value}"); - - /// - /// Removes all markdown formatting from specified text. - /// - /// Text to strip of formatting. - /// Formatting-stripped text. - public static string Strip(string text) - => GetMarkdownStripRegex().Replace(text, _ => string.Empty); - - /// - /// Creates a mention for specified user or member. Can optionally specify to resolve nicknames. - /// - /// User to create mention for. - /// Whether the mention should resolve nicknames or not. - /// Formatted mention. - public static string Mention(DiscordUser user, bool nickname = false) - => nickname - ? $"<@!{user.Id.ToString(CultureInfo.InvariantCulture)}>" - : $"<@{user.Id.ToString(CultureInfo.InvariantCulture)}>"; - - /// - /// Creates a mention for specified channel. - /// - /// Channel to mention. - /// Formatted mention. - public static string Mention(DiscordChannel channel) - => $"<#{channel.Id.ToString(CultureInfo.InvariantCulture)}>"; - - /// - /// Creates a mention for specified role. - /// - /// Role to mention. - /// Formatted mention. - public static string Mention(DiscordRole role) - => $"<@&{role.Id.ToString(CultureInfo.InvariantCulture)}>"; - - /// - /// Creates a mention for specified application command. - /// - /// Application command to mention. - /// Formatted mention. - public static string Mention(DiscordApplicationCommand command) - => $""; - - /// - /// Creates a custom emoji string. - /// - /// Emoji to display. - /// Formatted emoji. - public static string Emoji(DiscordEmoji emoji) - => $"<:{emoji.Name}:{emoji.Id.ToString(CultureInfo.InvariantCulture)}>"; - - /// - /// Creates a url for using attachments in embeds. This can only be used as an Image URL, Thumbnail URL, Author icon URL or Footer icon URL. - /// - /// Name of attached image to display - /// - public static string AttachedImageUrl(string filename) - => $"attachment://{filename}"; - - /// - /// Creates a big header. - /// - /// Text to display as a big header. - /// Formatted header. - public static string ToBigHeader(string value) - => $"# {value}"; - - /// - /// Creates a medium header. - /// - /// Text to display as a medium header. - /// Formatted header. - public static string ToMediumHeader(string value) - => $"## {value}"; - - /// - /// Creates a small header. - /// - /// Text to display as a small header. - /// Formatted header. - public static string ToSmallHeader(string value) - => $"### {value}"; - [GeneratedRegex(@"([`\*_~<>\[\]\(\)""@\!\&#:\|])", RegexOptions.ECMAScript)] - private static partial Regex GetMarkdownSanitizationRegex(); - [GeneratedRegex(@"([`\*_~\[\]\(\)""\|]|<@\!?\d+>|<#\d+>|<@\&\d+>|<:[a-zA-Z0-9_\-]:\d+>)", RegexOptions.ECMAScript)] - private static partial Regex GetMarkdownStripRegex(); -} +using System; +using System.Globalization; +using System.Linq; +using System.Text.RegularExpressions; +using DSharpPlus.Entities; + +namespace DSharpPlus; + +/// +/// Contains markdown formatting helpers. +/// +public static partial class Formatter +{ + private const string AnsiEscapeStarter = "\u001b["; + + /// + /// Colorizes text based using ANSI escape codes. Escape codes are only properly rendered in code blocks. Resets are inserted automatically. + /// + /// The text to colorize. + /// + /// + public static string Colorize(string text, params AnsiColor[] styles) + { + string joined = styles.Select(s => ((int)s).ToString()).Aggregate((a, b) => $"{a};{b}"); + + return $"{AnsiEscapeStarter}{joined}m{text}{AnsiEscapeStarter}{(int)AnsiColor.Reset}m"; + } + + /// + /// Creates a block of code. + /// + /// Contents of the block. + /// Language to use for highlighting. + /// Formatted block of code. + public static string BlockCode(string content, string language = "") + => $"```{language}\n{content}\n```"; + + /// + /// Creates inline code snippet. + /// + /// Contents of the snippet. + /// Formatted inline code snippet. + public static string InlineCode(string content) + => $"`{content}`"; + + /// + /// Creates a rendered timestamp. + /// + /// The time from now. + /// The format to render the timestamp in. Defaults to relative. + /// A formatted timestamp. + public static string Timestamp(TimeSpan time, TimestampFormat format = TimestampFormat.RelativeTime) + => Timestamp(DateTimeOffset.UtcNow + time, format); + + /// + /// Creates a rendered timestamp. + /// + /// The time from now. + /// The format to render the timestamp in. Defaults to relative. + /// A formatted timestamp. + public static string Timestamp(DateTime time, TimestampFormat format = TimestampFormat.RelativeTime) + => Timestamp(new DateTimeOffset(time.ToUniversalTime()), format); + + /// + /// Creates a rendered timestamp. + /// + /// Timestamp to format. + /// The format to render the timestamp in. Defaults to relative. + /// A formatted timestamp. + public static string Timestamp(DateTimeOffset time, TimestampFormat format = TimestampFormat.RelativeTime) + => $""; + + /// + /// Creates bold text. + /// + /// Text to bolden. + /// Formatted text. + public static string Bold(string content) + => $"**{content}**"; + + /// + /// Creates italicized text. + /// + /// Text to italicize. + /// Formatted text. + public static string Italic(string content) + => $"*{content}*"; + + /// + /// Creates spoiler from text. + /// + /// Text to spoilerize. + /// Formatted text. + public static string Spoiler(string content) + => $"||{content}||"; + + /// + /// Creates underlined text. + /// + /// Text to underline. + /// Formatted text. + public static string Underline(string content) + => $"__{content}__"; + + /// + /// Creates strikethrough text. + /// + /// Text to strikethrough. + /// Formatted text. + public static string Strike(string content) + => $"~~{content}~~"; + + /// + /// Creates a URL that won't create a link preview. + /// + /// Url to prevent from being previewed. + /// Formatted url. + public static string EmbedlessUrl(Uri url) + => $"<{url}>"; + + /// + /// Creates a masked link. This link will display as specified text, and alternatively provided alt text. This can only be used in embeds. + /// + /// Text to display the link as. + /// Url that the link will lead to. + /// Alt text to display on hover. + /// Formatted url. + public static string MaskedUrl(string text, Uri url, string alt_text = "") + => $"[{text}]({url}{(!string.IsNullOrWhiteSpace(alt_text) ? $" \"{alt_text}\"" : "")})"; + + /// + /// Escapes all markdown formatting from specified text. + /// + /// Text to sanitize. + /// Sanitized text. + public static string Sanitize(string text) + => GetMarkdownSanitizationRegex().Replace(text, m => $"\\{m.Groups[1].Value}"); + + /// + /// Removes all markdown formatting from specified text. + /// + /// Text to strip of formatting. + /// Formatting-stripped text. + public static string Strip(string text) + => GetMarkdownStripRegex().Replace(text, _ => string.Empty); + + /// + /// Creates a mention for specified user or member. Can optionally specify to resolve nicknames. + /// + /// User to create mention for. + /// Whether the mention should resolve nicknames or not. + /// Formatted mention. + public static string Mention(DiscordUser user, bool nickname = false) + => nickname + ? $"<@!{user.Id.ToString(CultureInfo.InvariantCulture)}>" + : $"<@{user.Id.ToString(CultureInfo.InvariantCulture)}>"; + + /// + /// Creates a mention for specified channel. + /// + /// Channel to mention. + /// Formatted mention. + public static string Mention(DiscordChannel channel) + => $"<#{channel.Id.ToString(CultureInfo.InvariantCulture)}>"; + + /// + /// Creates a mention for specified role. + /// + /// Role to mention. + /// Formatted mention. + public static string Mention(DiscordRole role) + => $"<@&{role.Id.ToString(CultureInfo.InvariantCulture)}>"; + + /// + /// Creates a mention for specified application command. + /// + /// Application command to mention. + /// Formatted mention. + public static string Mention(DiscordApplicationCommand command) + => $""; + + /// + /// Creates a custom emoji string. + /// + /// Emoji to display. + /// Formatted emoji. + public static string Emoji(DiscordEmoji emoji) + => $"<:{emoji.Name}:{emoji.Id.ToString(CultureInfo.InvariantCulture)}>"; + + /// + /// Creates a url for using attachments in embeds. This can only be used as an Image URL, Thumbnail URL, Author icon URL or Footer icon URL. + /// + /// Name of attached image to display + /// + public static string AttachedImageUrl(string filename) + => $"attachment://{filename}"; + + /// + /// Creates a big header. + /// + /// Text to display as a big header. + /// Formatted header. + public static string ToBigHeader(string value) + => $"# {value}"; + + /// + /// Creates a medium header. + /// + /// Text to display as a medium header. + /// Formatted header. + public static string ToMediumHeader(string value) + => $"## {value}"; + + /// + /// Creates a small header. + /// + /// Text to display as a small header. + /// Formatted header. + public static string ToSmallHeader(string value) + => $"### {value}"; + [GeneratedRegex(@"([`\*_~<>\[\]\(\)""@\!\&#:\|])", RegexOptions.ECMAScript)] + private static partial Regex GetMarkdownSanitizationRegex(); + [GeneratedRegex(@"([`\*_~\[\]\(\)""\|]|<@\!?\d+>|<#\d+>|<@\&\d+>|<:[a-zA-Z0-9_\-]:\d+>)", RegexOptions.ECMAScript)] + private static partial Regex GetMarkdownStripRegex(); +} diff --git a/DSharpPlus/IMessageCacheProvider.cs b/DSharpPlus/IMessageCacheProvider.cs index c470655fe0..fa2c7b19a3 100644 --- a/DSharpPlus/IMessageCacheProvider.cs +++ b/DSharpPlus/IMessageCacheProvider.cs @@ -1,27 +1,27 @@ -using System.Diagnostics.CodeAnalysis; -using DSharpPlus.Entities; - -namespace DSharpPlus; - -public interface IMessageCacheProvider -{ - /// - /// Add a object to the cache. - /// - /// The object to add to the cache. - public void Add(DiscordMessage message); - - /// - /// Remove the object associated with the message ID from the cache. - /// - /// The ID of the message to remove from the cache. - public void Remove(ulong messageId); - - /// - /// Try to get a object associated with the message ID from the cache. - /// - /// The ID of the message to retrieve from the cache. - /// The object retrieved from the cache, if it exists; null otherwise. - /// if the message can be retrieved from the cache, otherwise. - public bool TryGet(ulong messageId, [NotNullWhen(true)] out DiscordMessage? message); -} +using System.Diagnostics.CodeAnalysis; +using DSharpPlus.Entities; + +namespace DSharpPlus; + +public interface IMessageCacheProvider +{ + /// + /// Add a object to the cache. + /// + /// The object to add to the cache. + public void Add(DiscordMessage message); + + /// + /// Remove the object associated with the message ID from the cache. + /// + /// The ID of the message to remove from the cache. + public void Remove(ulong messageId); + + /// + /// Try to get a object associated with the message ID from the cache. + /// + /// The ID of the message to retrieve from the cache. + /// The object retrieved from the cache, if it exists; null otherwise. + /// if the message can be retrieved from the cache, otherwise. + public bool TryGet(ulong messageId, [NotNullWhen(true)] out DiscordMessage? message); +} diff --git a/DSharpPlus/Logging/CompositeDefaultLogger.cs b/DSharpPlus/Logging/CompositeDefaultLogger.cs index be2bd97e57..adc73f87ca 100644 --- a/DSharpPlus/Logging/CompositeDefaultLogger.cs +++ b/DSharpPlus/Logging/CompositeDefaultLogger.cs @@ -1,40 +1,40 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -using Microsoft.Extensions.Logging; - -namespace DSharpPlus; - -internal class CompositeDefaultLogger : ILogger -{ - private IEnumerable Loggers { get; } - - public CompositeDefaultLogger(IEnumerable providers) - { - this.Loggers = providers.Select(x => x.CreateLogger(typeof(BaseDiscordClient).FullName!)) - .ToList(); - } - - public bool IsEnabled(LogLevel logLevel) - => true; - - public void Log - ( - LogLevel logLevel, - EventId eventId, - TState state, - Exception? exception, - Func formatter - ) - { - foreach (ILogger logger in this.Loggers) - { - logger.Log(logLevel, eventId, state, exception, formatter); - } - } - - public IDisposable? BeginScope(TState state) - where TState : notnull - => throw new NotImplementedException(); -} +using System; +using System.Collections.Generic; +using System.Linq; + +using Microsoft.Extensions.Logging; + +namespace DSharpPlus; + +internal class CompositeDefaultLogger : ILogger +{ + private IEnumerable Loggers { get; } + + public CompositeDefaultLogger(IEnumerable providers) + { + this.Loggers = providers.Select(x => x.CreateLogger(typeof(BaseDiscordClient).FullName!)) + .ToList(); + } + + public bool IsEnabled(LogLevel logLevel) + => true; + + public void Log + ( + LogLevel logLevel, + EventId eventId, + TState state, + Exception? exception, + Func formatter + ) + { + foreach (ILogger logger in this.Loggers) + { + logger.Log(logLevel, eventId, state, exception, formatter); + } + } + + public IDisposable? BeginScope(TState state) + where TState : notnull + => throw new NotImplementedException(); +} diff --git a/DSharpPlus/Logging/DefaultLogger.cs b/DSharpPlus/Logging/DefaultLogger.cs index 8bcbec570c..13c81f1909 100644 --- a/DSharpPlus/Logging/DefaultLogger.cs +++ b/DSharpPlus/Logging/DefaultLogger.cs @@ -1,89 +1,89 @@ -using System; - -using Microsoft.Extensions.Logging; - -namespace DSharpPlus.Logging; - -/// -/// The DSharpPlus default logger. -/// -internal sealed class DefaultLogger : ILogger -{ - private readonly string name; - private readonly LogLevel minimumLogLevel; - private readonly object @lock = new(); - private readonly string timestampFormat; - - public DefaultLogger(string name,LogLevel minimumLogLevel,string timestampFormat) - { - this.name = name; - this.minimumLogLevel = minimumLogLevel; - this.timestampFormat = timestampFormat; - } - - public IDisposable? BeginScope(TState state) - where TState : notnull - => default; - - public bool IsEnabled(LogLevel logLevel) - => logLevel >= this.minimumLogLevel && logLevel != LogLevel.None; - - public void Log - ( - LogLevel logLevel, - EventId eventId, - TState state, - Exception? exception, - Func formatter - ) - { - if (!IsEnabled(logLevel)) - { - return; - } - - lock (this.@lock) - { - if (logLevel == LogLevel.Trace) - { - Console.ForegroundColor = ConsoleColor.Gray; - } - - Console.Write($"[{DateTimeOffset.UtcNow.ToString(this.timestampFormat)}] [{this.name}] "); - - Console.ForegroundColor = logLevel switch - { - LogLevel.Trace => ConsoleColor.Gray, - LogLevel.Debug => ConsoleColor.Green, - LogLevel.Information => ConsoleColor.Magenta, - LogLevel.Warning => ConsoleColor.Yellow, - LogLevel.Error => ConsoleColor.Red, - LogLevel.Critical => ConsoleColor.DarkRed, - _ => throw new ArgumentException("Invalid log level specified.", nameof(logLevel)) - }; - - Console.Write - ( - logLevel switch - { - LogLevel.Trace => "[Trace] ", - LogLevel.Debug => "[Debug] ", - LogLevel.Information => "[Info] ", - LogLevel.Warning => "[Warn] ", - LogLevel.Error => "[Error] ", - LogLevel.Critical => "[Crit] ", - _ => "This code path is unreachable." - } - ); - - Console.ResetColor(); - - Console.WriteLine(formatter(state, exception)); - - if (exception != null) - { - Console.WriteLine($"{exception} : {exception.Message}\n{exception.StackTrace}"); - } - } - } -} +using System; + +using Microsoft.Extensions.Logging; + +namespace DSharpPlus.Logging; + +/// +/// The DSharpPlus default logger. +/// +internal sealed class DefaultLogger : ILogger +{ + private readonly string name; + private readonly LogLevel minimumLogLevel; + private readonly object @lock = new(); + private readonly string timestampFormat; + + public DefaultLogger(string name,LogLevel minimumLogLevel,string timestampFormat) + { + this.name = name; + this.minimumLogLevel = minimumLogLevel; + this.timestampFormat = timestampFormat; + } + + public IDisposable? BeginScope(TState state) + where TState : notnull + => default; + + public bool IsEnabled(LogLevel logLevel) + => logLevel >= this.minimumLogLevel && logLevel != LogLevel.None; + + public void Log + ( + LogLevel logLevel, + EventId eventId, + TState state, + Exception? exception, + Func formatter + ) + { + if (!IsEnabled(logLevel)) + { + return; + } + + lock (this.@lock) + { + if (logLevel == LogLevel.Trace) + { + Console.ForegroundColor = ConsoleColor.Gray; + } + + Console.Write($"[{DateTimeOffset.UtcNow.ToString(this.timestampFormat)}] [{this.name}] "); + + Console.ForegroundColor = logLevel switch + { + LogLevel.Trace => ConsoleColor.Gray, + LogLevel.Debug => ConsoleColor.Green, + LogLevel.Information => ConsoleColor.Magenta, + LogLevel.Warning => ConsoleColor.Yellow, + LogLevel.Error => ConsoleColor.Red, + LogLevel.Critical => ConsoleColor.DarkRed, + _ => throw new ArgumentException("Invalid log level specified.", nameof(logLevel)) + }; + + Console.Write + ( + logLevel switch + { + LogLevel.Trace => "[Trace] ", + LogLevel.Debug => "[Debug] ", + LogLevel.Information => "[Info] ", + LogLevel.Warning => "[Warn] ", + LogLevel.Error => "[Error] ", + LogLevel.Critical => "[Crit] ", + _ => "This code path is unreachable." + } + ); + + Console.ResetColor(); + + Console.WriteLine(formatter(state, exception)); + + if (exception != null) + { + Console.WriteLine($"{exception} : {exception.Message}\n{exception.StackTrace}"); + } + } + } +} diff --git a/DSharpPlus/Logging/DefaultLoggerFactory.cs b/DSharpPlus/Logging/DefaultLoggerFactory.cs index 99669f180a..ff74ff6710 100644 --- a/DSharpPlus/Logging/DefaultLoggerFactory.cs +++ b/DSharpPlus/Logging/DefaultLoggerFactory.cs @@ -1,37 +1,37 @@ -using System; -using System.Collections.Generic; -using Microsoft.Extensions.Logging; - -namespace DSharpPlus.Logging; - -internal class DefaultLoggerFactory : ILoggerFactory -{ - private List Providers { get; } = []; - private bool isDisposed = false; - - public void AddProvider(ILoggerProvider provider) => this.Providers.Add(provider); - - public ILogger CreateLogger(string categoryName) - { - return this.isDisposed - ? throw new InvalidOperationException("This logger factory is already disposed.") - : new CompositeDefaultLogger(this.Providers); - } - - public void Dispose() - { - if (this.isDisposed) - { - return; - } - - this.isDisposed = true; - - foreach (ILoggerProvider provider in this.Providers) - { - provider.Dispose(); - } - - this.Providers.Clear(); - } -} +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Logging; + +namespace DSharpPlus.Logging; + +internal class DefaultLoggerFactory : ILoggerFactory +{ + private List Providers { get; } = []; + private bool isDisposed = false; + + public void AddProvider(ILoggerProvider provider) => this.Providers.Add(provider); + + public ILogger CreateLogger(string categoryName) + { + return this.isDisposed + ? throw new InvalidOperationException("This logger factory is already disposed.") + : new CompositeDefaultLogger(this.Providers); + } + + public void Dispose() + { + if (this.isDisposed) + { + return; + } + + this.isDisposed = true; + + foreach (ILoggerProvider provider in this.Providers) + { + provider.Dispose(); + } + + this.Providers.Clear(); + } +} diff --git a/DSharpPlus/Logging/DefaultLoggerProvider.cs b/DSharpPlus/Logging/DefaultLoggerProvider.cs index d5c55a8e51..c0b4b682c3 100644 --- a/DSharpPlus/Logging/DefaultLoggerProvider.cs +++ b/DSharpPlus/Logging/DefaultLoggerProvider.cs @@ -1,41 +1,41 @@ -using System; -using System.Collections.Concurrent; - -using Microsoft.Extensions.Logging; - -namespace DSharpPlus.Logging; - -internal class DefaultLoggerProvider : ILoggerProvider -{ - private readonly ConcurrentDictionary loggers = new(StringComparer.Ordinal); - private readonly LogLevel minimum; - private readonly string timestampFormat; - - public DefaultLoggerProvider(LogLevel minimum = LogLevel.Trace, string timestampFormat = "yyyy-MM-dd HH:mm:ss zzz") - { - this.minimum = minimum; - this.timestampFormat = timestampFormat; - } - - /// - public ILogger CreateLogger(string categoryName) - { - if (this.loggers.TryGetValue(categoryName, out DefaultLogger? value)) - { - return value; - } - else - { - DefaultLogger logger = new(categoryName, this.minimum, this.timestampFormat); - - return this.loggers.AddOrUpdate - ( - categoryName, - logger, - (_, _) => logger - ); - } - } - - public void Dispose() { } -} +using System; +using System.Collections.Concurrent; + +using Microsoft.Extensions.Logging; + +namespace DSharpPlus.Logging; + +internal class DefaultLoggerProvider : ILoggerProvider +{ + private readonly ConcurrentDictionary loggers = new(StringComparer.Ordinal); + private readonly LogLevel minimum; + private readonly string timestampFormat; + + public DefaultLoggerProvider(LogLevel minimum = LogLevel.Trace, string timestampFormat = "yyyy-MM-dd HH:mm:ss zzz") + { + this.minimum = minimum; + this.timestampFormat = timestampFormat; + } + + /// + public ILogger CreateLogger(string categoryName) + { + if (this.loggers.TryGetValue(categoryName, out DefaultLogger? value)) + { + return value; + } + else + { + DefaultLogger logger = new(categoryName, this.minimum, this.timestampFormat); + + return this.loggers.AddOrUpdate + ( + categoryName, + logger, + (_, _) => logger + ); + } + } + + public void Dispose() { } +} diff --git a/DSharpPlus/Logging/LoggerEvents.cs b/DSharpPlus/Logging/LoggerEvents.cs index fed4845b7b..aaefc85b86 100644 --- a/DSharpPlus/Logging/LoggerEvents.cs +++ b/DSharpPlus/Logging/LoggerEvents.cs @@ -1,128 +1,128 @@ -using Microsoft.Extensions.Logging; - -namespace DSharpPlus; - -/// -/// Contains well-defined event IDs used by core of DSharpPlus. -/// -public static class LoggerEvents -{ - /// - /// Miscellaneous events, that do not fit in any other category. - /// - public static EventId Misc { get; } = new EventId(100, "DSharpPlus"); - - /// - /// Events pertaining to startup tasks. - /// - public static EventId Startup { get; } = new EventId(101, nameof(Startup)); - - /// - /// Events typically emitted whenever WebSocket connections fail or are terminated. - /// - public static EventId ConnectionFailure { get; } = new EventId(102, nameof(ConnectionFailure)); - - /// - /// Events pertaining to Discord-issued session state updates. - /// - public static EventId SessionUpdate { get; } = new EventId(103, nameof(SessionUpdate)); - - /// - /// Events emitted when exceptions are thrown in handlers attached to async events. - /// - public static EventId EventHandlerException { get; } = new EventId(104, nameof(EventHandlerException)); - - /// - /// Events emitted for various high-level WebSocket receive events. - /// - public static EventId WebSocketReceive { get; } = new EventId(105, nameof(WebSocketReceive)); - - /// - /// Events emitted for various low-level WebSocket receive events. - /// - public static EventId WebSocketReceiveRaw { get; } = new EventId(106, nameof(WebSocketReceiveRaw)); - - /// - /// Events emitted for various low-level WebSocket send events. - /// - public static EventId WebSocketSendRaw { get; } = new EventId(107, nameof(WebSocketSendRaw)); - - /// - /// Events emitted for various WebSocket payload processing failures, typically when deserialization or decoding fails. - /// - public static EventId WebSocketReceiveFailure { get; } = new EventId(108, nameof(WebSocketReceiveFailure)); - - /// - /// Events pertaining to connection lifecycle, specifically, heartbeats. - /// - public static EventId Heartbeat { get; } = new EventId(109, nameof(Heartbeat)); - - /// - /// Events pertaining to various heartbeat failures, typically fatal. - /// - public static EventId HeartbeatFailure { get; } = new EventId(110, nameof(HeartbeatFailure)); - - /// - /// Events pertaining to clean connection closes. - /// - public static EventId ConnectionClose { get; } = new EventId(111, nameof(ConnectionClose)); - - /// - /// Events emitted when REST processing fails for any reason. - /// - public static EventId RestError { get; } = new EventId(112, nameof(RestError)); - - /// - /// Events pertaining to ratelimit exhaustion. - /// - public static EventId RatelimitHit { get; } = new EventId(114, nameof(RatelimitHit)); - - /// - /// Events pertaining to ratelimit diagnostics. Typically contain raw bucket info. - /// - public static EventId RatelimitDiag { get; } = new EventId(115, nameof(RatelimitDiag)); - - /// - /// Events emitted when a ratelimit is exhausted and a request is preemtively blocked. - /// - public static EventId RatelimitPreemptive { get; } = new EventId(116, nameof(RatelimitPreemptive)); - - /// - /// Events pertaining to audit log processing. - /// - public static EventId AuditLog { get; } = new EventId(117, nameof(AuditLog)); - - /// - /// Events containing raw (but decompressed) payloads, received from Discord Gateway. - /// - public static EventId GatewayWsRx { get; } = new EventId(118, "Gateway ↓"); - - /// - /// Events containing raw payloads, as they're being sent to Discord Gateway. - /// - public static EventId GatewayWsTx { get; } = new EventId(119, "Gateway ↑"); - - /// - /// Events pertaining to Gateway Intents. Typically diagnostic information. - /// - public static EventId Intents { get; } = new EventId(120, nameof(Intents)); - - /// - /// Events pertaining to autosharded client shard shutdown, clean or otherwise. - /// - public static EventId ShardShutdown { get; } = new EventId(121, nameof(ShardShutdown)); - - /// - /// Events containing raw payloads, as they're received from Discord's REST API. - /// - public static EventId RestRx { get; } = new EventId(123, "REST ↓"); - - /// - /// Events containing raw payloads, as they're sent to Discord's REST API. - /// - public static EventId RestTx { get; } = new EventId(124, "REST ↑"); - - public static EventId RestCleaner { get; } = new EventId(125, nameof(RestCleaner)); - - public static EventId RestHashMover { get; } = new EventId(126, nameof(RestHashMover)); -} +using Microsoft.Extensions.Logging; + +namespace DSharpPlus; + +/// +/// Contains well-defined event IDs used by core of DSharpPlus. +/// +public static class LoggerEvents +{ + /// + /// Miscellaneous events, that do not fit in any other category. + /// + public static EventId Misc { get; } = new EventId(100, "DSharpPlus"); + + /// + /// Events pertaining to startup tasks. + /// + public static EventId Startup { get; } = new EventId(101, nameof(Startup)); + + /// + /// Events typically emitted whenever WebSocket connections fail or are terminated. + /// + public static EventId ConnectionFailure { get; } = new EventId(102, nameof(ConnectionFailure)); + + /// + /// Events pertaining to Discord-issued session state updates. + /// + public static EventId SessionUpdate { get; } = new EventId(103, nameof(SessionUpdate)); + + /// + /// Events emitted when exceptions are thrown in handlers attached to async events. + /// + public static EventId EventHandlerException { get; } = new EventId(104, nameof(EventHandlerException)); + + /// + /// Events emitted for various high-level WebSocket receive events. + /// + public static EventId WebSocketReceive { get; } = new EventId(105, nameof(WebSocketReceive)); + + /// + /// Events emitted for various low-level WebSocket receive events. + /// + public static EventId WebSocketReceiveRaw { get; } = new EventId(106, nameof(WebSocketReceiveRaw)); + + /// + /// Events emitted for various low-level WebSocket send events. + /// + public static EventId WebSocketSendRaw { get; } = new EventId(107, nameof(WebSocketSendRaw)); + + /// + /// Events emitted for various WebSocket payload processing failures, typically when deserialization or decoding fails. + /// + public static EventId WebSocketReceiveFailure { get; } = new EventId(108, nameof(WebSocketReceiveFailure)); + + /// + /// Events pertaining to connection lifecycle, specifically, heartbeats. + /// + public static EventId Heartbeat { get; } = new EventId(109, nameof(Heartbeat)); + + /// + /// Events pertaining to various heartbeat failures, typically fatal. + /// + public static EventId HeartbeatFailure { get; } = new EventId(110, nameof(HeartbeatFailure)); + + /// + /// Events pertaining to clean connection closes. + /// + public static EventId ConnectionClose { get; } = new EventId(111, nameof(ConnectionClose)); + + /// + /// Events emitted when REST processing fails for any reason. + /// + public static EventId RestError { get; } = new EventId(112, nameof(RestError)); + + /// + /// Events pertaining to ratelimit exhaustion. + /// + public static EventId RatelimitHit { get; } = new EventId(114, nameof(RatelimitHit)); + + /// + /// Events pertaining to ratelimit diagnostics. Typically contain raw bucket info. + /// + public static EventId RatelimitDiag { get; } = new EventId(115, nameof(RatelimitDiag)); + + /// + /// Events emitted when a ratelimit is exhausted and a request is preemtively blocked. + /// + public static EventId RatelimitPreemptive { get; } = new EventId(116, nameof(RatelimitPreemptive)); + + /// + /// Events pertaining to audit log processing. + /// + public static EventId AuditLog { get; } = new EventId(117, nameof(AuditLog)); + + /// + /// Events containing raw (but decompressed) payloads, received from Discord Gateway. + /// + public static EventId GatewayWsRx { get; } = new EventId(118, "Gateway ↓"); + + /// + /// Events containing raw payloads, as they're being sent to Discord Gateway. + /// + public static EventId GatewayWsTx { get; } = new EventId(119, "Gateway ↑"); + + /// + /// Events pertaining to Gateway Intents. Typically diagnostic information. + /// + public static EventId Intents { get; } = new EventId(120, nameof(Intents)); + + /// + /// Events pertaining to autosharded client shard shutdown, clean or otherwise. + /// + public static EventId ShardShutdown { get; } = new EventId(121, nameof(ShardShutdown)); + + /// + /// Events containing raw payloads, as they're received from Discord's REST API. + /// + public static EventId RestRx { get; } = new EventId(123, "REST ↓"); + + /// + /// Events containing raw payloads, as they're sent to Discord's REST API. + /// + public static EventId RestTx { get; } = new EventId(124, "REST ↑"); + + public static EventId RestCleaner { get; } = new EventId(125, nameof(RestCleaner)); + + public static EventId RestHashMover { get; } = new EventId(126, nameof(RestHashMover)); +} diff --git a/DSharpPlus/MessageCache.cs b/DSharpPlus/MessageCache.cs index e326449f2f..470ab12bde 100644 --- a/DSharpPlus/MessageCache.cs +++ b/DSharpPlus/MessageCache.cs @@ -1,33 +1,33 @@ -using DSharpPlus.Entities; -using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.Options; - -namespace DSharpPlus; - -public class MessageCache : IMessageCacheProvider -{ - private readonly IMemoryCache cache; - private readonly MemoryCacheEntryOptions entryOptions; - - public MessageCache(IMemoryCache cache, IOptions config) - { - this.cache = cache; - - this.entryOptions = new MemoryCacheEntryOptions() - { - Size = 1, - SlidingExpiration = config.Value.SlidingMessageCacheExpiration, - AbsoluteExpirationRelativeToNow = config.Value.AbsoluteMessageCacheExpiration - }; - } - - /// - public void Add(DiscordMessage message) - => this.cache.Set(message.Id, message, this.entryOptions); - - /// - public void Remove(ulong messageId) => this.cache.Remove(messageId); - - /// - public bool TryGet(ulong messageId, out DiscordMessage? message) => this.cache.TryGetValue(messageId, out message); -} +using DSharpPlus.Entities; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; + +namespace DSharpPlus; + +public class MessageCache : IMessageCacheProvider +{ + private readonly IMemoryCache cache; + private readonly MemoryCacheEntryOptions entryOptions; + + public MessageCache(IMemoryCache cache, IOptions config) + { + this.cache = cache; + + this.entryOptions = new MemoryCacheEntryOptions() + { + Size = 1, + SlidingExpiration = config.Value.SlidingMessageCacheExpiration, + AbsoluteExpirationRelativeToNow = config.Value.AbsoluteMessageCacheExpiration + }; + } + + /// + public void Add(DiscordMessage message) + => this.cache.Set(message.Id, message, this.entryOptions); + + /// + public void Remove(ulong messageId) => this.cache.Remove(messageId); + + /// + public bool TryGet(ulong messageId, out DiscordMessage? message) => this.cache.TryGetValue(messageId, out message); +} diff --git a/DSharpPlus/Metrics/LiveRequestMetrics.cs b/DSharpPlus/Metrics/LiveRequestMetrics.cs index 5da071f004..28282737d5 100644 --- a/DSharpPlus/Metrics/LiveRequestMetrics.cs +++ b/DSharpPlus/Metrics/LiveRequestMetrics.cs @@ -1,17 +1,17 @@ -namespace DSharpPlus.Metrics; - - -internal record struct LiveRequestMetrics -{ - // these are fields so that we can use Interlocked.Increment directly - public int requests; - public int successful; - public int ratelimits; - public int globalRatelimits; - public int bucketRatelimits; - public int badRequests; - public int forbidden; - public int notFound; - public int tooLarge; - public int serverError; -} +namespace DSharpPlus.Metrics; + + +internal record struct LiveRequestMetrics +{ + // these are fields so that we can use Interlocked.Increment directly + public int requests; + public int successful; + public int ratelimits; + public int globalRatelimits; + public int bucketRatelimits; + public int badRequests; + public int forbidden; + public int notFound; + public int tooLarge; + public int serverError; +} diff --git a/DSharpPlus/Metrics/RequestMetricsCollection.cs b/DSharpPlus/Metrics/RequestMetricsCollection.cs index fe1c5173ad..42a8d52f9e 100644 --- a/DSharpPlus/Metrics/RequestMetricsCollection.cs +++ b/DSharpPlus/Metrics/RequestMetricsCollection.cs @@ -1,178 +1,178 @@ -using System; -using System.Globalization; -using System.Text; - -namespace DSharpPlus.Metrics; - -/// -/// Represents an immutable snapshot of request metrics. -/// -public readonly record struct RequestMetricsCollection -{ - /// - /// The total amount of requests made during the specified duration. - /// - public int TotalRequests { get; init; } - - /// - /// The successful requests made during the specified duration. - /// - public int SuccessfulRequests { get; init; } - - /// - /// The failed requests made during the specified duration. - /// - public int FailedRequests => this.TotalRequests - this.SuccessfulRequests; - - /// - /// The amount of ratelimits hit during the specified duration. - /// - public int RatelimitsHit { get; init; } - - /// - /// The amount of global ratelimits hit during the specified duration. - /// - public int GlobalRatelimitsHit { get; init; } - - /// - /// The amount of bucket ratelimits hit during the specified duration. - /// - public int BucketRatelimitsHit { get; init; } - - /// - /// The amount of bad requests made during the specified duration. - /// - public int BadRequests { get; init; } - - /// - /// The amount of forbidden or unauthorized requests made during the specified duration. - /// - public int Forbidden { get; init; } - - /// - /// The amount of requests whose target could not be found made during the specified duration. - /// - public int NotFound { get; init; } - - /// - /// The amount of requests whose payload was too large during the specified duration. - /// - public int TooLarge { get; init; } - - /// - /// The amount of server errors hit during the specified duration. - /// - public int ServerErrors { get; init; } - - /// - /// The duration covered by these metrics. - /// - public TimeSpan Duration { get; init; } - - /// - /// Returns a human-readable string representation of these metrics. - /// - public override readonly string ToString() - { - StringBuilder builder = new($"Total Requests: {this.TotalRequests} during {this.Duration}\n"); - - if (this.SuccessfulRequests > 0) - { - builder.AppendLine - ( - CultureInfo.CurrentCulture, - $"Successful Requests: {this.SuccessfulRequests} ({Percentage(this.TotalRequests, this.SuccessfulRequests)})" - ); - } - - if (this.FailedRequests > 0) - { - builder.AppendLine - ( - CultureInfo.CurrentCulture, - $"Failed Requests: {this.FailedRequests} ({Percentage(this.TotalRequests, this.FailedRequests)})" - ); - - if (this.RatelimitsHit > 0) - { - builder.AppendLine - ( - CultureInfo.CurrentCulture, - $" - Ratelimits hit: {this.RatelimitsHit} ({Percentage(this.TotalRequests, this.RatelimitsHit)})" - ); - - if (this.GlobalRatelimitsHit > 0) - { - builder.AppendLine - ( - CultureInfo.CurrentCulture, - $" - Global ratelimits hit: {this.GlobalRatelimitsHit} ({Percentage(this.TotalRequests, this.GlobalRatelimitsHit)})" - ); - } - - if (this.BucketRatelimitsHit > 0) - { - builder.AppendLine - ( - CultureInfo.CurrentCulture, - $" - Bucket ratelimits hit: {this.BucketRatelimitsHit} ({Percentage(this.TotalRequests, this.BucketRatelimitsHit)})" - ); - } - } - - if (this.BadRequests > 0) - { - builder.AppendLine - ( - CultureInfo.CurrentCulture, - $" - Bad requests executed: {this.BadRequests} ({Percentage(this.TotalRequests, this.BadRequests)})" - ); - } - - if (this.Forbidden > 0) - { - builder.AppendLine - ( - CultureInfo.CurrentCulture, - $" - Forbidden/Unauthorized requests executed: {this.Forbidden} ({Percentage(this.TotalRequests, this.Forbidden)})" - ); - } - - if (this.NotFound > 0) - { - builder.AppendLine - ( - CultureInfo.CurrentCulture, - $" - Requests not found: {this.NotFound} ({Percentage(this.TotalRequests, this.NotFound)})" - ); - } - - if (this.TooLarge > 0) - { - builder.AppendLine - ( - CultureInfo.CurrentCulture, - $" - Requests too large: {this.TooLarge} ({Percentage(this.TotalRequests, this.TooLarge)})" - ); - } - - if (this.ServerErrors > 0) - { - builder.AppendLine - ( - CultureInfo.CurrentCulture, - $" - Server errors: {this.ServerErrors} ({Percentage(this.TotalRequests, this.ServerErrors)})" - ); - } - } - - return builder.ToString(); - } - - private static string Percentage(int total, int part) - { - double ratio = (double)part / total; - ratio *= 100; - return $"{ratio:N4}%"; - } -} +using System; +using System.Globalization; +using System.Text; + +namespace DSharpPlus.Metrics; + +/// +/// Represents an immutable snapshot of request metrics. +/// +public readonly record struct RequestMetricsCollection +{ + /// + /// The total amount of requests made during the specified duration. + /// + public int TotalRequests { get; init; } + + /// + /// The successful requests made during the specified duration. + /// + public int SuccessfulRequests { get; init; } + + /// + /// The failed requests made during the specified duration. + /// + public int FailedRequests => this.TotalRequests - this.SuccessfulRequests; + + /// + /// The amount of ratelimits hit during the specified duration. + /// + public int RatelimitsHit { get; init; } + + /// + /// The amount of global ratelimits hit during the specified duration. + /// + public int GlobalRatelimitsHit { get; init; } + + /// + /// The amount of bucket ratelimits hit during the specified duration. + /// + public int BucketRatelimitsHit { get; init; } + + /// + /// The amount of bad requests made during the specified duration. + /// + public int BadRequests { get; init; } + + /// + /// The amount of forbidden or unauthorized requests made during the specified duration. + /// + public int Forbidden { get; init; } + + /// + /// The amount of requests whose target could not be found made during the specified duration. + /// + public int NotFound { get; init; } + + /// + /// The amount of requests whose payload was too large during the specified duration. + /// + public int TooLarge { get; init; } + + /// + /// The amount of server errors hit during the specified duration. + /// + public int ServerErrors { get; init; } + + /// + /// The duration covered by these metrics. + /// + public TimeSpan Duration { get; init; } + + /// + /// Returns a human-readable string representation of these metrics. + /// + public override readonly string ToString() + { + StringBuilder builder = new($"Total Requests: {this.TotalRequests} during {this.Duration}\n"); + + if (this.SuccessfulRequests > 0) + { + builder.AppendLine + ( + CultureInfo.CurrentCulture, + $"Successful Requests: {this.SuccessfulRequests} ({Percentage(this.TotalRequests, this.SuccessfulRequests)})" + ); + } + + if (this.FailedRequests > 0) + { + builder.AppendLine + ( + CultureInfo.CurrentCulture, + $"Failed Requests: {this.FailedRequests} ({Percentage(this.TotalRequests, this.FailedRequests)})" + ); + + if (this.RatelimitsHit > 0) + { + builder.AppendLine + ( + CultureInfo.CurrentCulture, + $" - Ratelimits hit: {this.RatelimitsHit} ({Percentage(this.TotalRequests, this.RatelimitsHit)})" + ); + + if (this.GlobalRatelimitsHit > 0) + { + builder.AppendLine + ( + CultureInfo.CurrentCulture, + $" - Global ratelimits hit: {this.GlobalRatelimitsHit} ({Percentage(this.TotalRequests, this.GlobalRatelimitsHit)})" + ); + } + + if (this.BucketRatelimitsHit > 0) + { + builder.AppendLine + ( + CultureInfo.CurrentCulture, + $" - Bucket ratelimits hit: {this.BucketRatelimitsHit} ({Percentage(this.TotalRequests, this.BucketRatelimitsHit)})" + ); + } + } + + if (this.BadRequests > 0) + { + builder.AppendLine + ( + CultureInfo.CurrentCulture, + $" - Bad requests executed: {this.BadRequests} ({Percentage(this.TotalRequests, this.BadRequests)})" + ); + } + + if (this.Forbidden > 0) + { + builder.AppendLine + ( + CultureInfo.CurrentCulture, + $" - Forbidden/Unauthorized requests executed: {this.Forbidden} ({Percentage(this.TotalRequests, this.Forbidden)})" + ); + } + + if (this.NotFound > 0) + { + builder.AppendLine + ( + CultureInfo.CurrentCulture, + $" - Requests not found: {this.NotFound} ({Percentage(this.TotalRequests, this.NotFound)})" + ); + } + + if (this.TooLarge > 0) + { + builder.AppendLine + ( + CultureInfo.CurrentCulture, + $" - Requests too large: {this.TooLarge} ({Percentage(this.TotalRequests, this.TooLarge)})" + ); + } + + if (this.ServerErrors > 0) + { + builder.AppendLine + ( + CultureInfo.CurrentCulture, + $" - Server errors: {this.ServerErrors} ({Percentage(this.TotalRequests, this.ServerErrors)})" + ); + } + } + + return builder.ToString(); + } + + private static string Percentage(int total, int part) + { + double ratio = (double)part / total; + ratio *= 100; + return $"{ratio:N4}%"; + } +} diff --git a/DSharpPlus/Metrics/RequestMetricsContainer.cs b/DSharpPlus/Metrics/RequestMetricsContainer.cs index 102ce55cea..ed0605756f 100644 --- a/DSharpPlus/Metrics/RequestMetricsContainer.cs +++ b/DSharpPlus/Metrics/RequestMetricsContainer.cs @@ -1,132 +1,132 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http.Headers; -using System.Threading; - -namespace DSharpPlus.Metrics; - -internal sealed class RequestMetricsContainer -{ - private LiveRequestMetrics lifetime = default; - private LiveRequestMetrics temporal = default; - private DateTimeOffset lastReset = DateTimeOffset.UtcNow; - private readonly DateTimeOffset creation = DateTimeOffset.UtcNow; - - public RequestMetricsCollection GetLifetimeMetrics() - { - return new() - { - Duration = DateTimeOffset.UtcNow - this.creation, - - BadRequests = this.lifetime.badRequests, - BucketRatelimitsHit = this.lifetime.bucketRatelimits, - Forbidden = this.lifetime.forbidden, - GlobalRatelimitsHit = this.lifetime.globalRatelimits, - NotFound = this.lifetime.notFound, - RatelimitsHit = this.lifetime.ratelimits, - ServerErrors = this.lifetime.serverError, - SuccessfulRequests = this.lifetime.successful, - TooLarge = this.lifetime.tooLarge, - TotalRequests = this.lifetime.requests - }; - } - - public RequestMetricsCollection GetTemporalMetrics() - { - RequestMetricsCollection collection = new() - { - Duration = DateTimeOffset.UtcNow - this.lastReset, - - BadRequests = this.temporal.badRequests, - BucketRatelimitsHit = this.temporal.bucketRatelimits, - Forbidden = this.temporal.forbidden, - GlobalRatelimitsHit = this.temporal.globalRatelimits, - NotFound = this.temporal.notFound, - RatelimitsHit = this.temporal.ratelimits, - ServerErrors = this.temporal.serverError, - SuccessfulRequests = this.temporal.successful, - TooLarge = this.temporal.tooLarge, - TotalRequests = this.temporal.requests - }; - - this.lastReset = DateTimeOffset.UtcNow; - this.temporal = default; - - return collection; - } - - public void RegisterBadRequest() - { - Interlocked.Increment(ref this.lifetime.badRequests); - Interlocked.Increment(ref this.temporal.badRequests); - - Interlocked.Increment(ref this.lifetime.requests); - Interlocked.Increment(ref this.temporal.requests); - } - - public void RegisterForbidden() - { - Interlocked.Increment(ref this.lifetime.forbidden); - Interlocked.Increment(ref this.temporal.forbidden); - - Interlocked.Increment(ref this.lifetime.requests); - Interlocked.Increment(ref this.temporal.requests); - } - - public void RegisterNotFound() - { - Interlocked.Increment(ref this.lifetime.notFound); - Interlocked.Increment(ref this.temporal.notFound); - - Interlocked.Increment(ref this.lifetime.requests); - Interlocked.Increment(ref this.temporal.requests); - } - - public void RegisterRequestTooLarge() - { - Interlocked.Increment(ref this.lifetime.tooLarge); - Interlocked.Increment(ref this.temporal.tooLarge); - - Interlocked.Increment(ref this.lifetime.requests); - Interlocked.Increment(ref this.temporal.requests); - } - - public void RegisterRatelimitHit(HttpResponseHeaders headers) - { - if (headers.TryGetValues("x-ratelimit-scope", out IEnumerable? values) && values.First() == "global") - { - Interlocked.Increment(ref this.lifetime.globalRatelimits); - Interlocked.Increment(ref this.temporal.globalRatelimits); - } - else - { - Interlocked.Increment(ref this.lifetime.bucketRatelimits); - Interlocked.Increment(ref this.temporal.bucketRatelimits); - } - - Interlocked.Increment(ref this.lifetime.ratelimits); - Interlocked.Increment(ref this.temporal.ratelimits); - - Interlocked.Increment(ref this.lifetime.requests); - Interlocked.Increment(ref this.temporal.requests); - } - - public void RegisterServerError() - { - Interlocked.Increment(ref this.lifetime.serverError); - Interlocked.Increment(ref this.temporal.serverError); - - Interlocked.Increment(ref this.lifetime.requests); - Interlocked.Increment(ref this.temporal.requests); - } - - public void RegisterSuccess() - { - Interlocked.Increment(ref this.lifetime.successful); - Interlocked.Increment(ref this.temporal.successful); - - Interlocked.Increment(ref this.lifetime.requests); - Interlocked.Increment(ref this.temporal.requests); - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http.Headers; +using System.Threading; + +namespace DSharpPlus.Metrics; + +internal sealed class RequestMetricsContainer +{ + private LiveRequestMetrics lifetime = default; + private LiveRequestMetrics temporal = default; + private DateTimeOffset lastReset = DateTimeOffset.UtcNow; + private readonly DateTimeOffset creation = DateTimeOffset.UtcNow; + + public RequestMetricsCollection GetLifetimeMetrics() + { + return new() + { + Duration = DateTimeOffset.UtcNow - this.creation, + + BadRequests = this.lifetime.badRequests, + BucketRatelimitsHit = this.lifetime.bucketRatelimits, + Forbidden = this.lifetime.forbidden, + GlobalRatelimitsHit = this.lifetime.globalRatelimits, + NotFound = this.lifetime.notFound, + RatelimitsHit = this.lifetime.ratelimits, + ServerErrors = this.lifetime.serverError, + SuccessfulRequests = this.lifetime.successful, + TooLarge = this.lifetime.tooLarge, + TotalRequests = this.lifetime.requests + }; + } + + public RequestMetricsCollection GetTemporalMetrics() + { + RequestMetricsCollection collection = new() + { + Duration = DateTimeOffset.UtcNow - this.lastReset, + + BadRequests = this.temporal.badRequests, + BucketRatelimitsHit = this.temporal.bucketRatelimits, + Forbidden = this.temporal.forbidden, + GlobalRatelimitsHit = this.temporal.globalRatelimits, + NotFound = this.temporal.notFound, + RatelimitsHit = this.temporal.ratelimits, + ServerErrors = this.temporal.serverError, + SuccessfulRequests = this.temporal.successful, + TooLarge = this.temporal.tooLarge, + TotalRequests = this.temporal.requests + }; + + this.lastReset = DateTimeOffset.UtcNow; + this.temporal = default; + + return collection; + } + + public void RegisterBadRequest() + { + Interlocked.Increment(ref this.lifetime.badRequests); + Interlocked.Increment(ref this.temporal.badRequests); + + Interlocked.Increment(ref this.lifetime.requests); + Interlocked.Increment(ref this.temporal.requests); + } + + public void RegisterForbidden() + { + Interlocked.Increment(ref this.lifetime.forbidden); + Interlocked.Increment(ref this.temporal.forbidden); + + Interlocked.Increment(ref this.lifetime.requests); + Interlocked.Increment(ref this.temporal.requests); + } + + public void RegisterNotFound() + { + Interlocked.Increment(ref this.lifetime.notFound); + Interlocked.Increment(ref this.temporal.notFound); + + Interlocked.Increment(ref this.lifetime.requests); + Interlocked.Increment(ref this.temporal.requests); + } + + public void RegisterRequestTooLarge() + { + Interlocked.Increment(ref this.lifetime.tooLarge); + Interlocked.Increment(ref this.temporal.tooLarge); + + Interlocked.Increment(ref this.lifetime.requests); + Interlocked.Increment(ref this.temporal.requests); + } + + public void RegisterRatelimitHit(HttpResponseHeaders headers) + { + if (headers.TryGetValues("x-ratelimit-scope", out IEnumerable? values) && values.First() == "global") + { + Interlocked.Increment(ref this.lifetime.globalRatelimits); + Interlocked.Increment(ref this.temporal.globalRatelimits); + } + else + { + Interlocked.Increment(ref this.lifetime.bucketRatelimits); + Interlocked.Increment(ref this.temporal.bucketRatelimits); + } + + Interlocked.Increment(ref this.lifetime.ratelimits); + Interlocked.Increment(ref this.temporal.ratelimits); + + Interlocked.Increment(ref this.lifetime.requests); + Interlocked.Increment(ref this.temporal.requests); + } + + public void RegisterServerError() + { + Interlocked.Increment(ref this.lifetime.serverError); + Interlocked.Increment(ref this.temporal.serverError); + + Interlocked.Increment(ref this.lifetime.requests); + Interlocked.Increment(ref this.temporal.requests); + } + + public void RegisterSuccess() + { + Interlocked.Increment(ref this.lifetime.successful); + Interlocked.Increment(ref this.temporal.successful); + + Interlocked.Increment(ref this.lifetime.requests); + Interlocked.Increment(ref this.temporal.requests); + } +} diff --git a/DSharpPlus/Net/Abstractions/AuditLogAbstractions.cs b/DSharpPlus/Net/Abstractions/AuditLogAbstractions.cs index 94405e1639..e59110fbd8 100644 --- a/DSharpPlus/Net/Abstractions/AuditLogAbstractions.cs +++ b/DSharpPlus/Net/Abstractions/AuditLogAbstractions.cs @@ -1,150 +1,150 @@ -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using DSharpPlus.Entities; -using DSharpPlus.Entities.AuditLogs; -using DSharpPlus.Net.Serialization; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace DSharpPlus.Net.Abstractions; - -internal sealed class AuditLogActionChange -{ - // this can be a string or an array - [JsonProperty("old_value")] - public object OldValue { get; set; } - - [JsonIgnore] - public IEnumerable OldValues - => (this.OldValue as JArray)?.ToDiscordObject>(); - - [JsonIgnore] - public ulong OldValueUlong - => (ulong)this.OldValue; - - [JsonIgnore] - public string OldValueString - => (string)this.OldValue; - - [JsonIgnore] - public bool OldValueBool - => (bool)this.OldValue; - - [JsonIgnore] - public long OldValueLong - => (long)this.OldValue; - - // this can be a string or an array - [JsonProperty("new_value")] - public object NewValue { get; set; } - - [JsonIgnore] - public IEnumerable NewValues - => (this.NewValue as JArray)?.ToDiscordObject>(); - - [JsonIgnore] - public ulong NewValueUlong - => (ulong)this.NewValue; - - [JsonIgnore] - public string NewValueString - => (string)this.NewValue; - - [JsonIgnore] - public bool NewValueBool - => (bool)this.NewValue; - - [JsonIgnore] - public long NewValueLong - => (long)this.NewValue; - - [JsonProperty("key")] - public string Key { get; set; } -} - -internal sealed class AuditLogActionOptions -{ - [JsonProperty("application_id")] - public ulong ApplicationId { get; set; } - - [JsonProperty("auto_moderation_rule_name")] - public string AutoModerationRuleName { get; set; } - - [JsonProperty("auto_moderation_rule_trigger_type")] - public string AutoModerationRuleTriggerType { get; set; } - - [JsonProperty("channel_id")] - public ulong ChannelId { get; set; } - - [JsonProperty("count")] - public int Count { get; set; } - - [JsonProperty("delete_member_days")] - public int DeleteMemberDays { get; set; } - - [JsonProperty("id")] - public ulong Id { get; set; } - - [JsonProperty("members_removed")] - public int MembersRemoved { get; set; } - - [JsonProperty("message_id")] - public ulong MessageId { get; set; } - - [JsonProperty("role_name")] - public string RoleName { get; set; } - - [JsonProperty("type")] - public object Type { get; set; } -} - -internal sealed class AuditLogAction -{ - [JsonProperty("target_id")] - public ulong? TargetId { get; set; } - - [JsonProperty("user_id")] - public ulong? UserId { get; set; } - - [JsonProperty("id")] - public ulong Id { get; set; } - - [JsonProperty("action_type")] - public DiscordAuditLogActionType ActionType { get; set; } - - [JsonProperty("changes")] - public IEnumerable? Changes { get; set; } - - [JsonProperty("options")] - public AuditLogActionOptions? Options { get; set; } - - [JsonProperty("reason")] - public string? Reason { get; set; } -} - -internal sealed class AuditLog -{ - [JsonProperty("application_commands"), SuppressMessage("CodeQuality", "IDE0051:Remove unread private members", Justification = "This is used by JSON.NET")] - private IEnumerable SlashCommands { get; set; } - - [JsonProperty("audit_log_entries")] - public IEnumerable Entries { get; set; } - - [JsonProperty("auto_moderation_rules"), SuppressMessage("CodeQuality", "IDE0051:Remove unread private members", Justification = "This is used by JSON.NET")] - private IEnumerable AutoModerationRules { get; set; } - - [JsonProperty("guild_scheduled_events")] - public IEnumerable Events { get; set; } - - [JsonProperty("integrations")] - public IEnumerable Integrations { get; set; } - - [JsonProperty("threads")] - public IEnumerable Threads { get; set; } - - [JsonProperty("users")] - public IEnumerable Users { get; set; } - - [JsonProperty("webhooks")] - public IEnumerable Webhooks { get; set; } -} +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using DSharpPlus.Entities; +using DSharpPlus.Entities.AuditLogs; +using DSharpPlus.Net.Serialization; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace DSharpPlus.Net.Abstractions; + +internal sealed class AuditLogActionChange +{ + // this can be a string or an array + [JsonProperty("old_value")] + public object OldValue { get; set; } + + [JsonIgnore] + public IEnumerable OldValues + => (this.OldValue as JArray)?.ToDiscordObject>(); + + [JsonIgnore] + public ulong OldValueUlong + => (ulong)this.OldValue; + + [JsonIgnore] + public string OldValueString + => (string)this.OldValue; + + [JsonIgnore] + public bool OldValueBool + => (bool)this.OldValue; + + [JsonIgnore] + public long OldValueLong + => (long)this.OldValue; + + // this can be a string or an array + [JsonProperty("new_value")] + public object NewValue { get; set; } + + [JsonIgnore] + public IEnumerable NewValues + => (this.NewValue as JArray)?.ToDiscordObject>(); + + [JsonIgnore] + public ulong NewValueUlong + => (ulong)this.NewValue; + + [JsonIgnore] + public string NewValueString + => (string)this.NewValue; + + [JsonIgnore] + public bool NewValueBool + => (bool)this.NewValue; + + [JsonIgnore] + public long NewValueLong + => (long)this.NewValue; + + [JsonProperty("key")] + public string Key { get; set; } +} + +internal sealed class AuditLogActionOptions +{ + [JsonProperty("application_id")] + public ulong ApplicationId { get; set; } + + [JsonProperty("auto_moderation_rule_name")] + public string AutoModerationRuleName { get; set; } + + [JsonProperty("auto_moderation_rule_trigger_type")] + public string AutoModerationRuleTriggerType { get; set; } + + [JsonProperty("channel_id")] + public ulong ChannelId { get; set; } + + [JsonProperty("count")] + public int Count { get; set; } + + [JsonProperty("delete_member_days")] + public int DeleteMemberDays { get; set; } + + [JsonProperty("id")] + public ulong Id { get; set; } + + [JsonProperty("members_removed")] + public int MembersRemoved { get; set; } + + [JsonProperty("message_id")] + public ulong MessageId { get; set; } + + [JsonProperty("role_name")] + public string RoleName { get; set; } + + [JsonProperty("type")] + public object Type { get; set; } +} + +internal sealed class AuditLogAction +{ + [JsonProperty("target_id")] + public ulong? TargetId { get; set; } + + [JsonProperty("user_id")] + public ulong? UserId { get; set; } + + [JsonProperty("id")] + public ulong Id { get; set; } + + [JsonProperty("action_type")] + public DiscordAuditLogActionType ActionType { get; set; } + + [JsonProperty("changes")] + public IEnumerable? Changes { get; set; } + + [JsonProperty("options")] + public AuditLogActionOptions? Options { get; set; } + + [JsonProperty("reason")] + public string? Reason { get; set; } +} + +internal sealed class AuditLog +{ + [JsonProperty("application_commands"), SuppressMessage("CodeQuality", "IDE0051:Remove unread private members", Justification = "This is used by JSON.NET")] + private IEnumerable SlashCommands { get; set; } + + [JsonProperty("audit_log_entries")] + public IEnumerable Entries { get; set; } + + [JsonProperty("auto_moderation_rules"), SuppressMessage("CodeQuality", "IDE0051:Remove unread private members", Justification = "This is used by JSON.NET")] + private IEnumerable AutoModerationRules { get; set; } + + [JsonProperty("guild_scheduled_events")] + public IEnumerable Events { get; set; } + + [JsonProperty("integrations")] + public IEnumerable Integrations { get; set; } + + [JsonProperty("threads")] + public IEnumerable Threads { get; set; } + + [JsonProperty("users")] + public IEnumerable Users { get; set; } + + [JsonProperty("webhooks")] + public IEnumerable Webhooks { get; set; } +} diff --git a/DSharpPlus/Net/Abstractions/ClientProperties.cs b/DSharpPlus/Net/Abstractions/ClientProperties.cs index f855715cbf..43908437e1 100644 --- a/DSharpPlus/Net/Abstractions/ClientProperties.cs +++ b/DSharpPlus/Net/Abstractions/ClientProperties.cs @@ -1,71 +1,71 @@ -using System.Diagnostics.CodeAnalysis; -using System.Reflection; -using System.Runtime.InteropServices; -using Newtonsoft.Json; - -namespace DSharpPlus.Net.Abstractions; - -/// -/// Represents data for identify payload's client properties. -/// -internal sealed class ClientProperties -{ - /// - /// Gets the client's operating system. - /// - [JsonProperty("os"), SuppressMessage("Quality Assurance", "CA1822:Mark members as static", Justification = "This is a JSON-serializable object.")] - public string OperatingSystem - { - get - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - return "windows"; - } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - return "linux"; - } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - { - return "osx"; - } - - string plat = RuntimeInformation.OSDescription.ToLowerInvariant(); - return plat.Contains("freebsd") - ? "freebsd" - : plat.Contains("openbsd") - ? "openbsd" - : plat.Contains("netbsd") - ? "netbsd" - : plat.Contains("dragonfly") - ? "dragonflybsd" - : plat.Contains("miros bsd") || plat.Contains("mirbsd") - ? "miros bsd" - : plat.Contains("desktopbsd") - ? "desktopbsd" - : plat.Contains("darwin") ? "osx" : plat.Contains("unix") ? "unix" : "toaster (unknown)"; - } - } - - /// - /// Gets the client's browser. - /// - [JsonProperty("browser"), SuppressMessage("Quality Assurance", "CA1822:Mark members as static", Justification = "This is a JSON-serializable object.")] - public string Browser - { - get - { - Assembly a = typeof(DiscordClient).GetTypeInfo().Assembly; - AssemblyName an = a.GetName(); - return $"DSharpPlus {an.Version.ToString(4)}"; - } - } - - /// - /// Gets the client's device. - /// - [JsonProperty("device")] - public string Device - => this.Browser; -} +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Runtime.InteropServices; +using Newtonsoft.Json; + +namespace DSharpPlus.Net.Abstractions; + +/// +/// Represents data for identify payload's client properties. +/// +internal sealed class ClientProperties +{ + /// + /// Gets the client's operating system. + /// + [JsonProperty("os"), SuppressMessage("Quality Assurance", "CA1822:Mark members as static", Justification = "This is a JSON-serializable object.")] + public string OperatingSystem + { + get + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return "windows"; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + return "linux"; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + return "osx"; + } + + string plat = RuntimeInformation.OSDescription.ToLowerInvariant(); + return plat.Contains("freebsd") + ? "freebsd" + : plat.Contains("openbsd") + ? "openbsd" + : plat.Contains("netbsd") + ? "netbsd" + : plat.Contains("dragonfly") + ? "dragonflybsd" + : plat.Contains("miros bsd") || plat.Contains("mirbsd") + ? "miros bsd" + : plat.Contains("desktopbsd") + ? "desktopbsd" + : plat.Contains("darwin") ? "osx" : plat.Contains("unix") ? "unix" : "toaster (unknown)"; + } + } + + /// + /// Gets the client's browser. + /// + [JsonProperty("browser"), SuppressMessage("Quality Assurance", "CA1822:Mark members as static", Justification = "This is a JSON-serializable object.")] + public string Browser + { + get + { + Assembly a = typeof(DiscordClient).GetTypeInfo().Assembly; + AssemblyName an = a.GetName(); + return $"DSharpPlus {an.Version.ToString(4)}"; + } + } + + /// + /// Gets the client's device. + /// + [JsonProperty("device")] + public string Device + => this.Browser; +} diff --git a/DSharpPlus/Net/Abstractions/FollowedChannelAddPayload.cs b/DSharpPlus/Net/Abstractions/FollowedChannelAddPayload.cs index 3c7cc2f8cb..e8afca5d34 100644 --- a/DSharpPlus/Net/Abstractions/FollowedChannelAddPayload.cs +++ b/DSharpPlus/Net/Abstractions/FollowedChannelAddPayload.cs @@ -1,9 +1,9 @@ -using Newtonsoft.Json; - -namespace DSharpPlus.Net.Abstractions; - -internal sealed class FollowedChannelAddPayload -{ - [JsonProperty("webhook_channel_id")] - public ulong WebhookChannelId { get; set; } -} +using Newtonsoft.Json; + +namespace DSharpPlus.Net.Abstractions; + +internal sealed class FollowedChannelAddPayload +{ + [JsonProperty("webhook_channel_id")] + public ulong WebhookChannelId { get; set; } +} diff --git a/DSharpPlus/Net/Abstractions/Gateway/GatewayHello.cs b/DSharpPlus/Net/Abstractions/Gateway/GatewayHello.cs index f331bbcb22..8cb3c553d5 100644 --- a/DSharpPlus/Net/Abstractions/Gateway/GatewayHello.cs +++ b/DSharpPlus/Net/Abstractions/Gateway/GatewayHello.cs @@ -1,22 +1,22 @@ -using System.Collections.Generic; -using Newtonsoft.Json; - -namespace DSharpPlus.Net.Abstractions; - -/// -/// Represents data for a websocket hello payload. -/// -internal sealed class GatewayHello -{ - /// - /// Gets the target heartbeat interval (in milliseconds) requested by Discord. - /// - [JsonProperty("heartbeat_interval")] - public int HeartbeatInterval { get; private set; } - - /// - /// Gets debug data sent by Discord. This contains a list of servers to which the client is connected. - /// - [JsonProperty("_trace")] - public IReadOnlyList Trace { get; private set; } -} +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace DSharpPlus.Net.Abstractions; + +/// +/// Represents data for a websocket hello payload. +/// +internal sealed class GatewayHello +{ + /// + /// Gets the target heartbeat interval (in milliseconds) requested by Discord. + /// + [JsonProperty("heartbeat_interval")] + public int HeartbeatInterval { get; private set; } + + /// + /// Gets debug data sent by Discord. This contains a list of servers to which the client is connected. + /// + [JsonProperty("_trace")] + public IReadOnlyList Trace { get; private set; } +} diff --git a/DSharpPlus/Net/Abstractions/Gateway/GatewayIdentifyResume.cs b/DSharpPlus/Net/Abstractions/Gateway/GatewayIdentifyResume.cs index db0a8b852a..afc861d2b6 100644 --- a/DSharpPlus/Net/Abstractions/Gateway/GatewayIdentifyResume.cs +++ b/DSharpPlus/Net/Abstractions/Gateway/GatewayIdentifyResume.cs @@ -1,75 +1,75 @@ -using Newtonsoft.Json; - -namespace DSharpPlus.Net.Abstractions; - -/// -/// Represents data for websocket identify payload. -/// -internal sealed class GatewayIdentify -{ - /// - /// Gets or sets the token used to identify the client to Discord. - /// - [JsonProperty("token")] - public string Token { get; set; } - - /// - /// Gets or sets the client's properties. - /// - [JsonProperty("properties")] - public ClientProperties ClientProperties { get; } = new ClientProperties(); - - /// - /// Gets or sets whether to encrypt websocket traffic. - /// - [JsonProperty("compress", DefaultValueHandling = DefaultValueHandling.Ignore)] - public bool Compress { get; set; } - - /// - /// Gets or sets the member count at which the guild is to be considered large. - /// - [JsonProperty("large_threshold")] - public int LargeThreshold { get; set; } - - /// - /// Gets or sets the shard info for this connection. - /// - [JsonProperty("shard", NullValueHandling = NullValueHandling.Ignore)] - public ShardInfo? ShardInfo { get; set; } - - /// - /// Gets or sets the presence for this connection. - /// - [JsonProperty("presence", NullValueHandling = NullValueHandling.Ignore)] - public StatusUpdate? Presence { get; set; } = null; - - /// - /// Gets or sets the intent flags for this connection. - /// - [JsonProperty("intents")] - public DiscordIntents Intents { get; set; } -} - -/// -/// Represents data for websocket identify payload. -/// -internal sealed class GatewayResume -{ - /// - /// Gets or sets the token used to identify the client to Discord. - /// - [JsonProperty("token")] - public string Token { get; set; } - - /// - /// Gets or sets the session id used to resume last session. - /// - [JsonProperty("session_id")] - public string SessionId { get; set; } - - /// - /// Gets or sets the last received sequence number. - /// - [JsonProperty("seq")] - public long SequenceNumber { get; set; } -} +using Newtonsoft.Json; + +namespace DSharpPlus.Net.Abstractions; + +/// +/// Represents data for websocket identify payload. +/// +internal sealed class GatewayIdentify +{ + /// + /// Gets or sets the token used to identify the client to Discord. + /// + [JsonProperty("token")] + public string Token { get; set; } + + /// + /// Gets or sets the client's properties. + /// + [JsonProperty("properties")] + public ClientProperties ClientProperties { get; } = new ClientProperties(); + + /// + /// Gets or sets whether to encrypt websocket traffic. + /// + [JsonProperty("compress", DefaultValueHandling = DefaultValueHandling.Ignore)] + public bool Compress { get; set; } + + /// + /// Gets or sets the member count at which the guild is to be considered large. + /// + [JsonProperty("large_threshold")] + public int LargeThreshold { get; set; } + + /// + /// Gets or sets the shard info for this connection. + /// + [JsonProperty("shard", NullValueHandling = NullValueHandling.Ignore)] + public ShardInfo? ShardInfo { get; set; } + + /// + /// Gets or sets the presence for this connection. + /// + [JsonProperty("presence", NullValueHandling = NullValueHandling.Ignore)] + public StatusUpdate? Presence { get; set; } = null; + + /// + /// Gets or sets the intent flags for this connection. + /// + [JsonProperty("intents")] + public DiscordIntents Intents { get; set; } +} + +/// +/// Represents data for websocket identify payload. +/// +internal sealed class GatewayResume +{ + /// + /// Gets or sets the token used to identify the client to Discord. + /// + [JsonProperty("token")] + public string Token { get; set; } + + /// + /// Gets or sets the session id used to resume last session. + /// + [JsonProperty("session_id")] + public string SessionId { get; set; } + + /// + /// Gets or sets the last received sequence number. + /// + [JsonProperty("seq")] + public long SequenceNumber { get; set; } +} diff --git a/DSharpPlus/Net/Abstractions/Gateway/GatewayInfo.cs b/DSharpPlus/Net/Abstractions/Gateway/GatewayInfo.cs index 30fd9c6740..6c56403737 100644 --- a/DSharpPlus/Net/Abstractions/Gateway/GatewayInfo.cs +++ b/DSharpPlus/Net/Abstractions/Gateway/GatewayInfo.cs @@ -1,27 +1,27 @@ -using Newtonsoft.Json; - -namespace DSharpPlus.Net; - -/// -/// Represents information used to identify with Discord. -/// -public sealed class GatewayInfo -{ - /// - /// Gets the gateway URL for the WebSocket connection. - /// - [JsonProperty("url")] - public string Url { get; set; } - - /// - /// Gets the recommended amount of shards. - /// - [JsonProperty("shards")] - public int ShardCount { get; internal set; } - - /// - /// Gets the session start limit data. - /// - [JsonProperty("session_start_limit")] - public SessionBucket SessionBucket { get; internal set; } -} +using Newtonsoft.Json; + +namespace DSharpPlus.Net; + +/// +/// Represents information used to identify with Discord. +/// +public sealed class GatewayInfo +{ + /// + /// Gets the gateway URL for the WebSocket connection. + /// + [JsonProperty("url")] + public string Url { get; set; } + + /// + /// Gets the recommended amount of shards. + /// + [JsonProperty("shards")] + public int ShardCount { get; internal set; } + + /// + /// Gets the session start limit data. + /// + [JsonProperty("session_start_limit")] + public SessionBucket SessionBucket { get; internal set; } +} diff --git a/DSharpPlus/Net/Abstractions/Gateway/GatewayOpCode.cs b/DSharpPlus/Net/Abstractions/Gateway/GatewayOpCode.cs index d8bfdfaf64..043ec24bad 100644 --- a/DSharpPlus/Net/Abstractions/Gateway/GatewayOpCode.cs +++ b/DSharpPlus/Net/Abstractions/Gateway/GatewayOpCode.cs @@ -1,73 +1,73 @@ -namespace DSharpPlus.Net.Abstractions; - - -/// -/// Specifies an OP code in a gateway payload. -/// -public enum GatewayOpCode : int -{ - /// - /// Used for dispatching events. - /// - Dispatch = 0, - - /// - /// Used for pinging the gateway or client, to ensure the connection is still alive. - /// - Heartbeat = 1, - - /// - /// Used for initial handshake with the gateway. - /// - Identify = 2, - - /// - /// Used to update client status. - /// - StatusUpdate = 3, - - /// - /// Used to update voice state, when joining, leaving, or moving between voice channels. - /// - VoiceStateUpdate = 4, - - /// - /// Used for pinging the voice gateway or client, to ensure the connection is still alive. - /// - VoiceServerPing = 5, - - /// - /// Used to resume a closed connection. - /// - Resume = 6, - - /// - /// Used to notify the client that it has to reconnect. - /// - Reconnect = 7, - - /// - /// Used to request guild members. - /// - RequestGuildMembers = 8, - - /// - /// Used to notify the client about an invalidated session. - /// - InvalidSession = 9, - - /// - /// Used by the gateway upon connecting. - /// - Hello = 10, - - /// - /// Used to acknowledge a heartbeat. - /// - HeartbeatAck = 11, - - /// - /// Used to request guild synchronization. - /// - GuildSync = 12 -} +namespace DSharpPlus.Net.Abstractions; + + +/// +/// Specifies an OP code in a gateway payload. +/// +public enum GatewayOpCode : int +{ + /// + /// Used for dispatching events. + /// + Dispatch = 0, + + /// + /// Used for pinging the gateway or client, to ensure the connection is still alive. + /// + Heartbeat = 1, + + /// + /// Used for initial handshake with the gateway. + /// + Identify = 2, + + /// + /// Used to update client status. + /// + StatusUpdate = 3, + + /// + /// Used to update voice state, when joining, leaving, or moving between voice channels. + /// + VoiceStateUpdate = 4, + + /// + /// Used for pinging the voice gateway or client, to ensure the connection is still alive. + /// + VoiceServerPing = 5, + + /// + /// Used to resume a closed connection. + /// + Resume = 6, + + /// + /// Used to notify the client that it has to reconnect. + /// + Reconnect = 7, + + /// + /// Used to request guild members. + /// + RequestGuildMembers = 8, + + /// + /// Used to notify the client about an invalidated session. + /// + InvalidSession = 9, + + /// + /// Used by the gateway upon connecting. + /// + Hello = 10, + + /// + /// Used to acknowledge a heartbeat. + /// + HeartbeatAck = 11, + + /// + /// Used to request guild synchronization. + /// + GuildSync = 12 +} diff --git a/DSharpPlus/Net/Abstractions/Gateway/GatewayPayload.cs b/DSharpPlus/Net/Abstractions/Gateway/GatewayPayload.cs index 59105d83ef..969af9f9c6 100644 --- a/DSharpPlus/Net/Abstractions/Gateway/GatewayPayload.cs +++ b/DSharpPlus/Net/Abstractions/Gateway/GatewayPayload.cs @@ -1,33 +1,33 @@ -using Newtonsoft.Json; - -namespace DSharpPlus.Net.Abstractions; - -/// -/// Represents a websocket payload exchanged between Discord and the client. -/// -public class GatewayPayload -{ - /// - /// Gets or sets the OP code of the payload. - /// - [JsonProperty("op")] - public GatewayOpCode OpCode { get; set; } - - /// - /// Gets or sets the data of the payload. - /// - [JsonProperty("d")] - public object Data { get; set; } - - /// - /// Gets or sets the sequence number of the payload. Only present for OP 0. - /// - [JsonProperty("s", NullValueHandling = NullValueHandling.Ignore)] - public int? Sequence { get; set; } - - /// - /// Gets or sets the event name of the payload. Only present for OP 0. - /// - [JsonProperty("t", NullValueHandling = NullValueHandling.Ignore)] - public string EventName { get; set; } -} +using Newtonsoft.Json; + +namespace DSharpPlus.Net.Abstractions; + +/// +/// Represents a websocket payload exchanged between Discord and the client. +/// +public class GatewayPayload +{ + /// + /// Gets or sets the OP code of the payload. + /// + [JsonProperty("op")] + public GatewayOpCode OpCode { get; set; } + + /// + /// Gets or sets the data of the payload. + /// + [JsonProperty("d")] + public object Data { get; set; } + + /// + /// Gets or sets the sequence number of the payload. Only present for OP 0. + /// + [JsonProperty("s", NullValueHandling = NullValueHandling.Ignore)] + public int? Sequence { get; set; } + + /// + /// Gets or sets the event name of the payload. Only present for OP 0. + /// + [JsonProperty("t", NullValueHandling = NullValueHandling.Ignore)] + public string EventName { get; set; } +} diff --git a/DSharpPlus/Net/Abstractions/Gateway/GatewayRequestGuildMembers.cs b/DSharpPlus/Net/Abstractions/Gateway/GatewayRequestGuildMembers.cs index 68d12eb8d5..cb7531635b 100644 --- a/DSharpPlus/Net/Abstractions/Gateway/GatewayRequestGuildMembers.cs +++ b/DSharpPlus/Net/Abstractions/Gateway/GatewayRequestGuildMembers.cs @@ -1,28 +1,28 @@ -using System.Collections.Generic; -using DSharpPlus.Entities; -using Newtonsoft.Json; - -namespace DSharpPlus.Net.Abstractions; - -internal sealed class GatewayRequestGuildMembers -{ - [JsonProperty("guild_id")] - public ulong GuildId { get; } - - [JsonProperty("query", NullValueHandling = NullValueHandling.Ignore)] - public string Query { get; set; } = null; - - [JsonProperty("limit")] - public int Limit { get; set; } = 0; - - [JsonProperty("presences", NullValueHandling = NullValueHandling.Ignore)] - public bool? Presences { get; set; } = null; - - [JsonProperty("user_ids", NullValueHandling = NullValueHandling.Ignore)] - public IEnumerable UserIds { get; set; } = null; - - [JsonProperty("nonce", NullValueHandling = NullValueHandling.Ignore)] - public string Nonce { get; internal set; } - - public GatewayRequestGuildMembers(DiscordGuild guild) => this.GuildId = guild.Id; -} +using System.Collections.Generic; +using DSharpPlus.Entities; +using Newtonsoft.Json; + +namespace DSharpPlus.Net.Abstractions; + +internal sealed class GatewayRequestGuildMembers +{ + [JsonProperty("guild_id")] + public ulong GuildId { get; } + + [JsonProperty("query", NullValueHandling = NullValueHandling.Ignore)] + public string Query { get; set; } = null; + + [JsonProperty("limit")] + public int Limit { get; set; } = 0; + + [JsonProperty("presences", NullValueHandling = NullValueHandling.Ignore)] + public bool? Presences { get; set; } = null; + + [JsonProperty("user_ids", NullValueHandling = NullValueHandling.Ignore)] + public IEnumerable UserIds { get; set; } = null; + + [JsonProperty("nonce", NullValueHandling = NullValueHandling.Ignore)] + public string Nonce { get; internal set; } + + public GatewayRequestGuildMembers(DiscordGuild guild) => this.GuildId = guild.Id; +} diff --git a/DSharpPlus/Net/Abstractions/IOAuth2Payload.cs b/DSharpPlus/Net/Abstractions/IOAuth2Payload.cs index f542433fed..9436f5021f 100644 --- a/DSharpPlus/Net/Abstractions/IOAuth2Payload.cs +++ b/DSharpPlus/Net/Abstractions/IOAuth2Payload.cs @@ -1,7 +1,7 @@ -namespace DSharpPlus.Net.Abstractions; - - -internal interface IOAuth2Payload -{ - public string AccessToken { get; set; } -} +namespace DSharpPlus.Net.Abstractions; + + +internal interface IOAuth2Payload +{ + public string AccessToken { get; set; } +} diff --git a/DSharpPlus/Net/Abstractions/PollCreatePayload.cs b/DSharpPlus/Net/Abstractions/PollCreatePayload.cs index e712e2a809..eba7d3179c 100644 --- a/DSharpPlus/Net/Abstractions/PollCreatePayload.cs +++ b/DSharpPlus/Net/Abstractions/PollCreatePayload.cs @@ -1,57 +1,57 @@ -using System.Collections.Generic; -using System.Linq; -using DSharpPlus.Entities; -using Newtonsoft.Json; - -namespace DSharpPlus.Net.Abstractions; - -public sealed class PollCreatePayload -{ - /// - /// Gets the question for this poll. Only text is supported. - /// - [JsonProperty("question")] - public DiscordPollMedia Question { get; internal set; } - - /// - /// Gets the answers available in the poll. - /// - [JsonProperty("answers")] - public IReadOnlyList Answers { get; internal set; } - - /// - /// Gets the expiry date for this poll. - /// - [JsonProperty("duration")] - public int Duration { get; internal set; } - - /// - /// Whether the poll allows for multiple answers. - /// - [JsonProperty("allow_multiselect")] - public bool AllowMultisect { get; internal set; } - - /// - /// Gets the layout type for this poll. Defaults to . - /// - [JsonProperty("layout_type", NullValueHandling = NullValueHandling.Ignore)] - public DiscordPollLayoutType? Layout { get; internal set; } - - internal PollCreatePayload() { } - - internal PollCreatePayload(DiscordPoll poll) - { - this.Question = poll.Question; - this.Answers = poll.Answers; - this.AllowMultisect = poll.AllowMultisect; - this.Layout = poll.Layout; - } - - internal PollCreatePayload(DiscordPollBuilder builder) - { - this.Question = new DiscordPollMedia { Text = builder.Question }; - this.Answers = builder.Options.Select(x => new DiscordPollAnswer { AnswerData = x }).ToList(); - this.AllowMultisect = builder.IsMultipleChoice; - this.Duration = builder.Duration; - } -} +using System.Collections.Generic; +using System.Linq; +using DSharpPlus.Entities; +using Newtonsoft.Json; + +namespace DSharpPlus.Net.Abstractions; + +public sealed class PollCreatePayload +{ + /// + /// Gets the question for this poll. Only text is supported. + /// + [JsonProperty("question")] + public DiscordPollMedia Question { get; internal set; } + + /// + /// Gets the answers available in the poll. + /// + [JsonProperty("answers")] + public IReadOnlyList Answers { get; internal set; } + + /// + /// Gets the expiry date for this poll. + /// + [JsonProperty("duration")] + public int Duration { get; internal set; } + + /// + /// Whether the poll allows for multiple answers. + /// + [JsonProperty("allow_multiselect")] + public bool AllowMultisect { get; internal set; } + + /// + /// Gets the layout type for this poll. Defaults to . + /// + [JsonProperty("layout_type", NullValueHandling = NullValueHandling.Ignore)] + public DiscordPollLayoutType? Layout { get; internal set; } + + internal PollCreatePayload() { } + + internal PollCreatePayload(DiscordPoll poll) + { + this.Question = poll.Question; + this.Answers = poll.Answers; + this.AllowMultisect = poll.AllowMultisect; + this.Layout = poll.Layout; + } + + internal PollCreatePayload(DiscordPollBuilder builder) + { + this.Question = new DiscordPollMedia { Text = builder.Question }; + this.Answers = builder.Options.Select(x => new DiscordPollAnswer { AnswerData = x }).ToList(); + this.AllowMultisect = builder.IsMultipleChoice; + this.Duration = builder.Duration; + } +} diff --git a/DSharpPlus/Net/Abstractions/ReadyPayload.cs b/DSharpPlus/Net/Abstractions/ReadyPayload.cs index 819d9a4ca9..bfa7dae49a 100644 --- a/DSharpPlus/Net/Abstractions/ReadyPayload.cs +++ b/DSharpPlus/Net/Abstractions/ReadyPayload.cs @@ -1,59 +1,59 @@ -using System.Collections.Generic; -using DSharpPlus.Entities; -using Newtonsoft.Json; - -namespace DSharpPlus.Net.Abstractions; - -/// -/// Represents data for websocket ready event payload. -/// -internal class ReadyPayload -{ - /// - /// Gets the gateway version the client is connectected to. - /// - [JsonProperty("v")] - public int GatewayVersion { get; private set; } - - /// - /// Gets the current user. - /// - [JsonProperty("user")] - public TransportUser CurrentUser { get; private set; } - - /// - /// Gets the private channels available for this shard. - /// - [JsonProperty("private_channels")] - public IReadOnlyList DmChannels { get; private set; } - - /// - /// Gets the guilds available for this shard. - /// - [JsonProperty("guilds")] - public IReadOnlyList Guilds { get; private set; } - - /// - /// Gets the current session's ID. - /// - [JsonProperty("session_id")] - public string SessionId { get; private set; } - - /// - /// Gets the url which should be used for resuming the session after disconnect/reconnect - /// - [JsonProperty("resume_gateway_url")] - public string ResumeGatewayUrl { get; private set; } - - /// - /// Gets the current application sent by Discord. - /// - [JsonProperty("application")] - public DiscordApplication Application { get; private set; } - - /// - /// Gets debug data sent by Discord. This contains a list of servers to which the client is connected. - /// - [JsonProperty("_trace")] - public IReadOnlyList Trace { get; private set; } -} +using System.Collections.Generic; +using DSharpPlus.Entities; +using Newtonsoft.Json; + +namespace DSharpPlus.Net.Abstractions; + +/// +/// Represents data for websocket ready event payload. +/// +internal class ReadyPayload +{ + /// + /// Gets the gateway version the client is connectected to. + /// + [JsonProperty("v")] + public int GatewayVersion { get; private set; } + + /// + /// Gets the current user. + /// + [JsonProperty("user")] + public TransportUser CurrentUser { get; private set; } + + /// + /// Gets the private channels available for this shard. + /// + [JsonProperty("private_channels")] + public IReadOnlyList DmChannels { get; private set; } + + /// + /// Gets the guilds available for this shard. + /// + [JsonProperty("guilds")] + public IReadOnlyList Guilds { get; private set; } + + /// + /// Gets the current session's ID. + /// + [JsonProperty("session_id")] + public string SessionId { get; private set; } + + /// + /// Gets the url which should be used for resuming the session after disconnect/reconnect + /// + [JsonProperty("resume_gateway_url")] + public string ResumeGatewayUrl { get; private set; } + + /// + /// Gets the current application sent by Discord. + /// + [JsonProperty("application")] + public DiscordApplication Application { get; private set; } + + /// + /// Gets debug data sent by Discord. This contains a list of servers to which the client is connected. + /// + [JsonProperty("_trace")] + public IReadOnlyList Trace { get; private set; } +} diff --git a/DSharpPlus/Net/Abstractions/Rest/RestApplicationCommandPayloads.cs b/DSharpPlus/Net/Abstractions/Rest/RestApplicationCommandPayloads.cs index 86ea6d612b..d79cf0cb48 100644 --- a/DSharpPlus/Net/Abstractions/Rest/RestApplicationCommandPayloads.cs +++ b/DSharpPlus/Net/Abstractions/Rest/RestApplicationCommandPayloads.cs @@ -1,130 +1,130 @@ -using System.Collections.Generic; -using DSharpPlus.Entities; -using DSharpPlus.Net.Serialization; -using Newtonsoft.Json; - -namespace DSharpPlus.Net.Abstractions; - -internal class RestApplicationCommandCreatePayload -{ - [JsonProperty("type")] - public DiscordApplicationCommandType Type { get; set; } - - [JsonProperty("name")] - public string Name { get; set; } - - [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] - public string Description { get; set; } - - [JsonProperty("options", NullValueHandling = NullValueHandling.Ignore)] - public IEnumerable Options { get; set; } - - [JsonProperty("default_permission", NullValueHandling = NullValueHandling.Ignore)] - public bool? DefaultPermission { get; set; } - - [JsonProperty("name_localizations")] - public IReadOnlyDictionary NameLocalizations { get; set; } - - [JsonProperty("description_localizations")] - public IReadOnlyDictionary DescriptionLocalizations { get; set; } - - [JsonProperty("dm_permission", NullValueHandling = NullValueHandling.Ignore)] - public bool? AllowDMUsage { get; set; } - - [JsonProperty("default_member_permissions", NullValueHandling = NullValueHandling.Ignore)] - [JsonConverter(typeof(DiscordPermissionsAsStringJsonConverter))] - public DiscordPermissions? DefaultMemberPermissions { get; set; } - - [JsonProperty("nsfw", NullValueHandling = NullValueHandling.Ignore)] - public bool? NSFW { get; set; } - - /// - /// Interaction context(s) where the command can be used. - /// - [JsonProperty("contexts", NullValueHandling = NullValueHandling.Ignore)] - public IEnumerable? AllowedContexts { get; set; } - - /// - /// Installation context(s) where the command is available. - /// - [JsonProperty("integration_types", NullValueHandling = NullValueHandling.Ignore)] - public IEnumerable? InstallTypes { get; set; } -} - -internal class RestApplicationCommandEditPayload -{ - [JsonProperty("name")] - public Optional Name { get; set; } - - [JsonProperty("description")] - public Optional Description { get; set; } - - [JsonProperty("options")] - public Optional> Options { get; set; } - - [JsonProperty("default_permission", NullValueHandling = NullValueHandling.Ignore)] - public Optional DefaultPermission { get; set; } - - [JsonProperty("name_localizations")] - public IReadOnlyDictionary? NameLocalizations { get; set; } - - [JsonProperty("description_localizations")] - public IReadOnlyDictionary? DescriptionLocalizations { get; set; } - - [JsonProperty("dm_permission", NullValueHandling = NullValueHandling.Ignore)] - public Optional AllowDMUsage { get; set; } - - [JsonProperty("default_member_permissions", NullValueHandling = NullValueHandling.Ignore)] - public Optional DefaultMemberPermissions { get; set; } - - [JsonProperty("nsfw", NullValueHandling = NullValueHandling.Ignore)] - public Optional NSFW { get; set; } - - /// - /// Interaction context(s) where the command can be used. - /// - [JsonProperty("contexts", NullValueHandling = NullValueHandling.Ignore)] - public Optional> AllowedContexts { get; set; } - - /// - /// Installation context(s) where the command is available. - /// - [JsonProperty("integration_types", NullValueHandling = NullValueHandling.Ignore)] - public Optional> InstallTypes { get; set; } -} - -internal class DiscordInteractionResponsePayload -{ - [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] - public DiscordInteractionResponseType Type { get; set; } - - [JsonProperty("data", NullValueHandling = NullValueHandling.Ignore)] - public DiscordInteractionApplicationCommandCallbackData? Data { get; set; } -} - -internal class RestFollowupMessageCreatePayload -{ - [JsonProperty("content", NullValueHandling = NullValueHandling.Ignore)] - public string? Content { get; set; } - - [JsonProperty("tts", NullValueHandling = NullValueHandling.Ignore)] - public bool? IsTTS { get; set; } - - [JsonProperty("embeds", NullValueHandling = NullValueHandling.Ignore)] - public IEnumerable? Embeds { get; set; } - - [JsonProperty("allowed_mentions", NullValueHandling = NullValueHandling.Ignore)] - public DiscordMentions? Mentions { get; set; } - - [JsonProperty("flags", NullValueHandling = NullValueHandling.Ignore)] - public DiscordMessageFlags Flags { get; set; } - - [JsonProperty("components", NullValueHandling = NullValueHandling.Ignore)] - public IReadOnlyList Components { get; set; } -} - -internal class RestEditApplicationCommandPermissionsPayload -{ - [JsonProperty("permissions")] - public IEnumerable Permissions { get; set; } -} +using System.Collections.Generic; +using DSharpPlus.Entities; +using DSharpPlus.Net.Serialization; +using Newtonsoft.Json; + +namespace DSharpPlus.Net.Abstractions; + +internal class RestApplicationCommandCreatePayload +{ + [JsonProperty("type")] + public DiscordApplicationCommandType Type { get; set; } + + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] + public string Description { get; set; } + + [JsonProperty("options", NullValueHandling = NullValueHandling.Ignore)] + public IEnumerable Options { get; set; } + + [JsonProperty("default_permission", NullValueHandling = NullValueHandling.Ignore)] + public bool? DefaultPermission { get; set; } + + [JsonProperty("name_localizations")] + public IReadOnlyDictionary NameLocalizations { get; set; } + + [JsonProperty("description_localizations")] + public IReadOnlyDictionary DescriptionLocalizations { get; set; } + + [JsonProperty("dm_permission", NullValueHandling = NullValueHandling.Ignore)] + public bool? AllowDMUsage { get; set; } + + [JsonProperty("default_member_permissions", NullValueHandling = NullValueHandling.Ignore)] + [JsonConverter(typeof(DiscordPermissionsAsStringJsonConverter))] + public DiscordPermissions? DefaultMemberPermissions { get; set; } + + [JsonProperty("nsfw", NullValueHandling = NullValueHandling.Ignore)] + public bool? NSFW { get; set; } + + /// + /// Interaction context(s) where the command can be used. + /// + [JsonProperty("contexts", NullValueHandling = NullValueHandling.Ignore)] + public IEnumerable? AllowedContexts { get; set; } + + /// + /// Installation context(s) where the command is available. + /// + [JsonProperty("integration_types", NullValueHandling = NullValueHandling.Ignore)] + public IEnumerable? InstallTypes { get; set; } +} + +internal class RestApplicationCommandEditPayload +{ + [JsonProperty("name")] + public Optional Name { get; set; } + + [JsonProperty("description")] + public Optional Description { get; set; } + + [JsonProperty("options")] + public Optional> Options { get; set; } + + [JsonProperty("default_permission", NullValueHandling = NullValueHandling.Ignore)] + public Optional DefaultPermission { get; set; } + + [JsonProperty("name_localizations")] + public IReadOnlyDictionary? NameLocalizations { get; set; } + + [JsonProperty("description_localizations")] + public IReadOnlyDictionary? DescriptionLocalizations { get; set; } + + [JsonProperty("dm_permission", NullValueHandling = NullValueHandling.Ignore)] + public Optional AllowDMUsage { get; set; } + + [JsonProperty("default_member_permissions", NullValueHandling = NullValueHandling.Ignore)] + public Optional DefaultMemberPermissions { get; set; } + + [JsonProperty("nsfw", NullValueHandling = NullValueHandling.Ignore)] + public Optional NSFW { get; set; } + + /// + /// Interaction context(s) where the command can be used. + /// + [JsonProperty("contexts", NullValueHandling = NullValueHandling.Ignore)] + public Optional> AllowedContexts { get; set; } + + /// + /// Installation context(s) where the command is available. + /// + [JsonProperty("integration_types", NullValueHandling = NullValueHandling.Ignore)] + public Optional> InstallTypes { get; set; } +} + +internal class DiscordInteractionResponsePayload +{ + [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] + public DiscordInteractionResponseType Type { get; set; } + + [JsonProperty("data", NullValueHandling = NullValueHandling.Ignore)] + public DiscordInteractionApplicationCommandCallbackData? Data { get; set; } +} + +internal class RestFollowupMessageCreatePayload +{ + [JsonProperty("content", NullValueHandling = NullValueHandling.Ignore)] + public string? Content { get; set; } + + [JsonProperty("tts", NullValueHandling = NullValueHandling.Ignore)] + public bool? IsTTS { get; set; } + + [JsonProperty("embeds", NullValueHandling = NullValueHandling.Ignore)] + public IEnumerable? Embeds { get; set; } + + [JsonProperty("allowed_mentions", NullValueHandling = NullValueHandling.Ignore)] + public DiscordMentions? Mentions { get; set; } + + [JsonProperty("flags", NullValueHandling = NullValueHandling.Ignore)] + public DiscordMessageFlags Flags { get; set; } + + [JsonProperty("components", NullValueHandling = NullValueHandling.Ignore)] + public IReadOnlyList Components { get; set; } +} + +internal class RestEditApplicationCommandPermissionsPayload +{ + [JsonProperty("permissions")] + public IEnumerable Permissions { get; set; } +} diff --git a/DSharpPlus/Net/Abstractions/Rest/RestChannelPayloads.cs b/DSharpPlus/Net/Abstractions/Rest/RestChannelPayloads.cs index b45125a648..4984ab5c66 100644 --- a/DSharpPlus/Net/Abstractions/Rest/RestChannelPayloads.cs +++ b/DSharpPlus/Net/Abstractions/Rest/RestChannelPayloads.cs @@ -1,331 +1,331 @@ -using System; -using System.Collections.Generic; -using DSharpPlus.Entities; -using Newtonsoft.Json; - -namespace DSharpPlus.Net.Abstractions; - -internal sealed class RestChannelCreatePayload -{ - [JsonProperty("name")] - public string Name { get; set; } - - [JsonProperty("type")] - public DiscordChannelType Type { get; set; } - - [JsonProperty("parent_id", NullValueHandling = NullValueHandling.Ignore)] - public ulong? Parent { get; set; } - - [JsonProperty("topic")] - public Optional Topic { get; set; } - - [JsonProperty("bitrate", NullValueHandling = NullValueHandling.Ignore)] - public int? Bitrate { get; set; } - - [JsonProperty("user_limit", NullValueHandling = NullValueHandling.Ignore)] - public int? UserLimit { get; set; } - - [JsonProperty("permission_overwrites", NullValueHandling = NullValueHandling.Ignore)] - public IEnumerable? PermissionOverwrites { get; set; } - - [JsonProperty("nsfw", NullValueHandling = NullValueHandling.Ignore)] - public bool? Nsfw { get; set; } - - [JsonProperty("rate_limit_per_user")] - public Optional PerUserRateLimit { get; set; } - - [JsonProperty("video_quality_mode", NullValueHandling = NullValueHandling.Ignore)] - public DiscordVideoQualityMode? QualityMode { get; set; } - - [JsonProperty("position", NullValueHandling = NullValueHandling.Ignore)] - public int? Position { get; set; } - - [JsonProperty("default_auth_archive_duration", NullValueHandling = NullValueHandling.Ignore)] - public DiscordAutoArchiveDuration? DefaultAutoArchiveDuration { get; set; } - - [JsonProperty("default_reaction_emoji", NullValueHandling = NullValueHandling.Ignore)] - public DefaultReaction? DefaultReaction { get; set; } - - [JsonProperty("available_tags", NullValueHandling = NullValueHandling.Ignore)] - public IEnumerable? AvailableTags { get; set; } - - [JsonProperty("default_sort_order", NullValueHandling = NullValueHandling.Ignore)] - public DiscordDefaultSortOrder? DefaultSortOrder { get; set; } -} - -internal sealed class RestChannelModifyPayload -{ - [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] - public required string Name { get; set; } - - [JsonProperty("type")] - public Optional Type { get; set; } - - [JsonProperty("position", NullValueHandling = NullValueHandling.Ignore)] - public int? Position { get; set; } - - [JsonProperty("topic")] - public Optional Topic { get; set; } - - [JsonProperty("nsfw", NullValueHandling = NullValueHandling.Ignore)] - public bool? Nsfw { get; set; } - - [JsonProperty("parent_id")] - public Optional Parent { get; set; } - - [JsonProperty("bitrate", NullValueHandling = NullValueHandling.Ignore)] - public int? Bitrate { get; set; } - - [JsonProperty("user_limit", NullValueHandling = NullValueHandling.Ignore)] - public int? UserLimit { get; set; } - - [JsonProperty("permission_overwrites", NullValueHandling = NullValueHandling.Ignore)] - public IEnumerable? PermissionOverwrites { get; set; } - - [JsonProperty("rate_limit_per_user")] - public Optional PerUserRateLimit { get; set; } - - [JsonProperty("rtc_region")] - public Optional RtcRegion { get; set; } - - [JsonProperty("video_quality_mode", NullValueHandling = NullValueHandling.Ignore)] - public DiscordVideoQualityMode? QualityMode { get; set; } - - [JsonProperty("default_auto_archive_duration", NullValueHandling = NullValueHandling.Ignore)] - public Optional DefaultAutoArchiveDuration { get; set; } - - [JsonProperty("default_sort_order", NullValueHandling = NullValueHandling.Ignore)] - public Optional DefaultSortOrder { get; set; } - - [JsonProperty("flags", NullValueHandling = NullValueHandling.Ignore)] - public Optional Flags { get; set; } - - [JsonProperty("default_reaction_emoji", NullValueHandling = NullValueHandling.Ignore)] - public Optional DefaultReaction { get; set; } - - [JsonProperty("default_per_user_rate_limit", NullValueHandling = NullValueHandling.Ignore)] - public Optional DefaultPerUserRateLimit { get; set; } - - [JsonProperty("available_tags", NullValueHandling = NullValueHandling.Ignore)] - public IEnumerable? AvailableTags { get; set; } - - [JsonProperty("default_forum_layout", NullValueHandling = NullValueHandling.Ignore)] - public Optional DefaultForumLayout { get; set; } -} - -internal sealed class RestThreadChannelModifyPayload -{ - [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] - public required string Name { get; set; } - - [JsonProperty("type")] - public Optional Type { get; set; } - - [JsonProperty("position", NullValueHandling = NullValueHandling.Ignore)] - public int? Position { get; set; } - - [JsonProperty("topic")] - public Optional Topic { get; set; } - - [JsonProperty("nsfw", NullValueHandling = NullValueHandling.Ignore)] - public bool? Nsfw { get; set; } - - [JsonProperty("parent_id")] - public Optional Parent { get; set; } - - [JsonProperty("bitrate", NullValueHandling = NullValueHandling.Ignore)] - public int? Bitrate { get; set; } - - [JsonProperty("user_limit", NullValueHandling = NullValueHandling.Ignore)] - public int? UserLimit { get; set; } - - [JsonProperty("permission_overwrites", NullValueHandling = NullValueHandling.Ignore)] - public IEnumerable? PermissionOverwrites { get; set; } - - [JsonProperty("rate_limit_per_user")] - public Optional PerUserRateLimit { get; set; } - - [JsonProperty("rtc_region")] - public Optional RtcRegion { get; set; } - - [JsonProperty("video_quality_mode", NullValueHandling = NullValueHandling.Ignore)] - public DiscordVideoQualityMode? QualityMode { get; set; } - - [JsonProperty("archived", NullValueHandling = NullValueHandling.Ignore)] - public bool? IsArchived { get; set; } - - [JsonProperty("auto_archive_duration", NullValueHandling = NullValueHandling.Ignore)] - public DiscordAutoArchiveDuration? ArchiveDuration { get; set; } - - [JsonProperty("locked", NullValueHandling = NullValueHandling.Ignore)] - public bool? Locked { get; set; } - - [JsonProperty("invitable", NullValueHandling = NullValueHandling.Ignore)] - public bool? IsInvitable { get; set; } - - [JsonProperty("applied_tags", NullValueHandling = NullValueHandling.Ignore)] - public IEnumerable? AppliedTags { get; set; } -} - -internal class RestChannelMessageEditPayload -{ - [JsonProperty("content", NullValueHandling = NullValueHandling.Include)] - public string? Content { get; set; } - - [JsonIgnore] - public bool HasContent { get; set; } - - [JsonProperty("embeds", NullValueHandling = NullValueHandling.Ignore)] - public IEnumerable? Embeds { get; set; } - - [JsonProperty("allowed_mentions", NullValueHandling = NullValueHandling.Ignore)] - public DiscordMentions? Mentions { get; set; } - - [JsonProperty("components", NullValueHandling = NullValueHandling.Ignore)] - public IReadOnlyList? Components { get; set; } - - [JsonProperty("flags", NullValueHandling = NullValueHandling.Ignore)] - public DiscordMessageFlags? Flags { get; set; } - - [JsonProperty("attachments", NullValueHandling = NullValueHandling.Ignore)] - public IEnumerable? Attachments { get; set; } - - [JsonIgnore] - public bool HasEmbed { get; set; } - - public bool ShouldSerializeContent() - => this.HasContent; - - public bool ShouldSerializeEmbed() - => this.HasEmbed; -} - -internal sealed class RestChannelMessageCreatePayload : RestChannelMessageEditPayload -{ - [JsonProperty("tts", NullValueHandling = NullValueHandling.Ignore)] - public bool? IsTTS { get; set; } - - [JsonProperty("sticker_ids", NullValueHandling = NullValueHandling.Ignore)] - public IEnumerable? StickersIds { get; set; } // Discord sends an array, but you can only have one* sticker on a message // - - [JsonProperty("message_reference", NullValueHandling = NullValueHandling.Ignore)] - public InternalDiscordMessageReference? MessageReference { get; set; } - - [JsonProperty("poll", NullValueHandling = NullValueHandling.Ignore)] - public PollCreatePayload? Poll { get; set; } -} - -internal sealed class RestChannelMessageCreateMultipartPayload -{ - [JsonProperty("content", NullValueHandling = NullValueHandling.Ignore)] - public string Content { get; set; } - - [JsonProperty("tts", NullValueHandling = NullValueHandling.Ignore)] - public bool? IsTTS { get; set; } - - [JsonProperty("embeds", NullValueHandling = NullValueHandling.Ignore)] - public IEnumerable Embeds { get; set; } - - [JsonProperty("allowed_mentions", NullValueHandling = NullValueHandling.Ignore)] - public DiscordMentions Mentions { get; set; } - - [JsonProperty("message_reference", NullValueHandling = NullValueHandling.Ignore)] - public InternalDiscordMessageReference? MessageReference { get; set; } - - [JsonProperty("poll", NullValueHandling = NullValueHandling.Ignore)] - public PollCreatePayload? Poll { get; set; } -} - -internal sealed class RestChannelMessageBulkDeletePayload -{ - [JsonProperty("messages", NullValueHandling = NullValueHandling.Ignore)] - public IEnumerable Messages { get; set; } -} - -internal sealed class RestChannelMessageSuppressEmbedsPayload -{ - [JsonProperty("suppress", NullValueHandling = NullValueHandling.Ignore)] - public bool? Suppress { get; set; } -} - -internal sealed class RestChannelInviteCreatePayload -{ - [JsonProperty("max_age", NullValueHandling = NullValueHandling.Ignore)] - public int MaxAge { get; set; } - - [JsonProperty("max_uses", NullValueHandling = NullValueHandling.Ignore)] - public int MaxUses { get; set; } - - [JsonProperty("temporary", NullValueHandling = NullValueHandling.Ignore)] - public bool Temporary { get; set; } - - [JsonProperty("unique", NullValueHandling = NullValueHandling.Ignore)] - public bool Unique { get; set; } - - [JsonProperty("target_type", NullValueHandling = NullValueHandling.Ignore)] - public DiscordInviteTargetType? TargetType { get; set; } - - [JsonProperty("target_user_id", NullValueHandling = NullValueHandling.Ignore)] - public ulong? TargetUserId { get; set; } - - [JsonProperty("target_application_id", NullValueHandling = NullValueHandling.Ignore)] - public ulong? TargetApplicationId { get; set; } -} - -internal sealed class RestChannelPermissionEditPayload -{ - [JsonProperty("allow", NullValueHandling = NullValueHandling.Ignore)] - public DiscordPermissions Allow { get; set; } - - [JsonProperty("deny", NullValueHandling = NullValueHandling.Ignore)] - public DiscordPermissions Deny { get; set; } - - [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] - public int Type { get; set; } -} - -internal sealed class RestChannelGroupDmRecipientAddPayload : IOAuth2Payload -{ - [JsonProperty("access_token")] - public string AccessToken { get; set; } - - [JsonProperty("nick", NullValueHandling = NullValueHandling.Ignore)] - public string Nickname { get; set; } -} - -internal sealed class AcknowledgePayload -{ - [JsonProperty("token", NullValueHandling = NullValueHandling.Include)] - public string Token { get; set; } -} - -internal sealed class RestCreateStageInstancePayload -{ - [JsonProperty("channel_id")] - public ulong ChannelId { get; set; } - - [JsonProperty("topic")] - public string Topic { get; set; } - - [JsonProperty("privacy_level", NullValueHandling = NullValueHandling.Ignore)] - public DiscordStagePrivacyLevel? PrivacyLevel { get; set; } -} - -internal sealed class RestModifyStageInstancePayload -{ - [JsonProperty("topic")] - public Optional Topic { get; set; } - - [JsonProperty("privacy_level")] - public Optional PrivacyLevel { get; set; } -} - -internal sealed class RestBecomeStageSpeakerInstancePayload -{ - [JsonProperty("channel_id")] - public ulong ChannelId { get; set; } - [JsonProperty("request_to_speak_timestamp", NullValueHandling = NullValueHandling.Ignore)] - public DateTime? RequestToSpeakTimestamp { get; set; } - [JsonProperty("suppress", NullValueHandling = NullValueHandling.Ignore)] - public bool? Suppress { get; set; } -} +using System; +using System.Collections.Generic; +using DSharpPlus.Entities; +using Newtonsoft.Json; + +namespace DSharpPlus.Net.Abstractions; + +internal sealed class RestChannelCreatePayload +{ + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("type")] + public DiscordChannelType Type { get; set; } + + [JsonProperty("parent_id", NullValueHandling = NullValueHandling.Ignore)] + public ulong? Parent { get; set; } + + [JsonProperty("topic")] + public Optional Topic { get; set; } + + [JsonProperty("bitrate", NullValueHandling = NullValueHandling.Ignore)] + public int? Bitrate { get; set; } + + [JsonProperty("user_limit", NullValueHandling = NullValueHandling.Ignore)] + public int? UserLimit { get; set; } + + [JsonProperty("permission_overwrites", NullValueHandling = NullValueHandling.Ignore)] + public IEnumerable? PermissionOverwrites { get; set; } + + [JsonProperty("nsfw", NullValueHandling = NullValueHandling.Ignore)] + public bool? Nsfw { get; set; } + + [JsonProperty("rate_limit_per_user")] + public Optional PerUserRateLimit { get; set; } + + [JsonProperty("video_quality_mode", NullValueHandling = NullValueHandling.Ignore)] + public DiscordVideoQualityMode? QualityMode { get; set; } + + [JsonProperty("position", NullValueHandling = NullValueHandling.Ignore)] + public int? Position { get; set; } + + [JsonProperty("default_auth_archive_duration", NullValueHandling = NullValueHandling.Ignore)] + public DiscordAutoArchiveDuration? DefaultAutoArchiveDuration { get; set; } + + [JsonProperty("default_reaction_emoji", NullValueHandling = NullValueHandling.Ignore)] + public DefaultReaction? DefaultReaction { get; set; } + + [JsonProperty("available_tags", NullValueHandling = NullValueHandling.Ignore)] + public IEnumerable? AvailableTags { get; set; } + + [JsonProperty("default_sort_order", NullValueHandling = NullValueHandling.Ignore)] + public DiscordDefaultSortOrder? DefaultSortOrder { get; set; } +} + +internal sealed class RestChannelModifyPayload +{ + [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] + public required string Name { get; set; } + + [JsonProperty("type")] + public Optional Type { get; set; } + + [JsonProperty("position", NullValueHandling = NullValueHandling.Ignore)] + public int? Position { get; set; } + + [JsonProperty("topic")] + public Optional Topic { get; set; } + + [JsonProperty("nsfw", NullValueHandling = NullValueHandling.Ignore)] + public bool? Nsfw { get; set; } + + [JsonProperty("parent_id")] + public Optional Parent { get; set; } + + [JsonProperty("bitrate", NullValueHandling = NullValueHandling.Ignore)] + public int? Bitrate { get; set; } + + [JsonProperty("user_limit", NullValueHandling = NullValueHandling.Ignore)] + public int? UserLimit { get; set; } + + [JsonProperty("permission_overwrites", NullValueHandling = NullValueHandling.Ignore)] + public IEnumerable? PermissionOverwrites { get; set; } + + [JsonProperty("rate_limit_per_user")] + public Optional PerUserRateLimit { get; set; } + + [JsonProperty("rtc_region")] + public Optional RtcRegion { get; set; } + + [JsonProperty("video_quality_mode", NullValueHandling = NullValueHandling.Ignore)] + public DiscordVideoQualityMode? QualityMode { get; set; } + + [JsonProperty("default_auto_archive_duration", NullValueHandling = NullValueHandling.Ignore)] + public Optional DefaultAutoArchiveDuration { get; set; } + + [JsonProperty("default_sort_order", NullValueHandling = NullValueHandling.Ignore)] + public Optional DefaultSortOrder { get; set; } + + [JsonProperty("flags", NullValueHandling = NullValueHandling.Ignore)] + public Optional Flags { get; set; } + + [JsonProperty("default_reaction_emoji", NullValueHandling = NullValueHandling.Ignore)] + public Optional DefaultReaction { get; set; } + + [JsonProperty("default_per_user_rate_limit", NullValueHandling = NullValueHandling.Ignore)] + public Optional DefaultPerUserRateLimit { get; set; } + + [JsonProperty("available_tags", NullValueHandling = NullValueHandling.Ignore)] + public IEnumerable? AvailableTags { get; set; } + + [JsonProperty("default_forum_layout", NullValueHandling = NullValueHandling.Ignore)] + public Optional DefaultForumLayout { get; set; } +} + +internal sealed class RestThreadChannelModifyPayload +{ + [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] + public required string Name { get; set; } + + [JsonProperty("type")] + public Optional Type { get; set; } + + [JsonProperty("position", NullValueHandling = NullValueHandling.Ignore)] + public int? Position { get; set; } + + [JsonProperty("topic")] + public Optional Topic { get; set; } + + [JsonProperty("nsfw", NullValueHandling = NullValueHandling.Ignore)] + public bool? Nsfw { get; set; } + + [JsonProperty("parent_id")] + public Optional Parent { get; set; } + + [JsonProperty("bitrate", NullValueHandling = NullValueHandling.Ignore)] + public int? Bitrate { get; set; } + + [JsonProperty("user_limit", NullValueHandling = NullValueHandling.Ignore)] + public int? UserLimit { get; set; } + + [JsonProperty("permission_overwrites", NullValueHandling = NullValueHandling.Ignore)] + public IEnumerable? PermissionOverwrites { get; set; } + + [JsonProperty("rate_limit_per_user")] + public Optional PerUserRateLimit { get; set; } + + [JsonProperty("rtc_region")] + public Optional RtcRegion { get; set; } + + [JsonProperty("video_quality_mode", NullValueHandling = NullValueHandling.Ignore)] + public DiscordVideoQualityMode? QualityMode { get; set; } + + [JsonProperty("archived", NullValueHandling = NullValueHandling.Ignore)] + public bool? IsArchived { get; set; } + + [JsonProperty("auto_archive_duration", NullValueHandling = NullValueHandling.Ignore)] + public DiscordAutoArchiveDuration? ArchiveDuration { get; set; } + + [JsonProperty("locked", NullValueHandling = NullValueHandling.Ignore)] + public bool? Locked { get; set; } + + [JsonProperty("invitable", NullValueHandling = NullValueHandling.Ignore)] + public bool? IsInvitable { get; set; } + + [JsonProperty("applied_tags", NullValueHandling = NullValueHandling.Ignore)] + public IEnumerable? AppliedTags { get; set; } +} + +internal class RestChannelMessageEditPayload +{ + [JsonProperty("content", NullValueHandling = NullValueHandling.Include)] + public string? Content { get; set; } + + [JsonIgnore] + public bool HasContent { get; set; } + + [JsonProperty("embeds", NullValueHandling = NullValueHandling.Ignore)] + public IEnumerable? Embeds { get; set; } + + [JsonProperty("allowed_mentions", NullValueHandling = NullValueHandling.Ignore)] + public DiscordMentions? Mentions { get; set; } + + [JsonProperty("components", NullValueHandling = NullValueHandling.Ignore)] + public IReadOnlyList? Components { get; set; } + + [JsonProperty("flags", NullValueHandling = NullValueHandling.Ignore)] + public DiscordMessageFlags? Flags { get; set; } + + [JsonProperty("attachments", NullValueHandling = NullValueHandling.Ignore)] + public IEnumerable? Attachments { get; set; } + + [JsonIgnore] + public bool HasEmbed { get; set; } + + public bool ShouldSerializeContent() + => this.HasContent; + + public bool ShouldSerializeEmbed() + => this.HasEmbed; +} + +internal sealed class RestChannelMessageCreatePayload : RestChannelMessageEditPayload +{ + [JsonProperty("tts", NullValueHandling = NullValueHandling.Ignore)] + public bool? IsTTS { get; set; } + + [JsonProperty("sticker_ids", NullValueHandling = NullValueHandling.Ignore)] + public IEnumerable? StickersIds { get; set; } // Discord sends an array, but you can only have one* sticker on a message // + + [JsonProperty("message_reference", NullValueHandling = NullValueHandling.Ignore)] + public InternalDiscordMessageReference? MessageReference { get; set; } + + [JsonProperty("poll", NullValueHandling = NullValueHandling.Ignore)] + public PollCreatePayload? Poll { get; set; } +} + +internal sealed class RestChannelMessageCreateMultipartPayload +{ + [JsonProperty("content", NullValueHandling = NullValueHandling.Ignore)] + public string Content { get; set; } + + [JsonProperty("tts", NullValueHandling = NullValueHandling.Ignore)] + public bool? IsTTS { get; set; } + + [JsonProperty("embeds", NullValueHandling = NullValueHandling.Ignore)] + public IEnumerable Embeds { get; set; } + + [JsonProperty("allowed_mentions", NullValueHandling = NullValueHandling.Ignore)] + public DiscordMentions Mentions { get; set; } + + [JsonProperty("message_reference", NullValueHandling = NullValueHandling.Ignore)] + public InternalDiscordMessageReference? MessageReference { get; set; } + + [JsonProperty("poll", NullValueHandling = NullValueHandling.Ignore)] + public PollCreatePayload? Poll { get; set; } +} + +internal sealed class RestChannelMessageBulkDeletePayload +{ + [JsonProperty("messages", NullValueHandling = NullValueHandling.Ignore)] + public IEnumerable Messages { get; set; } +} + +internal sealed class RestChannelMessageSuppressEmbedsPayload +{ + [JsonProperty("suppress", NullValueHandling = NullValueHandling.Ignore)] + public bool? Suppress { get; set; } +} + +internal sealed class RestChannelInviteCreatePayload +{ + [JsonProperty("max_age", NullValueHandling = NullValueHandling.Ignore)] + public int MaxAge { get; set; } + + [JsonProperty("max_uses", NullValueHandling = NullValueHandling.Ignore)] + public int MaxUses { get; set; } + + [JsonProperty("temporary", NullValueHandling = NullValueHandling.Ignore)] + public bool Temporary { get; set; } + + [JsonProperty("unique", NullValueHandling = NullValueHandling.Ignore)] + public bool Unique { get; set; } + + [JsonProperty("target_type", NullValueHandling = NullValueHandling.Ignore)] + public DiscordInviteTargetType? TargetType { get; set; } + + [JsonProperty("target_user_id", NullValueHandling = NullValueHandling.Ignore)] + public ulong? TargetUserId { get; set; } + + [JsonProperty("target_application_id", NullValueHandling = NullValueHandling.Ignore)] + public ulong? TargetApplicationId { get; set; } +} + +internal sealed class RestChannelPermissionEditPayload +{ + [JsonProperty("allow", NullValueHandling = NullValueHandling.Ignore)] + public DiscordPermissions Allow { get; set; } + + [JsonProperty("deny", NullValueHandling = NullValueHandling.Ignore)] + public DiscordPermissions Deny { get; set; } + + [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] + public int Type { get; set; } +} + +internal sealed class RestChannelGroupDmRecipientAddPayload : IOAuth2Payload +{ + [JsonProperty("access_token")] + public string AccessToken { get; set; } + + [JsonProperty("nick", NullValueHandling = NullValueHandling.Ignore)] + public string Nickname { get; set; } +} + +internal sealed class AcknowledgePayload +{ + [JsonProperty("token", NullValueHandling = NullValueHandling.Include)] + public string Token { get; set; } +} + +internal sealed class RestCreateStageInstancePayload +{ + [JsonProperty("channel_id")] + public ulong ChannelId { get; set; } + + [JsonProperty("topic")] + public string Topic { get; set; } + + [JsonProperty("privacy_level", NullValueHandling = NullValueHandling.Ignore)] + public DiscordStagePrivacyLevel? PrivacyLevel { get; set; } +} + +internal sealed class RestModifyStageInstancePayload +{ + [JsonProperty("topic")] + public Optional Topic { get; set; } + + [JsonProperty("privacy_level")] + public Optional PrivacyLevel { get; set; } +} + +internal sealed class RestBecomeStageSpeakerInstancePayload +{ + [JsonProperty("channel_id")] + public ulong ChannelId { get; set; } + [JsonProperty("request_to_speak_timestamp", NullValueHandling = NullValueHandling.Ignore)] + public DateTime? RequestToSpeakTimestamp { get; set; } + [JsonProperty("suppress", NullValueHandling = NullValueHandling.Ignore)] + public bool? Suppress { get; set; } +} diff --git a/DSharpPlus/Net/Abstractions/Rest/RestGuildPayloads.cs b/DSharpPlus/Net/Abstractions/Rest/RestGuildPayloads.cs index 83fa56970f..d4662c2051 100644 --- a/DSharpPlus/Net/Abstractions/Rest/RestGuildPayloads.cs +++ b/DSharpPlus/Net/Abstractions/Rest/RestGuildPayloads.cs @@ -1,394 +1,394 @@ -using System; -using System.Collections.Generic; -using DSharpPlus.Entities; -using Newtonsoft.Json; - -namespace DSharpPlus.Net.Abstractions; - -internal interface IReasonAction -{ - public string Reason { get; set; } - - //[JsonProperty("reason", NullValueHandling = NullValueHandling.Ignore)] - //public string Reason { get; set; } -} - -internal class RestGuildCreatePayload -{ - [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] - public string Name { get; set; } - - [JsonProperty("region", NullValueHandling = NullValueHandling.Ignore)] - public string RegionId { get; set; } - - [JsonProperty("icon", NullValueHandling = NullValueHandling.Include)] - public Optional IconBase64 { get; set; } - - [JsonProperty("verification_level", NullValueHandling = NullValueHandling.Ignore)] - public DiscordVerificationLevel? VerificationLevel { get; set; } - - [JsonProperty("default_message_notifications", NullValueHandling = NullValueHandling.Ignore)] - public DiscordDefaultMessageNotifications? DefaultMessageNotifications { get; set; } - - [JsonProperty("roles", NullValueHandling = NullValueHandling.Ignore)] - public IEnumerable Roles { get; set; } - - [JsonProperty("channels", NullValueHandling = NullValueHandling.Ignore)] - public IEnumerable Channels { get; set; } - - [JsonProperty("system_channel_flags", NullValueHandling = NullValueHandling.Ignore)] - public DiscordSystemChannelFlags? SystemChannelFlags { get; set; } -} - -internal sealed class RestGuildCreateFromTemplatePayload -{ - [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] - public string Name { get; set; } - - [JsonProperty("icon", NullValueHandling = NullValueHandling.Include)] - public Optional IconBase64 { get; set; } -} - -internal sealed class RestGuildModifyPayload -{ - [JsonProperty("name")] - public Optional Name { get; set; } - - [JsonProperty("region")] - public Optional RegionId { get; set; } - - [JsonProperty("icon")] - public Optional IconBase64 { get; set; } - - [JsonProperty("verification_level")] - public Optional VerificationLevel { get; set; } - - [JsonProperty("default_message_notifications")] - public Optional DefaultMessageNotifications { get; set; } - - [JsonProperty("owner_id")] - public Optional OwnerId { get; set; } - - [JsonProperty("splash")] - public Optional SplashBase64 { get; set; } - - [JsonProperty("afk_channel_id")] - public Optional AfkChannelId { get; set; } - - [JsonProperty("afk_timeout")] - public Optional AfkTimeout { get; set; } - - [JsonProperty("mfa_level")] - public Optional MfaLevel { get; set; } - - [JsonProperty("explicit_content_filter")] - public Optional ExplicitContentFilter { get; set; } - - [JsonProperty("system_channel_id", NullValueHandling = NullValueHandling.Include)] - public Optional SystemChannelId { get; set; } - - [JsonProperty("banner")] - public Optional Banner { get; set; } - - [JsonProperty("discorvery_splash")] - public Optional DiscoverySplash { get; set; } - - [JsonProperty("system_channel_flags")] - public Optional SystemChannelFlags { get; set; } - - [JsonProperty("rules_channel_id")] - public Optional RulesChannelId { get; set; } - - [JsonProperty("public_updates_channel_id")] - public Optional PublicUpdatesChannelId { get; set; } - - [JsonProperty("preferred_locale")] - public Optional PreferredLocale { get; set; } - - [JsonProperty("description")] - public Optional Description { get; set; } - - [JsonProperty("features")] - public Optional> Features { get; set; } -} - -internal sealed class RestGuildMemberAddPayload : IOAuth2Payload -{ - [JsonProperty("access_token")] - public string AccessToken { get; set; } - - [JsonProperty("nick", NullValueHandling = NullValueHandling.Ignore)] - public string Nickname { get; set; } - - [JsonProperty("roles", NullValueHandling = NullValueHandling.Ignore)] - public IEnumerable Roles { get; set; } - - [JsonProperty("mute", NullValueHandling = NullValueHandling.Ignore)] - public bool? Mute { get; set; } - - [JsonProperty("deaf", NullValueHandling = NullValueHandling.Ignore)] - public bool? Deaf { get; set; } -} - -internal sealed class RestScheduledGuildEventCreatePayload -{ - [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] - public string Name { get; set; } - - [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] - public string Description { get; set; } - - [JsonProperty("channel_id", NullValueHandling = NullValueHandling.Ignore)] - public ulong? ChannelId { get; set; } - - [JsonProperty("privacy_level", NullValueHandling = NullValueHandling.Ignore)] - public DiscordScheduledGuildEventPrivacyLevel PrivacyLevel { get; set; } - - [JsonProperty("entity_type", NullValueHandling = NullValueHandling.Ignore)] - public DiscordScheduledGuildEventType Type { get; set; } - - [JsonProperty("scheduled_start_time", NullValueHandling = NullValueHandling.Ignore)] - public DateTimeOffset StartTime { get; set; } - - [JsonProperty("scheduled_end_time", NullValueHandling = NullValueHandling.Ignore)]// Null = no end date - public DateTimeOffset? EndTime { get; set; } - - [JsonProperty("entity_metadata", NullValueHandling = NullValueHandling.Ignore)] - public DiscordScheduledGuildEventMetadata? Metadata { get; set; } - - [JsonProperty("image", NullValueHandling = NullValueHandling.Ignore)] - public Optional CoverImage { get; set; } -} - -internal sealed class RestScheduledGuildEventModifyPayload -{ - [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] - public Optional Name { get; set; } - - [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] - public Optional Description { get; set; } - - [JsonProperty("channel_id", NullValueHandling = NullValueHandling.Ignore)] - public Optional ChannelId { get; set; } - - [JsonProperty("privacy_level", NullValueHandling = NullValueHandling.Ignore)] - public Optional PrivacyLevel { get; set; } - - [JsonProperty("entity_type", NullValueHandling = NullValueHandling.Ignore)] - public Optional Type { get; set; } - - [JsonProperty("scheduled_start_time", NullValueHandling = NullValueHandling.Ignore)] - public Optional StartTime { get; set; } - - [JsonProperty("scheduled_end_time", NullValueHandling = NullValueHandling.Ignore)] - public Optional EndTime { get; set; } - - [JsonProperty("entity_metadata", NullValueHandling = NullValueHandling.Ignore)] - public Optional Metadata { get; set; } - - [JsonProperty("status", NullValueHandling = NullValueHandling.Ignore)] - public Optional Status { get; set; } - - [JsonProperty("image", NullValueHandling = NullValueHandling.Ignore)] - public Optional CoverImage { get; set; } -} - -internal sealed class RestGuildChannelReorderPayload -{ - [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)] - public ulong ChannelId { get; set; } - - [JsonProperty("position", NullValueHandling = NullValueHandling.Ignore)] - public int Position { get; set; } - - [JsonProperty("lock_permissions", NullValueHandling = NullValueHandling.Ignore)] - public bool? LockPermissions { get; set; } - - [JsonProperty("parent_id", NullValueHandling = NullValueHandling.Ignore)] - public ulong? ParentId { get; set; } -} - -internal sealed class RestGuildRoleReorderPayload -{ - [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)] - public ulong RoleId { get; set; } - - [JsonProperty("position", NullValueHandling = NullValueHandling.Ignore)] - public int Position { get; set; } -} - -internal sealed class RestGuildMemberModifyPayload -{ - [JsonProperty("nick")] - public Optional Nickname { get; set; } - - [JsonProperty("roles")] - public Optional> RoleIds { get; set; } - - [JsonProperty("mute")] - public Optional Mute { get; set; } - - [JsonProperty("deaf")] - public Optional Deafen { get; set; } - - [JsonProperty("channel_id")] - public Optional VoiceChannelId { get; set; } - - [JsonProperty("communication_disabled_until", NullValueHandling = NullValueHandling.Include)] - public Optional CommunicationDisabledUntil { get; set; } - - [JsonProperty("flags")] - public Optional MemberFlags { get; set; } -} - -internal sealed class RestGuildRolePayload -{ - [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] - public string? Name { get; set; } - - [JsonProperty("permissions", NullValueHandling = NullValueHandling.Ignore)] - public DiscordPermissions? Permissions { get; set; } - - [JsonProperty("color", NullValueHandling = NullValueHandling.Ignore)] - public int? Color { get; set; } - - [JsonProperty("hoist", NullValueHandling = NullValueHandling.Ignore)] - public bool? Hoist { get; set; } - - [JsonProperty("mentionable", NullValueHandling = NullValueHandling.Ignore)] - public bool? Mentionable { get; set; } - - [JsonProperty("unicode_emoji", NullValueHandling = NullValueHandling.Ignore)] - public string? Emoji { get; set; } - - [JsonProperty("icon", NullValueHandling = NullValueHandling.Ignore)] - public string? Icon { get; set; } -} - -internal sealed class RestGuildPruneResultPayload -{ - [JsonProperty("pruned", NullValueHandling = NullValueHandling.Ignore)] - public int? Pruned { get; set; } -} - -internal sealed class RestGuildIntegrationAttachPayload -{ - [JsonProperty("type")] - public string Type { get; set; } - - [JsonProperty("id")] - public ulong Id { get; set; } -} - -internal sealed class RestGuildIntegrationModifyPayload -{ - [JsonProperty("expire_behavior", NullValueHandling = NullValueHandling.Ignore)] - public int? ExpireBehavior { get; set; } - - [JsonProperty("expire_grace_period", NullValueHandling = NullValueHandling.Ignore)] - public int? ExpireGracePeriod { get; set; } - - [JsonProperty("enable_emoticons", NullValueHandling = NullValueHandling.Ignore)] - public bool? EnableEmoticons { get; set; } -} - -internal class RestGuildEmojiModifyPayload -{ - [JsonProperty("name")] - public string? Name { get; set; } - - [JsonProperty("roles", NullValueHandling = NullValueHandling.Ignore)] - public ulong[]? Roles { get; set; } -} - -internal class RestGuildEmojiCreatePayload : RestGuildEmojiModifyPayload -{ - [JsonProperty("image")] - public string? ImageB64 { get; set; } -} - -internal class RestApplicationEmojiCreatePayload : RestApplicationEmojiModifyPayload -{ - [JsonProperty("image")] - public string ImageB64 { get; set; } -} - -internal class RestApplicationEmojiModifyPayload -{ - [JsonProperty("name")] - public string Name { get; set; } -} - -internal class RestGuildWidgetSettingsPayload -{ - [JsonProperty("enabled", NullValueHandling = NullValueHandling.Ignore)] - public bool? Enabled { get; set; } - - [JsonProperty("channel_id", NullValueHandling = NullValueHandling.Ignore)] - public ulong? ChannelId { get; set; } -} - -// TODO: this is wrong. i've annotated them for now, but we'll need to use optionals here -// since optional/nullable mean two different things in the context of modifying. -internal class RestGuildTemplateCreateOrModifyPayload -{ - [JsonProperty("name", NullValueHandling = NullValueHandling.Include)] - public string? Name { get; set; } - - [JsonProperty("description", NullValueHandling = NullValueHandling.Include)] - public string? Description { get; set; } -} - -internal class RestGuildMembershipScreeningFormModifyPayload -{ - [JsonProperty("enabled", NullValueHandling = NullValueHandling.Ignore)] - public Optional Enabled { get; set; } - - [JsonProperty("form_fields", NullValueHandling = NullValueHandling.Ignore)] - public Optional Fields { get; set; } - - [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] - public Optional Description { get; set; } -} - -internal class RestGuildWelcomeScreenModifyPayload -{ - [JsonProperty("enabled", NullValueHandling = NullValueHandling.Ignore)] - public Optional Enabled { get; set; } - - [JsonProperty("welcome_channels", NullValueHandling = NullValueHandling.Ignore)] - public Optional> WelcomeChannels { get; set; } - - [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] - public Optional Description { get; set; } -} - -internal class RestGuildUpdateCurrentUserVoiceStatePayload -{ - [JsonProperty("channel_id")] - public ulong ChannelId { get; set; } - - [JsonProperty("suppress", NullValueHandling = NullValueHandling.Ignore)] - public bool? Suppress { get; set; } - - [JsonProperty("request_to_speak_timestamp", NullValueHandling = NullValueHandling.Ignore)] - public DateTimeOffset? RequestToSpeakTimestamp { get; set; } -} - -internal class RestGuildUpdateUserVoiceStatePayload -{ - [JsonProperty("channel_id")] - public ulong ChannelId { get; set; } - - [JsonProperty("suppress", NullValueHandling = NullValueHandling.Ignore)] - public bool? Suppress { get; set; } -} - -internal class RestGuildBulkBanPayload -{ - [JsonProperty("delete_message_seconds", NullValueHandling = NullValueHandling.Ignore)] - public int? DeleteMessageSeconds { get; set; } - - [JsonProperty("user_ids")] - public IEnumerable UserIds { get; set; } -} +using System; +using System.Collections.Generic; +using DSharpPlus.Entities; +using Newtonsoft.Json; + +namespace DSharpPlus.Net.Abstractions; + +internal interface IReasonAction +{ + public string Reason { get; set; } + + //[JsonProperty("reason", NullValueHandling = NullValueHandling.Ignore)] + //public string Reason { get; set; } +} + +internal class RestGuildCreatePayload +{ + [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] + public string Name { get; set; } + + [JsonProperty("region", NullValueHandling = NullValueHandling.Ignore)] + public string RegionId { get; set; } + + [JsonProperty("icon", NullValueHandling = NullValueHandling.Include)] + public Optional IconBase64 { get; set; } + + [JsonProperty("verification_level", NullValueHandling = NullValueHandling.Ignore)] + public DiscordVerificationLevel? VerificationLevel { get; set; } + + [JsonProperty("default_message_notifications", NullValueHandling = NullValueHandling.Ignore)] + public DiscordDefaultMessageNotifications? DefaultMessageNotifications { get; set; } + + [JsonProperty("roles", NullValueHandling = NullValueHandling.Ignore)] + public IEnumerable Roles { get; set; } + + [JsonProperty("channels", NullValueHandling = NullValueHandling.Ignore)] + public IEnumerable Channels { get; set; } + + [JsonProperty("system_channel_flags", NullValueHandling = NullValueHandling.Ignore)] + public DiscordSystemChannelFlags? SystemChannelFlags { get; set; } +} + +internal sealed class RestGuildCreateFromTemplatePayload +{ + [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] + public string Name { get; set; } + + [JsonProperty("icon", NullValueHandling = NullValueHandling.Include)] + public Optional IconBase64 { get; set; } +} + +internal sealed class RestGuildModifyPayload +{ + [JsonProperty("name")] + public Optional Name { get; set; } + + [JsonProperty("region")] + public Optional RegionId { get; set; } + + [JsonProperty("icon")] + public Optional IconBase64 { get; set; } + + [JsonProperty("verification_level")] + public Optional VerificationLevel { get; set; } + + [JsonProperty("default_message_notifications")] + public Optional DefaultMessageNotifications { get; set; } + + [JsonProperty("owner_id")] + public Optional OwnerId { get; set; } + + [JsonProperty("splash")] + public Optional SplashBase64 { get; set; } + + [JsonProperty("afk_channel_id")] + public Optional AfkChannelId { get; set; } + + [JsonProperty("afk_timeout")] + public Optional AfkTimeout { get; set; } + + [JsonProperty("mfa_level")] + public Optional MfaLevel { get; set; } + + [JsonProperty("explicit_content_filter")] + public Optional ExplicitContentFilter { get; set; } + + [JsonProperty("system_channel_id", NullValueHandling = NullValueHandling.Include)] + public Optional SystemChannelId { get; set; } + + [JsonProperty("banner")] + public Optional Banner { get; set; } + + [JsonProperty("discorvery_splash")] + public Optional DiscoverySplash { get; set; } + + [JsonProperty("system_channel_flags")] + public Optional SystemChannelFlags { get; set; } + + [JsonProperty("rules_channel_id")] + public Optional RulesChannelId { get; set; } + + [JsonProperty("public_updates_channel_id")] + public Optional PublicUpdatesChannelId { get; set; } + + [JsonProperty("preferred_locale")] + public Optional PreferredLocale { get; set; } + + [JsonProperty("description")] + public Optional Description { get; set; } + + [JsonProperty("features")] + public Optional> Features { get; set; } +} + +internal sealed class RestGuildMemberAddPayload : IOAuth2Payload +{ + [JsonProperty("access_token")] + public string AccessToken { get; set; } + + [JsonProperty("nick", NullValueHandling = NullValueHandling.Ignore)] + public string Nickname { get; set; } + + [JsonProperty("roles", NullValueHandling = NullValueHandling.Ignore)] + public IEnumerable Roles { get; set; } + + [JsonProperty("mute", NullValueHandling = NullValueHandling.Ignore)] + public bool? Mute { get; set; } + + [JsonProperty("deaf", NullValueHandling = NullValueHandling.Ignore)] + public bool? Deaf { get; set; } +} + +internal sealed class RestScheduledGuildEventCreatePayload +{ + [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] + public string Name { get; set; } + + [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] + public string Description { get; set; } + + [JsonProperty("channel_id", NullValueHandling = NullValueHandling.Ignore)] + public ulong? ChannelId { get; set; } + + [JsonProperty("privacy_level", NullValueHandling = NullValueHandling.Ignore)] + public DiscordScheduledGuildEventPrivacyLevel PrivacyLevel { get; set; } + + [JsonProperty("entity_type", NullValueHandling = NullValueHandling.Ignore)] + public DiscordScheduledGuildEventType Type { get; set; } + + [JsonProperty("scheduled_start_time", NullValueHandling = NullValueHandling.Ignore)] + public DateTimeOffset StartTime { get; set; } + + [JsonProperty("scheduled_end_time", NullValueHandling = NullValueHandling.Ignore)]// Null = no end date + public DateTimeOffset? EndTime { get; set; } + + [JsonProperty("entity_metadata", NullValueHandling = NullValueHandling.Ignore)] + public DiscordScheduledGuildEventMetadata? Metadata { get; set; } + + [JsonProperty("image", NullValueHandling = NullValueHandling.Ignore)] + public Optional CoverImage { get; set; } +} + +internal sealed class RestScheduledGuildEventModifyPayload +{ + [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] + public Optional Name { get; set; } + + [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] + public Optional Description { get; set; } + + [JsonProperty("channel_id", NullValueHandling = NullValueHandling.Ignore)] + public Optional ChannelId { get; set; } + + [JsonProperty("privacy_level", NullValueHandling = NullValueHandling.Ignore)] + public Optional PrivacyLevel { get; set; } + + [JsonProperty("entity_type", NullValueHandling = NullValueHandling.Ignore)] + public Optional Type { get; set; } + + [JsonProperty("scheduled_start_time", NullValueHandling = NullValueHandling.Ignore)] + public Optional StartTime { get; set; } + + [JsonProperty("scheduled_end_time", NullValueHandling = NullValueHandling.Ignore)] + public Optional EndTime { get; set; } + + [JsonProperty("entity_metadata", NullValueHandling = NullValueHandling.Ignore)] + public Optional Metadata { get; set; } + + [JsonProperty("status", NullValueHandling = NullValueHandling.Ignore)] + public Optional Status { get; set; } + + [JsonProperty("image", NullValueHandling = NullValueHandling.Ignore)] + public Optional CoverImage { get; set; } +} + +internal sealed class RestGuildChannelReorderPayload +{ + [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)] + public ulong ChannelId { get; set; } + + [JsonProperty("position", NullValueHandling = NullValueHandling.Ignore)] + public int Position { get; set; } + + [JsonProperty("lock_permissions", NullValueHandling = NullValueHandling.Ignore)] + public bool? LockPermissions { get; set; } + + [JsonProperty("parent_id", NullValueHandling = NullValueHandling.Ignore)] + public ulong? ParentId { get; set; } +} + +internal sealed class RestGuildRoleReorderPayload +{ + [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)] + public ulong RoleId { get; set; } + + [JsonProperty("position", NullValueHandling = NullValueHandling.Ignore)] + public int Position { get; set; } +} + +internal sealed class RestGuildMemberModifyPayload +{ + [JsonProperty("nick")] + public Optional Nickname { get; set; } + + [JsonProperty("roles")] + public Optional> RoleIds { get; set; } + + [JsonProperty("mute")] + public Optional Mute { get; set; } + + [JsonProperty("deaf")] + public Optional Deafen { get; set; } + + [JsonProperty("channel_id")] + public Optional VoiceChannelId { get; set; } + + [JsonProperty("communication_disabled_until", NullValueHandling = NullValueHandling.Include)] + public Optional CommunicationDisabledUntil { get; set; } + + [JsonProperty("flags")] + public Optional MemberFlags { get; set; } +} + +internal sealed class RestGuildRolePayload +{ + [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] + public string? Name { get; set; } + + [JsonProperty("permissions", NullValueHandling = NullValueHandling.Ignore)] + public DiscordPermissions? Permissions { get; set; } + + [JsonProperty("color", NullValueHandling = NullValueHandling.Ignore)] + public int? Color { get; set; } + + [JsonProperty("hoist", NullValueHandling = NullValueHandling.Ignore)] + public bool? Hoist { get; set; } + + [JsonProperty("mentionable", NullValueHandling = NullValueHandling.Ignore)] + public bool? Mentionable { get; set; } + + [JsonProperty("unicode_emoji", NullValueHandling = NullValueHandling.Ignore)] + public string? Emoji { get; set; } + + [JsonProperty("icon", NullValueHandling = NullValueHandling.Ignore)] + public string? Icon { get; set; } +} + +internal sealed class RestGuildPruneResultPayload +{ + [JsonProperty("pruned", NullValueHandling = NullValueHandling.Ignore)] + public int? Pruned { get; set; } +} + +internal sealed class RestGuildIntegrationAttachPayload +{ + [JsonProperty("type")] + public string Type { get; set; } + + [JsonProperty("id")] + public ulong Id { get; set; } +} + +internal sealed class RestGuildIntegrationModifyPayload +{ + [JsonProperty("expire_behavior", NullValueHandling = NullValueHandling.Ignore)] + public int? ExpireBehavior { get; set; } + + [JsonProperty("expire_grace_period", NullValueHandling = NullValueHandling.Ignore)] + public int? ExpireGracePeriod { get; set; } + + [JsonProperty("enable_emoticons", NullValueHandling = NullValueHandling.Ignore)] + public bool? EnableEmoticons { get; set; } +} + +internal class RestGuildEmojiModifyPayload +{ + [JsonProperty("name")] + public string? Name { get; set; } + + [JsonProperty("roles", NullValueHandling = NullValueHandling.Ignore)] + public ulong[]? Roles { get; set; } +} + +internal class RestGuildEmojiCreatePayload : RestGuildEmojiModifyPayload +{ + [JsonProperty("image")] + public string? ImageB64 { get; set; } +} + +internal class RestApplicationEmojiCreatePayload : RestApplicationEmojiModifyPayload +{ + [JsonProperty("image")] + public string ImageB64 { get; set; } +} + +internal class RestApplicationEmojiModifyPayload +{ + [JsonProperty("name")] + public string Name { get; set; } +} + +internal class RestGuildWidgetSettingsPayload +{ + [JsonProperty("enabled", NullValueHandling = NullValueHandling.Ignore)] + public bool? Enabled { get; set; } + + [JsonProperty("channel_id", NullValueHandling = NullValueHandling.Ignore)] + public ulong? ChannelId { get; set; } +} + +// TODO: this is wrong. i've annotated them for now, but we'll need to use optionals here +// since optional/nullable mean two different things in the context of modifying. +internal class RestGuildTemplateCreateOrModifyPayload +{ + [JsonProperty("name", NullValueHandling = NullValueHandling.Include)] + public string? Name { get; set; } + + [JsonProperty("description", NullValueHandling = NullValueHandling.Include)] + public string? Description { get; set; } +} + +internal class RestGuildMembershipScreeningFormModifyPayload +{ + [JsonProperty("enabled", NullValueHandling = NullValueHandling.Ignore)] + public Optional Enabled { get; set; } + + [JsonProperty("form_fields", NullValueHandling = NullValueHandling.Ignore)] + public Optional Fields { get; set; } + + [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] + public Optional Description { get; set; } +} + +internal class RestGuildWelcomeScreenModifyPayload +{ + [JsonProperty("enabled", NullValueHandling = NullValueHandling.Ignore)] + public Optional Enabled { get; set; } + + [JsonProperty("welcome_channels", NullValueHandling = NullValueHandling.Ignore)] + public Optional> WelcomeChannels { get; set; } + + [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] + public Optional Description { get; set; } +} + +internal class RestGuildUpdateCurrentUserVoiceStatePayload +{ + [JsonProperty("channel_id")] + public ulong ChannelId { get; set; } + + [JsonProperty("suppress", NullValueHandling = NullValueHandling.Ignore)] + public bool? Suppress { get; set; } + + [JsonProperty("request_to_speak_timestamp", NullValueHandling = NullValueHandling.Ignore)] + public DateTimeOffset? RequestToSpeakTimestamp { get; set; } +} + +internal class RestGuildUpdateUserVoiceStatePayload +{ + [JsonProperty("channel_id")] + public ulong ChannelId { get; set; } + + [JsonProperty("suppress", NullValueHandling = NullValueHandling.Ignore)] + public bool? Suppress { get; set; } +} + +internal class RestGuildBulkBanPayload +{ + [JsonProperty("delete_message_seconds", NullValueHandling = NullValueHandling.Ignore)] + public int? DeleteMessageSeconds { get; set; } + + [JsonProperty("user_ids")] + public IEnumerable UserIds { get; set; } +} diff --git a/DSharpPlus/Net/Abstractions/Rest/RestStickerPayloads.cs b/DSharpPlus/Net/Abstractions/Rest/RestStickerPayloads.cs index 98080a6d3a..7f2dfaa8f1 100644 --- a/DSharpPlus/Net/Abstractions/Rest/RestStickerPayloads.cs +++ b/DSharpPlus/Net/Abstractions/Rest/RestStickerPayloads.cs @@ -1,28 +1,28 @@ -using DSharpPlus.Entities; -using Newtonsoft.Json; - -namespace DSharpPlus.Net.Abstractions; - -internal class RestStickerCreatePayload -{ - [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] - public string Name { get; set; } - - [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] - public string Description { get; set; } - - [JsonProperty("tags", NullValueHandling = NullValueHandling.Ignore)] - public string Tags { get; set; } -} - -internal class RestStickerModifyPayload -{ - [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] - public Optional Name { get; set; } - - [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] - public Optional Description { get; set; } - - [JsonProperty("tags", NullValueHandling = NullValueHandling.Ignore)] - public Optional Tags { get; set; } -} +using DSharpPlus.Entities; +using Newtonsoft.Json; + +namespace DSharpPlus.Net.Abstractions; + +internal class RestStickerCreatePayload +{ + [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] + public string Name { get; set; } + + [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] + public string Description { get; set; } + + [JsonProperty("tags", NullValueHandling = NullValueHandling.Ignore)] + public string Tags { get; set; } +} + +internal class RestStickerModifyPayload +{ + [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] + public Optional Name { get; set; } + + [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] + public Optional Description { get; set; } + + [JsonProperty("tags", NullValueHandling = NullValueHandling.Ignore)] + public Optional Tags { get; set; } +} diff --git a/DSharpPlus/Net/Abstractions/Rest/RestThreadPayloads.cs b/DSharpPlus/Net/Abstractions/Rest/RestThreadPayloads.cs index 2398d0bbaf..767f3b63a4 100644 --- a/DSharpPlus/Net/Abstractions/Rest/RestThreadPayloads.cs +++ b/DSharpPlus/Net/Abstractions/Rest/RestThreadPayloads.cs @@ -1,35 +1,35 @@ -using System.Collections.Generic; -using DSharpPlus.Entities; -using Newtonsoft.Json; - -namespace DSharpPlus.Net.Abstractions; - -internal sealed class RestThreadCreatePayload -{ - [JsonProperty("name")] - public required string Name { get; set; } - - [JsonProperty("auto_archive_duration", NullValueHandling = NullValueHandling.Ignore)] - public DiscordAutoArchiveDuration ArchiveAfter { get; set; } - - [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] - public DiscordChannelType? Type { get; set; } -} - -internal sealed class RestForumPostCreatePayload -{ - [JsonProperty("name")] - public required string Name { get; set; } - - [JsonProperty("auto_archive_duration", NullValueHandling = NullValueHandling.Ignore)] - public DiscordAutoArchiveDuration? ArchiveAfter { get; set; } - - [JsonProperty("rate_limit_per_user", NullValueHandling = NullValueHandling.Include)] - public int? RateLimitPerUser { get; set; } - - [JsonProperty("message", NullValueHandling = NullValueHandling.Ignore)] - public required RestChannelMessageCreatePayload Message { get; set; } - - [JsonProperty("applied_tags", NullValueHandling = NullValueHandling.Ignore)] - public IEnumerable? AppliedTags { get; set; } -} +using System.Collections.Generic; +using DSharpPlus.Entities; +using Newtonsoft.Json; + +namespace DSharpPlus.Net.Abstractions; + +internal sealed class RestThreadCreatePayload +{ + [JsonProperty("name")] + public required string Name { get; set; } + + [JsonProperty("auto_archive_duration", NullValueHandling = NullValueHandling.Ignore)] + public DiscordAutoArchiveDuration ArchiveAfter { get; set; } + + [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] + public DiscordChannelType? Type { get; set; } +} + +internal sealed class RestForumPostCreatePayload +{ + [JsonProperty("name")] + public required string Name { get; set; } + + [JsonProperty("auto_archive_duration", NullValueHandling = NullValueHandling.Ignore)] + public DiscordAutoArchiveDuration? ArchiveAfter { get; set; } + + [JsonProperty("rate_limit_per_user", NullValueHandling = NullValueHandling.Include)] + public int? RateLimitPerUser { get; set; } + + [JsonProperty("message", NullValueHandling = NullValueHandling.Ignore)] + public required RestChannelMessageCreatePayload Message { get; set; } + + [JsonProperty("applied_tags", NullValueHandling = NullValueHandling.Ignore)] + public IEnumerable? AppliedTags { get; set; } +} diff --git a/DSharpPlus/Net/Abstractions/Rest/RestUserPayloads.cs b/DSharpPlus/Net/Abstractions/Rest/RestUserPayloads.cs index 802253c079..38f9db6ebe 100644 --- a/DSharpPlus/Net/Abstractions/Rest/RestUserPayloads.cs +++ b/DSharpPlus/Net/Abstractions/Rest/RestUserPayloads.cs @@ -1,74 +1,74 @@ -using System.Collections.Generic; -using DSharpPlus.Entities; -using Newtonsoft.Json; - -namespace DSharpPlus.Net.Abstractions; - -internal sealed class RestUserDmCreatePayload -{ - [JsonProperty("recipient_id")] - public ulong Recipient { get; set; } -} - -internal sealed class RestUserGroupDmCreatePayload -{ - [JsonProperty("access_tokens")] - public IEnumerable? AccessTokens { get; set; } - - [JsonProperty("nicks")] - public IDictionary? Nicknames { get; set; } -} - -internal sealed class RestUserUpdateCurrentPayload -{ - [JsonProperty("username", NullValueHandling = NullValueHandling.Ignore)] - public string? Username { get; set; } - - [JsonProperty("avatar", NullValueHandling = NullValueHandling.Include)] - public string? AvatarBase64 { get; set; } - - [JsonIgnore] - public bool AvatarSet { get; set; } - - [JsonProperty("banner", NullValueHandling = NullValueHandling.Include)] - public string? BannerBase64 { get; set; } - - [JsonIgnore] - public bool BannerSet { get; set; } - - public bool ShouldSerializeAvatarBase64() - => this.AvatarSet; - - public bool ShouldSerializeBannerBase64() - => this.BannerSet; -} - -internal sealed class RestUserGuild -{ - [JsonProperty("id")] - public ulong Id { get; set; } - - [JsonProperty("avatar", NullValueHandling = NullValueHandling.Ignore)] - public string? Name { get; set; } - - [JsonProperty("icon_hash", NullValueHandling = NullValueHandling.Ignore)] - public string? IconHash { get; set; } - - [JsonProperty("is_owner", NullValueHandling = NullValueHandling.Ignore)] - public bool? IsOwner { get; set; } - - [JsonProperty("permissions", NullValueHandling = NullValueHandling.Ignore)] - public DiscordPermissions Permissions { get; set; } -} - -internal sealed class RestUserGuildListPayload -{ - [JsonProperty("limit", NullValueHandling = NullValueHandling.Ignore)] - public int Limit { get; set; } - - [JsonProperty("before", NullValueHandling = NullValueHandling.Ignore)] - public ulong? Before { get; set; } - - [JsonProperty("after", NullValueHandling = NullValueHandling.Ignore)] - public ulong? After { get; set; } -} +using System.Collections.Generic; +using DSharpPlus.Entities; +using Newtonsoft.Json; + +namespace DSharpPlus.Net.Abstractions; + +internal sealed class RestUserDmCreatePayload +{ + [JsonProperty("recipient_id")] + public ulong Recipient { get; set; } +} + +internal sealed class RestUserGroupDmCreatePayload +{ + [JsonProperty("access_tokens")] + public IEnumerable? AccessTokens { get; set; } + + [JsonProperty("nicks")] + public IDictionary? Nicknames { get; set; } +} + +internal sealed class RestUserUpdateCurrentPayload +{ + [JsonProperty("username", NullValueHandling = NullValueHandling.Ignore)] + public string? Username { get; set; } + + [JsonProperty("avatar", NullValueHandling = NullValueHandling.Include)] + public string? AvatarBase64 { get; set; } + + [JsonIgnore] + public bool AvatarSet { get; set; } + + [JsonProperty("banner", NullValueHandling = NullValueHandling.Include)] + public string? BannerBase64 { get; set; } + + [JsonIgnore] + public bool BannerSet { get; set; } + + public bool ShouldSerializeAvatarBase64() + => this.AvatarSet; + + public bool ShouldSerializeBannerBase64() + => this.BannerSet; +} + +internal sealed class RestUserGuild +{ + [JsonProperty("id")] + public ulong Id { get; set; } + + [JsonProperty("avatar", NullValueHandling = NullValueHandling.Ignore)] + public string? Name { get; set; } + + [JsonProperty("icon_hash", NullValueHandling = NullValueHandling.Ignore)] + public string? IconHash { get; set; } + + [JsonProperty("is_owner", NullValueHandling = NullValueHandling.Ignore)] + public bool? IsOwner { get; set; } + + [JsonProperty("permissions", NullValueHandling = NullValueHandling.Ignore)] + public DiscordPermissions Permissions { get; set; } +} + +internal sealed class RestUserGuildListPayload +{ + [JsonProperty("limit", NullValueHandling = NullValueHandling.Ignore)] + public int Limit { get; set; } + + [JsonProperty("before", NullValueHandling = NullValueHandling.Ignore)] + public ulong? Before { get; set; } + + [JsonProperty("after", NullValueHandling = NullValueHandling.Ignore)] + public ulong? After { get; set; } +} diff --git a/DSharpPlus/Net/Abstractions/Rest/RestWebhookPayloads.cs b/DSharpPlus/Net/Abstractions/Rest/RestWebhookPayloads.cs index 5a7625bc5b..7e7dedd0f0 100644 --- a/DSharpPlus/Net/Abstractions/Rest/RestWebhookPayloads.cs +++ b/DSharpPlus/Net/Abstractions/Rest/RestWebhookPayloads.cs @@ -1,74 +1,74 @@ -using System.Collections.Generic; -using DSharpPlus.Entities; -using Newtonsoft.Json; - -namespace DSharpPlus.Net.Abstractions; - -internal sealed class RestWebhookPayload -{ - [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] - public string? Name { get; set; } - - [JsonProperty("avatar", NullValueHandling = NullValueHandling.Include)] - public string? AvatarBase64 { get; set; } - - [JsonProperty("channel_id")] - public ulong ChannelId { get; set; } - - [JsonProperty] - public bool AvatarSet { get; set; } - - public bool ShouldSerializeAvatarBase64() - => this.AvatarSet; -} - -internal sealed class RestWebhookExecutePayload -{ - [JsonProperty("content", NullValueHandling = NullValueHandling.Ignore)] - public string? Content { get; set; } - - [JsonProperty("username", NullValueHandling = NullValueHandling.Ignore)] - public string? Username { get; set; } - - [JsonProperty("avatar_url", NullValueHandling = NullValueHandling.Ignore)] - public string? AvatarUrl { get; set; } - - [JsonProperty("tts", NullValueHandling = NullValueHandling.Ignore)] - public bool? IsTTS { get; set; } - - [JsonProperty("embeds", NullValueHandling = NullValueHandling.Ignore)] - public IEnumerable? Embeds { get; set; } - - [JsonProperty("components", NullValueHandling = NullValueHandling.Ignore)] - public IEnumerable? Components { get; set; } - - [JsonProperty("allowed_mentions", NullValueHandling = NullValueHandling.Ignore)] - public DiscordMentions? Mentions { get; set; } - - [JsonProperty("poll", NullValueHandling = NullValueHandling.Ignore)] - public PollCreatePayload? Poll { get; set; } - - [JsonProperty("flags", NullValueHandling = NullValueHandling.Ignore)] - public DiscordMessageFlags? Flags { get; set; } -} - -internal sealed class RestWebhookMessageEditPayload -{ - [JsonProperty("content", NullValueHandling = NullValueHandling.Ignore)] - public Optional Content { get; set; } - - [JsonProperty("embeds", NullValueHandling = NullValueHandling.Ignore)] - public IEnumerable? Embeds { get; set; } - - [JsonProperty("allowed_mentions", NullValueHandling = NullValueHandling.Ignore)] - public DiscordMentions? Mentions { get; set; } - - [JsonProperty("components", NullValueHandling = NullValueHandling.Ignore)] - public IEnumerable? Components { get; set; } - - [JsonProperty("attachments", NullValueHandling = NullValueHandling.Ignore)] - public IEnumerable? Attachments { get; set; } - - [JsonProperty("flags", NullValueHandling = NullValueHandling.Ignore)] - public DiscordMessageFlags? Flags { get; set; } -} +using System.Collections.Generic; +using DSharpPlus.Entities; +using Newtonsoft.Json; + +namespace DSharpPlus.Net.Abstractions; + +internal sealed class RestWebhookPayload +{ + [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] + public string? Name { get; set; } + + [JsonProperty("avatar", NullValueHandling = NullValueHandling.Include)] + public string? AvatarBase64 { get; set; } + + [JsonProperty("channel_id")] + public ulong ChannelId { get; set; } + + [JsonProperty] + public bool AvatarSet { get; set; } + + public bool ShouldSerializeAvatarBase64() + => this.AvatarSet; +} + +internal sealed class RestWebhookExecutePayload +{ + [JsonProperty("content", NullValueHandling = NullValueHandling.Ignore)] + public string? Content { get; set; } + + [JsonProperty("username", NullValueHandling = NullValueHandling.Ignore)] + public string? Username { get; set; } + + [JsonProperty("avatar_url", NullValueHandling = NullValueHandling.Ignore)] + public string? AvatarUrl { get; set; } + + [JsonProperty("tts", NullValueHandling = NullValueHandling.Ignore)] + public bool? IsTTS { get; set; } + + [JsonProperty("embeds", NullValueHandling = NullValueHandling.Ignore)] + public IEnumerable? Embeds { get; set; } + + [JsonProperty("components", NullValueHandling = NullValueHandling.Ignore)] + public IEnumerable? Components { get; set; } + + [JsonProperty("allowed_mentions", NullValueHandling = NullValueHandling.Ignore)] + public DiscordMentions? Mentions { get; set; } + + [JsonProperty("poll", NullValueHandling = NullValueHandling.Ignore)] + public PollCreatePayload? Poll { get; set; } + + [JsonProperty("flags", NullValueHandling = NullValueHandling.Ignore)] + public DiscordMessageFlags? Flags { get; set; } +} + +internal sealed class RestWebhookMessageEditPayload +{ + [JsonProperty("content", NullValueHandling = NullValueHandling.Ignore)] + public Optional Content { get; set; } + + [JsonProperty("embeds", NullValueHandling = NullValueHandling.Ignore)] + public IEnumerable? Embeds { get; set; } + + [JsonProperty("allowed_mentions", NullValueHandling = NullValueHandling.Ignore)] + public DiscordMentions? Mentions { get; set; } + + [JsonProperty("components", NullValueHandling = NullValueHandling.Ignore)] + public IEnumerable? Components { get; set; } + + [JsonProperty("attachments", NullValueHandling = NullValueHandling.Ignore)] + public IEnumerable? Attachments { get; set; } + + [JsonProperty("flags", NullValueHandling = NullValueHandling.Ignore)] + public DiscordMessageFlags? Flags { get; set; } +} diff --git a/DSharpPlus/Net/Abstractions/ShardInfo.cs b/DSharpPlus/Net/Abstractions/ShardInfo.cs index 91626b4bef..41a07e7285 100644 --- a/DSharpPlus/Net/Abstractions/ShardInfo.cs +++ b/DSharpPlus/Net/Abstractions/ShardInfo.cs @@ -1,55 +1,55 @@ -using System; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace DSharpPlus.Net.Abstractions; - -/// -/// Represents data for identify payload's shard info. -/// -[JsonConverter(typeof(ShardInfoConverter))] -public sealed class ShardInfo -{ - /// - /// Gets or sets this client's shard id. - /// - public int ShardId { get; set; } - - /// - /// Gets or sets the total shard count for this token. - /// - public int ShardCount { get; set; } -} - -internal sealed class ShardInfoConverter : JsonConverter -{ - public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) - { - ArgumentNullException.ThrowIfNull(value, nameof(value)); - - ShardInfo info = (ShardInfo)value; - int[] obj = [info.ShardId, info.ShardCount]; - - serializer.Serialize(writer, obj); - } - - public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) - { - JArray arr = ReadArrayObject(reader, serializer); - - return new ShardInfo - { - ShardId = (int)arr[0], - ShardCount = (int)arr[1], - }; - } - - private static JArray ReadArrayObject(JsonReader reader, JsonSerializer serializer) - { - return serializer.Deserialize(reader) is not JArray arr || arr.Count != 2 - ? throw new JsonSerializationException("Expected array of length 2") - : arr; - } - - public override bool CanConvert(Type objectType) => objectType == typeof(ShardInfo); -} +using System; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace DSharpPlus.Net.Abstractions; + +/// +/// Represents data for identify payload's shard info. +/// +[JsonConverter(typeof(ShardInfoConverter))] +public sealed class ShardInfo +{ + /// + /// Gets or sets this client's shard id. + /// + public int ShardId { get; set; } + + /// + /// Gets or sets the total shard count for this token. + /// + public int ShardCount { get; set; } +} + +internal sealed class ShardInfoConverter : JsonConverter +{ + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) + { + ArgumentNullException.ThrowIfNull(value, nameof(value)); + + ShardInfo info = (ShardInfo)value; + int[] obj = [info.ShardId, info.ShardCount]; + + serializer.Serialize(writer, obj); + } + + public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) + { + JArray arr = ReadArrayObject(reader, serializer); + + return new ShardInfo + { + ShardId = (int)arr[0], + ShardCount = (int)arr[1], + }; + } + + private static JArray ReadArrayObject(JsonReader reader, JsonSerializer serializer) + { + return serializer.Deserialize(reader) is not JArray arr || arr.Count != 2 + ? throw new JsonSerializationException("Expected array of length 2") + : arr; + } + + public override bool CanConvert(Type objectType) => objectType == typeof(ShardInfo); +} diff --git a/DSharpPlus/Net/Abstractions/StatusUpdate.cs b/DSharpPlus/Net/Abstractions/StatusUpdate.cs index 915fa0eb88..284f52d9ed 100644 --- a/DSharpPlus/Net/Abstractions/StatusUpdate.cs +++ b/DSharpPlus/Net/Abstractions/StatusUpdate.cs @@ -1,46 +1,46 @@ -using DSharpPlus.Entities; -using Newtonsoft.Json; - -namespace DSharpPlus.Net.Abstractions; - -/// -/// Represents data for websocket status update payload. -/// -internal sealed class StatusUpdate -{ - /// - /// Gets or sets the unix millisecond timestamp of when the user went idle. - /// - [JsonProperty("since", NullValueHandling = NullValueHandling.Include)] - public long? IdleSince { get; set; } - - /// - /// Gets or sets whether the user is AFK. - /// - [JsonProperty("afk")] - public bool IsAFK { get; set; } - - /// - /// Gets or sets the status of the user. - /// - [JsonIgnore] - public DiscordUserStatus Status { get; set; } = DiscordUserStatus.Online; - - [JsonProperty("status")] - internal string StatusString => this.Status switch - { - DiscordUserStatus.Online => "online", - DiscordUserStatus.Idle => "idle", - DiscordUserStatus.DoNotDisturb => "dnd", - DiscordUserStatus.Invisible or DiscordUserStatus.Offline => "invisible", - _ => "online", - }; - - /// - /// Gets or sets the game the user is playing. - /// - [JsonProperty("game", NullValueHandling = NullValueHandling.Ignore)] - public TransportActivity Activity { get; set; } - - internal DiscordActivity activity; -} +using DSharpPlus.Entities; +using Newtonsoft.Json; + +namespace DSharpPlus.Net.Abstractions; + +/// +/// Represents data for websocket status update payload. +/// +internal sealed class StatusUpdate +{ + /// + /// Gets or sets the unix millisecond timestamp of when the user went idle. + /// + [JsonProperty("since", NullValueHandling = NullValueHandling.Include)] + public long? IdleSince { get; set; } + + /// + /// Gets or sets whether the user is AFK. + /// + [JsonProperty("afk")] + public bool IsAFK { get; set; } + + /// + /// Gets or sets the status of the user. + /// + [JsonIgnore] + public DiscordUserStatus Status { get; set; } = DiscordUserStatus.Online; + + [JsonProperty("status")] + internal string StatusString => this.Status switch + { + DiscordUserStatus.Online => "online", + DiscordUserStatus.Idle => "idle", + DiscordUserStatus.DoNotDisturb => "dnd", + DiscordUserStatus.Invisible or DiscordUserStatus.Offline => "invisible", + _ => "online", + }; + + /// + /// Gets or sets the game the user is playing. + /// + [JsonProperty("game", NullValueHandling = NullValueHandling.Ignore)] + public TransportActivity Activity { get; set; } + + internal DiscordActivity activity; +} diff --git a/DSharpPlus/Net/Abstractions/Transport/TransportActivity.cs b/DSharpPlus/Net/Abstractions/Transport/TransportActivity.cs index 273e5b2d9b..6f14de94e0 100644 --- a/DSharpPlus/Net/Abstractions/Transport/TransportActivity.cs +++ b/DSharpPlus/Net/Abstractions/Transport/TransportActivity.cs @@ -1,275 +1,275 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using DSharpPlus.Entities; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace DSharpPlus.Net.Abstractions; - -/// -/// Represents a game a user is playing. -/// -internal sealed class TransportActivity -{ - /// - /// Gets or sets the name of the game the user is playing. - /// - [JsonProperty("name", NullValueHandling = NullValueHandling.Include)] - public string Name { get; internal set; } - - /// - /// Gets or sets the stream URI, if applicable. - /// - [JsonProperty("url", NullValueHandling = NullValueHandling.Ignore)] - public string StreamUrl { get; internal set; } - - /// - /// Gets or sets the livesteam type. - /// - [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] - public DiscordActivityType ActivityType { get; internal set; } - - /// - /// Gets or sets the details. - /// - /// This is a component of the rich presence, and, as such, can only be used by regular users. - /// - [JsonProperty("details", NullValueHandling = NullValueHandling.Ignore)] - public string Details { get; internal set; } - - /// - /// Gets or sets game state. - /// - /// This is a component of the rich presence, and, as such, can only be used by regular users. - /// - [JsonProperty("state", NullValueHandling = NullValueHandling.Ignore)] - public string State { get; internal set; } - - /// - /// Gets the emoji details for a custom status, if any. - /// - [JsonProperty("emoji", NullValueHandling = NullValueHandling.Ignore)] - public DiscordEmoji Emoji { get; internal set; } - - /// - /// Gets ID of the application for which this rich presence is for. - /// - /// This is a component of the rich presence, and, as such, can only be used by regular users. - /// - [JsonIgnore] - public ulong? ApplicationId - { - get => this.ApplicationIdStr != null ? ulong.Parse(this.ApplicationIdStr, CultureInfo.InvariantCulture) : null; - internal set => this.ApplicationIdStr = value?.ToString(CultureInfo.InvariantCulture); - } - - [JsonProperty("application_id", NullValueHandling = NullValueHandling.Ignore)] - internal string ApplicationIdStr { get; set; } - - /// - /// Gets or sets instance status. - /// - /// This is a component of the rich presence, and, as such, can only be used by regular users. - /// - [JsonProperty("instance", NullValueHandling = NullValueHandling.Ignore)] - public bool? Instance { get; internal set; } - - /// - /// Gets or sets information about the current game's party. - /// - /// This is a component of the rich presence, and, as such, can only be used by regular users. - /// - [JsonProperty("party", NullValueHandling = NullValueHandling.Ignore)] - public GameParty Party { get; internal set; } - - /// - /// Gets or sets information about assets related to this rich presence. - /// - /// This is a component of the rich presence, and, as such, can only be used by regular users. - /// - [JsonProperty("assets", NullValueHandling = NullValueHandling.Ignore)] - public PresenceAssets Assets { get; internal set; } - - /// - /// Gets or sets information about current game's timestamps. - /// - /// This is a component of the rich presence, and, as such, can only be used by regular users. - /// - [JsonProperty("timestamps", NullValueHandling = NullValueHandling.Ignore)] - public GameTimestamps Timestamps { get; internal set; } - - /// - /// Gets or sets information about current game's secret values. - /// - /// This is a component of the rich presence, and, as such, can only be used by regular users. - /// - [JsonProperty("secrets", NullValueHandling = NullValueHandling.Ignore)] - public GameSecrets Secrets { get; internal set; } - - [JsonProperty("buttons", NullValueHandling = NullValueHandling.Ignore)] - public IReadOnlyList Buttons { get; internal set; } - - internal TransportActivity() { } - - internal TransportActivity(DiscordActivity game) - { - if (game == null) - { - return; - } - - this.Name = game.Name; - this.State = game.CustomStatus?.Name!; - this.ActivityType = game.ActivityType; - this.StreamUrl = game.StreamUrl; - } - - public bool IsRichPresence() - => this.Details != null || this.State != null || this.ApplicationId != null || this.Instance != null || this.Party != null || this.Assets != null || this.Secrets != null || this.Timestamps != null; - - public bool IsCustomStatus() - => this.Name == "Custom Status"; - - /// - /// Represents information about assets attached to a rich presence. - /// - public class PresenceAssets - { - /// - /// Gets the large image asset ID. - /// - [JsonProperty("large_image")] - public string LargeImage { get; set; } - - /// - /// Gets the large image text. - /// - [JsonProperty("large_text", NullValueHandling = NullValueHandling.Ignore)] - public string LargeImageText { get; internal set; } - - /// - /// Gets the small image asset ID. - /// - [JsonProperty("small_image")] - internal string SmallImage { get; set; } - - /// - /// Gets the small image text. - /// - [JsonProperty("small_text", NullValueHandling = NullValueHandling.Ignore)] - public string SmallImageText { get; internal set; } - } - - /// - /// Represents information about rich presence game party. - /// - public class GameParty - { - /// - /// Gets the game party ID. - /// - [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)] - public string Id { get; internal set; } - - /// - /// Gets the size of the party. - /// - [JsonProperty("size", NullValueHandling = NullValueHandling.Ignore)] - public GamePartySize Size { get; internal set; } - - /// - /// Represents information about party size. - /// - [JsonConverter(typeof(GamePartySizeConverter))] - public class GamePartySize - { - /// - /// Gets the current number of players in the party. - /// - public long Current { get; internal set; } - - /// - /// Gets the maximum party size. - /// - public long Maximum { get; internal set; } - } - } - - /// - /// Represents information about the game state's timestamps. - /// - public class GameTimestamps - { - /// - /// Gets the time the game has started. - /// - [JsonIgnore] - public DateTimeOffset? Start - => this.start != null ? Utilities.GetDateTimeOffsetFromMilliseconds(this.start.Value, false) : null; - - [JsonProperty("start", NullValueHandling = NullValueHandling.Ignore)] - internal long? start; - - /// - /// Gets the time the game is going to end. - /// - [JsonIgnore] - public DateTimeOffset? End - => this.end != null ? Utilities.GetDateTimeOffsetFromMilliseconds(this.end.Value, false) : null; - - [JsonProperty("end", NullValueHandling = NullValueHandling.Ignore)] - internal long? end; - } - - /// - /// Represents information about secret values for the Join, Spectate, and Match actions. - /// - public class GameSecrets - { - /// - /// Gets the secret value for join action. - /// - [JsonProperty("join", NullValueHandling = NullValueHandling.Ignore)] - public string Join { get; internal set; } - - /// - /// Gets the secret value for match action. - /// - [JsonProperty("match", NullValueHandling = NullValueHandling.Ignore)] - public string Match { get; internal set; } - - /// - /// Gets the secret value for spectate action. - /// - [JsonProperty("spectate", NullValueHandling = NullValueHandling.Ignore)] - public string Spectate { get; internal set; } - } -} - -internal sealed class GamePartySizeConverter : JsonConverter -{ - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - object[]? obj = value is TransportActivity.GameParty.GamePartySize sinfo - ? new object[] { sinfo.Current, sinfo.Maximum } - : null; - serializer.Serialize(writer, obj); - } - - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - JArray arr = ReadArrayObject(reader, serializer); - return new TransportActivity.GameParty.GamePartySize - { - Current = (long)arr[0], - Maximum = (long)arr[1], - }; - } - - private static JArray ReadArrayObject(JsonReader reader, JsonSerializer serializer) => serializer.Deserialize(reader) is not JArray arr || arr.Count != 2 - ? throw new JsonSerializationException("Expected array of length 2") - : arr; - - public override bool CanConvert(Type objectType) => objectType == typeof(TransportActivity.GameParty.GamePartySize); -} +using System; +using System.Collections.Generic; +using System.Globalization; +using DSharpPlus.Entities; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace DSharpPlus.Net.Abstractions; + +/// +/// Represents a game a user is playing. +/// +internal sealed class TransportActivity +{ + /// + /// Gets or sets the name of the game the user is playing. + /// + [JsonProperty("name", NullValueHandling = NullValueHandling.Include)] + public string Name { get; internal set; } + + /// + /// Gets or sets the stream URI, if applicable. + /// + [JsonProperty("url", NullValueHandling = NullValueHandling.Ignore)] + public string StreamUrl { get; internal set; } + + /// + /// Gets or sets the livesteam type. + /// + [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] + public DiscordActivityType ActivityType { get; internal set; } + + /// + /// Gets or sets the details. + /// + /// This is a component of the rich presence, and, as such, can only be used by regular users. + /// + [JsonProperty("details", NullValueHandling = NullValueHandling.Ignore)] + public string Details { get; internal set; } + + /// + /// Gets or sets game state. + /// + /// This is a component of the rich presence, and, as such, can only be used by regular users. + /// + [JsonProperty("state", NullValueHandling = NullValueHandling.Ignore)] + public string State { get; internal set; } + + /// + /// Gets the emoji details for a custom status, if any. + /// + [JsonProperty("emoji", NullValueHandling = NullValueHandling.Ignore)] + public DiscordEmoji Emoji { get; internal set; } + + /// + /// Gets ID of the application for which this rich presence is for. + /// + /// This is a component of the rich presence, and, as such, can only be used by regular users. + /// + [JsonIgnore] + public ulong? ApplicationId + { + get => this.ApplicationIdStr != null ? ulong.Parse(this.ApplicationIdStr, CultureInfo.InvariantCulture) : null; + internal set => this.ApplicationIdStr = value?.ToString(CultureInfo.InvariantCulture); + } + + [JsonProperty("application_id", NullValueHandling = NullValueHandling.Ignore)] + internal string ApplicationIdStr { get; set; } + + /// + /// Gets or sets instance status. + /// + /// This is a component of the rich presence, and, as such, can only be used by regular users. + /// + [JsonProperty("instance", NullValueHandling = NullValueHandling.Ignore)] + public bool? Instance { get; internal set; } + + /// + /// Gets or sets information about the current game's party. + /// + /// This is a component of the rich presence, and, as such, can only be used by regular users. + /// + [JsonProperty("party", NullValueHandling = NullValueHandling.Ignore)] + public GameParty Party { get; internal set; } + + /// + /// Gets or sets information about assets related to this rich presence. + /// + /// This is a component of the rich presence, and, as such, can only be used by regular users. + /// + [JsonProperty("assets", NullValueHandling = NullValueHandling.Ignore)] + public PresenceAssets Assets { get; internal set; } + + /// + /// Gets or sets information about current game's timestamps. + /// + /// This is a component of the rich presence, and, as such, can only be used by regular users. + /// + [JsonProperty("timestamps", NullValueHandling = NullValueHandling.Ignore)] + public GameTimestamps Timestamps { get; internal set; } + + /// + /// Gets or sets information about current game's secret values. + /// + /// This is a component of the rich presence, and, as such, can only be used by regular users. + /// + [JsonProperty("secrets", NullValueHandling = NullValueHandling.Ignore)] + public GameSecrets Secrets { get; internal set; } + + [JsonProperty("buttons", NullValueHandling = NullValueHandling.Ignore)] + public IReadOnlyList Buttons { get; internal set; } + + internal TransportActivity() { } + + internal TransportActivity(DiscordActivity game) + { + if (game == null) + { + return; + } + + this.Name = game.Name; + this.State = game.CustomStatus?.Name!; + this.ActivityType = game.ActivityType; + this.StreamUrl = game.StreamUrl; + } + + public bool IsRichPresence() + => this.Details != null || this.State != null || this.ApplicationId != null || this.Instance != null || this.Party != null || this.Assets != null || this.Secrets != null || this.Timestamps != null; + + public bool IsCustomStatus() + => this.Name == "Custom Status"; + + /// + /// Represents information about assets attached to a rich presence. + /// + public class PresenceAssets + { + /// + /// Gets the large image asset ID. + /// + [JsonProperty("large_image")] + public string LargeImage { get; set; } + + /// + /// Gets the large image text. + /// + [JsonProperty("large_text", NullValueHandling = NullValueHandling.Ignore)] + public string LargeImageText { get; internal set; } + + /// + /// Gets the small image asset ID. + /// + [JsonProperty("small_image")] + internal string SmallImage { get; set; } + + /// + /// Gets the small image text. + /// + [JsonProperty("small_text", NullValueHandling = NullValueHandling.Ignore)] + public string SmallImageText { get; internal set; } + } + + /// + /// Represents information about rich presence game party. + /// + public class GameParty + { + /// + /// Gets the game party ID. + /// + [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)] + public string Id { get; internal set; } + + /// + /// Gets the size of the party. + /// + [JsonProperty("size", NullValueHandling = NullValueHandling.Ignore)] + public GamePartySize Size { get; internal set; } + + /// + /// Represents information about party size. + /// + [JsonConverter(typeof(GamePartySizeConverter))] + public class GamePartySize + { + /// + /// Gets the current number of players in the party. + /// + public long Current { get; internal set; } + + /// + /// Gets the maximum party size. + /// + public long Maximum { get; internal set; } + } + } + + /// + /// Represents information about the game state's timestamps. + /// + public class GameTimestamps + { + /// + /// Gets the time the game has started. + /// + [JsonIgnore] + public DateTimeOffset? Start + => this.start != null ? Utilities.GetDateTimeOffsetFromMilliseconds(this.start.Value, false) : null; + + [JsonProperty("start", NullValueHandling = NullValueHandling.Ignore)] + internal long? start; + + /// + /// Gets the time the game is going to end. + /// + [JsonIgnore] + public DateTimeOffset? End + => this.end != null ? Utilities.GetDateTimeOffsetFromMilliseconds(this.end.Value, false) : null; + + [JsonProperty("end", NullValueHandling = NullValueHandling.Ignore)] + internal long? end; + } + + /// + /// Represents information about secret values for the Join, Spectate, and Match actions. + /// + public class GameSecrets + { + /// + /// Gets the secret value for join action. + /// + [JsonProperty("join", NullValueHandling = NullValueHandling.Ignore)] + public string Join { get; internal set; } + + /// + /// Gets the secret value for match action. + /// + [JsonProperty("match", NullValueHandling = NullValueHandling.Ignore)] + public string Match { get; internal set; } + + /// + /// Gets the secret value for spectate action. + /// + [JsonProperty("spectate", NullValueHandling = NullValueHandling.Ignore)] + public string Spectate { get; internal set; } + } +} + +internal sealed class GamePartySizeConverter : JsonConverter +{ + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + object[]? obj = value is TransportActivity.GameParty.GamePartySize sinfo + ? new object[] { sinfo.Current, sinfo.Maximum } + : null; + serializer.Serialize(writer, obj); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + JArray arr = ReadArrayObject(reader, serializer); + return new TransportActivity.GameParty.GamePartySize + { + Current = (long)arr[0], + Maximum = (long)arr[1], + }; + } + + private static JArray ReadArrayObject(JsonReader reader, JsonSerializer serializer) => serializer.Deserialize(reader) is not JArray arr || arr.Count != 2 + ? throw new JsonSerializationException("Expected array of length 2") + : arr; + + public override bool CanConvert(Type objectType) => objectType == typeof(TransportActivity.GameParty.GamePartySize); +} diff --git a/DSharpPlus/Net/Abstractions/Transport/TransportApplication.cs b/DSharpPlus/Net/Abstractions/Transport/TransportApplication.cs index cdd71d80f5..aecc14863e 100644 --- a/DSharpPlus/Net/Abstractions/Transport/TransportApplication.cs +++ b/DSharpPlus/Net/Abstractions/Transport/TransportApplication.cs @@ -1,94 +1,94 @@ -using System.Collections.Generic; -using DSharpPlus.Entities; -using Newtonsoft.Json; - -namespace DSharpPlus.Net.Abstractions; - -internal sealed class TransportApplication -{ - [JsonProperty("id", NullValueHandling = NullValueHandling.Include)] - public ulong Id { get; set; } - - [JsonProperty("name", NullValueHandling = NullValueHandling.Include)] - public string Name { get; set; } - - [JsonProperty("icon", NullValueHandling = NullValueHandling.Include)] - public string IconHash { get; set; } - - [JsonProperty("description", NullValueHandling = NullValueHandling.Include)] - public string Description { get; set; } - - [JsonProperty("rpc_origins", NullValueHandling = NullValueHandling.Ignore)] - public IList? RpcOrigins { get; set; } - - [JsonProperty("bot_public", NullValueHandling = NullValueHandling.Include)] - public bool IsPublicBot { get; set; } - - [JsonProperty("bot_require_code_grant", NullValueHandling = NullValueHandling.Include)] - public bool BotRequiresCodeGrant { get; set; } - - [JsonProperty("bot")] - public TransportUser? Bot { get; set; } - - [JsonProperty("terms_of_service_url", NullValueHandling = NullValueHandling.Ignore)] - public string? TermsOfServiceUrl { get; set; } - - [JsonProperty("privacy_policy_url", NullValueHandling = NullValueHandling.Ignore)] - public string? PrivacyPolicyUrl { get; set; } - - [JsonProperty("owner", NullValueHandling = NullValueHandling.Include)] - public TransportUser? Owner { get; set; } - - [JsonProperty("verify_key", NullValueHandling = NullValueHandling.Include)] - public string VerifyKey { get; set; } - - [JsonProperty("team", NullValueHandling = NullValueHandling.Include)] - public TransportTeam? Team { get; set; } - - [JsonProperty("guild_id")] - public ulong? GuildId { get; set; } - - [JsonProperty("guild")] - public DiscordGuild? Guild { get; set; } - - [JsonProperty("primary_sku_id")] - public ulong PrimarySkuId { get; set; } - - [JsonProperty("slug")] - public string Slug { get; set; } - - [JsonProperty("cover_image")] - public string CoverImageHash { get; set; } - - [JsonProperty("flags", NullValueHandling = NullValueHandling.Ignore)] - public DiscordApplicationFlags? Flags { get; set; } - - [JsonProperty("approximate_guild_count")] - public int? ApproximateGuildCount { get; set; } - - [JsonProperty("approximate_user_install_count")] - public int? ApproximateUserInstallCount { get; set; } - - [JsonProperty("redirect_uris")] - public string[] RedirectUris { get; set; } - - [JsonProperty("interactions_endpoint_url")] - public string? InteractionEndpointUrl { get; set; } - - [JsonProperty("role_connections_verification_url")] - public string? RoleConnectionsVerificationUrl { get; set; } - - [JsonProperty("tags")] - public string[]? Tags { get; set; } - - [JsonProperty("install_params")] - public DiscordApplicationOAuth2InstallParams InstallParams { get; set; } - - [JsonProperty("integration_types_config")] - public Dictionary IntegrationTypeConfigurations { get; set; } - - [JsonProperty("custom_install_url")] - public string CustomInstallUrl { get; set; } - - internal TransportApplication() { } -} +using System.Collections.Generic; +using DSharpPlus.Entities; +using Newtonsoft.Json; + +namespace DSharpPlus.Net.Abstractions; + +internal sealed class TransportApplication +{ + [JsonProperty("id", NullValueHandling = NullValueHandling.Include)] + public ulong Id { get; set; } + + [JsonProperty("name", NullValueHandling = NullValueHandling.Include)] + public string Name { get; set; } + + [JsonProperty("icon", NullValueHandling = NullValueHandling.Include)] + public string IconHash { get; set; } + + [JsonProperty("description", NullValueHandling = NullValueHandling.Include)] + public string Description { get; set; } + + [JsonProperty("rpc_origins", NullValueHandling = NullValueHandling.Ignore)] + public IList? RpcOrigins { get; set; } + + [JsonProperty("bot_public", NullValueHandling = NullValueHandling.Include)] + public bool IsPublicBot { get; set; } + + [JsonProperty("bot_require_code_grant", NullValueHandling = NullValueHandling.Include)] + public bool BotRequiresCodeGrant { get; set; } + + [JsonProperty("bot")] + public TransportUser? Bot { get; set; } + + [JsonProperty("terms_of_service_url", NullValueHandling = NullValueHandling.Ignore)] + public string? TermsOfServiceUrl { get; set; } + + [JsonProperty("privacy_policy_url", NullValueHandling = NullValueHandling.Ignore)] + public string? PrivacyPolicyUrl { get; set; } + + [JsonProperty("owner", NullValueHandling = NullValueHandling.Include)] + public TransportUser? Owner { get; set; } + + [JsonProperty("verify_key", NullValueHandling = NullValueHandling.Include)] + public string VerifyKey { get; set; } + + [JsonProperty("team", NullValueHandling = NullValueHandling.Include)] + public TransportTeam? Team { get; set; } + + [JsonProperty("guild_id")] + public ulong? GuildId { get; set; } + + [JsonProperty("guild")] + public DiscordGuild? Guild { get; set; } + + [JsonProperty("primary_sku_id")] + public ulong PrimarySkuId { get; set; } + + [JsonProperty("slug")] + public string Slug { get; set; } + + [JsonProperty("cover_image")] + public string CoverImageHash { get; set; } + + [JsonProperty("flags", NullValueHandling = NullValueHandling.Ignore)] + public DiscordApplicationFlags? Flags { get; set; } + + [JsonProperty("approximate_guild_count")] + public int? ApproximateGuildCount { get; set; } + + [JsonProperty("approximate_user_install_count")] + public int? ApproximateUserInstallCount { get; set; } + + [JsonProperty("redirect_uris")] + public string[] RedirectUris { get; set; } + + [JsonProperty("interactions_endpoint_url")] + public string? InteractionEndpointUrl { get; set; } + + [JsonProperty("role_connections_verification_url")] + public string? RoleConnectionsVerificationUrl { get; set; } + + [JsonProperty("tags")] + public string[]? Tags { get; set; } + + [JsonProperty("install_params")] + public DiscordApplicationOAuth2InstallParams InstallParams { get; set; } + + [JsonProperty("integration_types_config")] + public Dictionary IntegrationTypeConfigurations { get; set; } + + [JsonProperty("custom_install_url")] + public string CustomInstallUrl { get; set; } + + internal TransportApplication() { } +} diff --git a/DSharpPlus/Net/Abstractions/Transport/TransportMember.cs b/DSharpPlus/Net/Abstractions/Transport/TransportMember.cs index ad390f023a..f6fe71eb0d 100644 --- a/DSharpPlus/Net/Abstractions/Transport/TransportMember.cs +++ b/DSharpPlus/Net/Abstractions/Transport/TransportMember.cs @@ -1,42 +1,42 @@ -using System; -using System.Collections.Generic; -using DSharpPlus.Entities; -using Newtonsoft.Json; - -namespace DSharpPlus.Net.Abstractions; - -internal class TransportMember -{ - [JsonProperty("avatar", NullValueHandling = NullValueHandling.Ignore)] - public string AvatarHash { get; internal set; } - - [JsonProperty("user", NullValueHandling = NullValueHandling.Ignore)] - public TransportUser User { get; internal set; } - - [JsonProperty("nick", NullValueHandling = NullValueHandling.Ignore)] - public string Nickname { get; internal set; } - - [JsonProperty("roles", NullValueHandling = NullValueHandling.Ignore)] - public List Roles { get; internal set; } - - [JsonProperty("communication_disabled_until", NullValueHandling = NullValueHandling.Include)] - public DateTimeOffset? CommunicationDisabledUntil { get; internal set; } - - [JsonProperty("joined_at", NullValueHandling = NullValueHandling.Ignore)] - public DateTime JoinedAt { get; internal set; } - - [JsonProperty("deaf", NullValueHandling = NullValueHandling.Ignore)] - public bool IsDeafened { get; internal set; } - - [JsonProperty("mute", NullValueHandling = NullValueHandling.Ignore)] - public bool IsMuted { get; internal set; } - - [JsonProperty("premium_since", NullValueHandling = NullValueHandling.Ignore)] - public DateTime? PremiumSince { get; internal set; } - - [JsonProperty("pending", NullValueHandling = NullValueHandling.Ignore)] - public bool? IsPending { get; internal set; } - - [JsonProperty("flags", NullValueHandling = NullValueHandling.Ignore)] - public DiscordMemberFlags? Flags { get; internal set; } -} +using System; +using System.Collections.Generic; +using DSharpPlus.Entities; +using Newtonsoft.Json; + +namespace DSharpPlus.Net.Abstractions; + +internal class TransportMember +{ + [JsonProperty("avatar", NullValueHandling = NullValueHandling.Ignore)] + public string AvatarHash { get; internal set; } + + [JsonProperty("user", NullValueHandling = NullValueHandling.Ignore)] + public TransportUser User { get; internal set; } + + [JsonProperty("nick", NullValueHandling = NullValueHandling.Ignore)] + public string Nickname { get; internal set; } + + [JsonProperty("roles", NullValueHandling = NullValueHandling.Ignore)] + public List Roles { get; internal set; } + + [JsonProperty("communication_disabled_until", NullValueHandling = NullValueHandling.Include)] + public DateTimeOffset? CommunicationDisabledUntil { get; internal set; } + + [JsonProperty("joined_at", NullValueHandling = NullValueHandling.Ignore)] + public DateTime JoinedAt { get; internal set; } + + [JsonProperty("deaf", NullValueHandling = NullValueHandling.Ignore)] + public bool IsDeafened { get; internal set; } + + [JsonProperty("mute", NullValueHandling = NullValueHandling.Ignore)] + public bool IsMuted { get; internal set; } + + [JsonProperty("premium_since", NullValueHandling = NullValueHandling.Ignore)] + public DateTime? PremiumSince { get; internal set; } + + [JsonProperty("pending", NullValueHandling = NullValueHandling.Ignore)] + public bool? IsPending { get; internal set; } + + [JsonProperty("flags", NullValueHandling = NullValueHandling.Ignore)] + public DiscordMemberFlags? Flags { get; internal set; } +} diff --git a/DSharpPlus/Net/Abstractions/Transport/TransportTeam.cs b/DSharpPlus/Net/Abstractions/Transport/TransportTeam.cs index 532c98f489..5e004b4e11 100644 --- a/DSharpPlus/Net/Abstractions/Transport/TransportTeam.cs +++ b/DSharpPlus/Net/Abstractions/Transport/TransportTeam.cs @@ -1,41 +1,41 @@ -using System.Collections.Generic; -using Newtonsoft.Json; - -namespace DSharpPlus.Net.Abstractions; - -internal sealed class TransportTeam -{ - [JsonProperty("id")] - public ulong Id { get; set; } - - [JsonProperty("name", NullValueHandling = NullValueHandling.Include)] - public string Name { get; set; } - - [JsonProperty("icon", NullValueHandling = NullValueHandling.Include)] - public string IconHash { get; set; } - - [JsonProperty("owner_user_id")] - public ulong OwnerId { get; set; } - - [JsonProperty("members", NullValueHandling = NullValueHandling.Include)] - public IEnumerable Members { get; set; } - - internal TransportTeam() { } -} - -internal sealed class TransportTeamMember -{ - [JsonProperty("membership_state")] - public int MembershipState { get; set; } - - [JsonProperty("permissions", NullValueHandling = NullValueHandling.Include)] - public IEnumerable Permissions { get; set; } - - [JsonProperty("team_id")] - public ulong TeamId { get; set; } - - [JsonProperty("user", NullValueHandling = NullValueHandling.Include)] - public TransportUser User { get; set; } - - internal TransportTeamMember() { } -} +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace DSharpPlus.Net.Abstractions; + +internal sealed class TransportTeam +{ + [JsonProperty("id")] + public ulong Id { get; set; } + + [JsonProperty("name", NullValueHandling = NullValueHandling.Include)] + public string Name { get; set; } + + [JsonProperty("icon", NullValueHandling = NullValueHandling.Include)] + public string IconHash { get; set; } + + [JsonProperty("owner_user_id")] + public ulong OwnerId { get; set; } + + [JsonProperty("members", NullValueHandling = NullValueHandling.Include)] + public IEnumerable Members { get; set; } + + internal TransportTeam() { } +} + +internal sealed class TransportTeamMember +{ + [JsonProperty("membership_state")] + public int MembershipState { get; set; } + + [JsonProperty("permissions", NullValueHandling = NullValueHandling.Include)] + public IEnumerable Permissions { get; set; } + + [JsonProperty("team_id")] + public ulong TeamId { get; set; } + + [JsonProperty("user", NullValueHandling = NullValueHandling.Include)] + public TransportUser User { get; set; } + + internal TransportTeamMember() { } +} diff --git a/DSharpPlus/Net/Abstractions/Transport/TransportUser.cs b/DSharpPlus/Net/Abstractions/Transport/TransportUser.cs index 24c48ded95..29fb874042 100644 --- a/DSharpPlus/Net/Abstractions/Transport/TransportUser.cs +++ b/DSharpPlus/Net/Abstractions/Transport/TransportUser.cs @@ -1,73 +1,73 @@ -using DSharpPlus.Entities; -using Newtonsoft.Json; - -namespace DSharpPlus.Net.Abstractions; - -internal class TransportUser -{ - [JsonProperty("id")] - public ulong Id { get; internal set; } - - [JsonProperty("username", NullValueHandling = NullValueHandling.Ignore)] - public string Username { get; internal set; } - - [JsonProperty("global_name", NullValueHandling = NullValueHandling.Ignore)] - public string GlobalDisplayName { get; internal set; } - - [JsonProperty("discriminator", NullValueHandling = NullValueHandling.Ignore)] - public string Discriminator { get; set; } - - [JsonProperty("avatar", NullValueHandling = NullValueHandling.Ignore)] - public string AvatarHash { get; internal set; } - - [JsonProperty("banner", NullValueHandling = NullValueHandling.Ignore)] - public string BannerHash { get; internal set; } - - [JsonProperty("accent_color")] - public int? BannerColor { get; internal set; } - - [JsonProperty("bot", NullValueHandling = NullValueHandling.Ignore)] - public bool IsBot { get; internal set; } - - [JsonProperty("mfa_enabled", NullValueHandling = NullValueHandling.Ignore)] - public bool? MfaEnabled { get; internal set; } - - [JsonProperty("verified", NullValueHandling = NullValueHandling.Ignore)] - public bool? Verified { get; internal set; } - - [JsonProperty("email", NullValueHandling = NullValueHandling.Ignore)] - public string Email { get; internal set; } - - [JsonProperty("premium_type", NullValueHandling = NullValueHandling.Ignore)] - public DiscordPremiumType? PremiumType { get; internal set; } - - [JsonProperty("locale", NullValueHandling = NullValueHandling.Ignore)] - public string Locale { get; internal set; } - - [JsonProperty("flags", NullValueHandling = NullValueHandling.Ignore)] - public DiscordUserFlags? OAuthFlags { get; internal set; } - - [JsonProperty("public_flags", NullValueHandling = NullValueHandling.Ignore)] - public DiscordUserFlags? Flags { get; internal set; } - - internal TransportUser() { } - - internal TransportUser(TransportUser other) - { - this.Id = other.Id; - this.Username = other.Username; - this.Discriminator = other.Discriminator; - this.GlobalDisplayName = other.GlobalDisplayName; - this.AvatarHash = other.AvatarHash; - this.BannerHash = other.BannerHash; - this.BannerColor = other.BannerColor; - this.IsBot = other.IsBot; - this.MfaEnabled = other.MfaEnabled; - this.Verified = other.Verified; - this.Email = other.Email; - this.PremiumType = other.PremiumType; - this.Locale = other.Locale; - this.Flags = other.Flags; - this.OAuthFlags = other.OAuthFlags; - } -} +using DSharpPlus.Entities; +using Newtonsoft.Json; + +namespace DSharpPlus.Net.Abstractions; + +internal class TransportUser +{ + [JsonProperty("id")] + public ulong Id { get; internal set; } + + [JsonProperty("username", NullValueHandling = NullValueHandling.Ignore)] + public string Username { get; internal set; } + + [JsonProperty("global_name", NullValueHandling = NullValueHandling.Ignore)] + public string GlobalDisplayName { get; internal set; } + + [JsonProperty("discriminator", NullValueHandling = NullValueHandling.Ignore)] + public string Discriminator { get; set; } + + [JsonProperty("avatar", NullValueHandling = NullValueHandling.Ignore)] + public string AvatarHash { get; internal set; } + + [JsonProperty("banner", NullValueHandling = NullValueHandling.Ignore)] + public string BannerHash { get; internal set; } + + [JsonProperty("accent_color")] + public int? BannerColor { get; internal set; } + + [JsonProperty("bot", NullValueHandling = NullValueHandling.Ignore)] + public bool IsBot { get; internal set; } + + [JsonProperty("mfa_enabled", NullValueHandling = NullValueHandling.Ignore)] + public bool? MfaEnabled { get; internal set; } + + [JsonProperty("verified", NullValueHandling = NullValueHandling.Ignore)] + public bool? Verified { get; internal set; } + + [JsonProperty("email", NullValueHandling = NullValueHandling.Ignore)] + public string Email { get; internal set; } + + [JsonProperty("premium_type", NullValueHandling = NullValueHandling.Ignore)] + public DiscordPremiumType? PremiumType { get; internal set; } + + [JsonProperty("locale", NullValueHandling = NullValueHandling.Ignore)] + public string Locale { get; internal set; } + + [JsonProperty("flags", NullValueHandling = NullValueHandling.Ignore)] + public DiscordUserFlags? OAuthFlags { get; internal set; } + + [JsonProperty("public_flags", NullValueHandling = NullValueHandling.Ignore)] + public DiscordUserFlags? Flags { get; internal set; } + + internal TransportUser() { } + + internal TransportUser(TransportUser other) + { + this.Id = other.Id; + this.Username = other.Username; + this.Discriminator = other.Discriminator; + this.GlobalDisplayName = other.GlobalDisplayName; + this.AvatarHash = other.AvatarHash; + this.BannerHash = other.BannerHash; + this.BannerColor = other.BannerColor; + this.IsBot = other.IsBot; + this.MfaEnabled = other.MfaEnabled; + this.Verified = other.Verified; + this.Email = other.Email; + this.PremiumType = other.PremiumType; + this.Locale = other.Locale; + this.Flags = other.Flags; + this.OAuthFlags = other.OAuthFlags; + } +} diff --git a/DSharpPlus/Net/Abstractions/VoiceStateUpdate.cs b/DSharpPlus/Net/Abstractions/VoiceStateUpdate.cs index 12dbc49184..3f69e2554c 100644 --- a/DSharpPlus/Net/Abstractions/VoiceStateUpdate.cs +++ b/DSharpPlus/Net/Abstractions/VoiceStateUpdate.cs @@ -1,33 +1,33 @@ -using Newtonsoft.Json; - -namespace DSharpPlus.Net.Abstractions; - -/// -/// Represents data for websocket voice state update payload. -/// -internal sealed class VoiceStateUpdate -{ - /// - /// Gets or sets the guild for which the user is updating their voice state. - /// - [JsonProperty("guild_id")] - public ulong GuildId { get; set; } - - /// - /// Gets or sets the channel user wants to connect to. Null if disconnecting. - /// - [JsonProperty("channel_id")] - public ulong? ChannelId { get; set; } - - /// - /// Gets or sets whether the client is muted. - /// - [JsonProperty("self_mute")] - public bool Mute { get; set; } - - /// - /// Gets or sets whether the client is deafened. - /// - [JsonProperty("self_deaf")] - public bool Deafen { get; set; } -} +using Newtonsoft.Json; + +namespace DSharpPlus.Net.Abstractions; + +/// +/// Represents data for websocket voice state update payload. +/// +internal sealed class VoiceStateUpdate +{ + /// + /// Gets or sets the guild for which the user is updating their voice state. + /// + [JsonProperty("guild_id")] + public ulong GuildId { get; set; } + + /// + /// Gets or sets the channel user wants to connect to. Null if disconnecting. + /// + [JsonProperty("channel_id")] + public ulong? ChannelId { get; set; } + + /// + /// Gets or sets whether the client is muted. + /// + [JsonProperty("self_mute")] + public bool Mute { get; set; } + + /// + /// Gets or sets whether the client is deafened. + /// + [JsonProperty("self_deaf")] + public bool Deafen { get; set; } +} diff --git a/DSharpPlus/Net/ConnectionEndpoint.cs b/DSharpPlus/Net/ConnectionEndpoint.cs index a4f88d3276..58676ae034 100644 --- a/DSharpPlus/Net/ConnectionEndpoint.cs +++ b/DSharpPlus/Net/ConnectionEndpoint.cs @@ -1,60 +1,60 @@ -namespace DSharpPlus.Net; - - -/// -/// Represents a network connection endpoint. -/// -public struct ConnectionEndpoint -{ - /// - /// Gets or sets the hostname associated with this endpoint. - /// - public string Hostname { get; set; } - - /// - /// Gets or sets the port associated with this endpoint. - /// - public int Port { get; set; } - - /// - /// Gets or sets the secured status of this connection. - /// - public bool Secured { get; set; } - - /// - /// Creates a new endpoint structure. - /// - /// Hostname to connect to. - /// Port to use for connection. - /// Whether the connection should be secured (https/wss). - public ConnectionEndpoint(string hostname, int port, bool secured = false) - { - this.Hostname = hostname; - this.Port = port; - this.Secured = secured; - } - - /// - /// Gets the hash code of this endpoint. - /// - /// Hash code of this endpoint. - public override readonly int GetHashCode() => 13 + (7 * this.Hostname.GetHashCode()) + (7 * this.Port); - - /// - /// Gets the string representation of this connection endpoint. - /// - /// String representation of this endpoint. - public override readonly string ToString() => $"{this.Hostname}:{this.Port}"; - - internal readonly string ToHttpString() - { - string secure = this.Secured ? "s" : ""; - return $"http{secure}://{this}"; - } - - internal readonly string ToWebSocketString() - { - string secure = this.Secured ? "s" : ""; - return $"ws{secure}://{this}/"; - } -} +namespace DSharpPlus.Net; + + +/// +/// Represents a network connection endpoint. +/// +public struct ConnectionEndpoint +{ + /// + /// Gets or sets the hostname associated with this endpoint. + /// + public string Hostname { get; set; } + + /// + /// Gets or sets the port associated with this endpoint. + /// + public int Port { get; set; } + + /// + /// Gets or sets the secured status of this connection. + /// + public bool Secured { get; set; } + + /// + /// Creates a new endpoint structure. + /// + /// Hostname to connect to. + /// Port to use for connection. + /// Whether the connection should be secured (https/wss). + public ConnectionEndpoint(string hostname, int port, bool secured = false) + { + this.Hostname = hostname; + this.Port = port; + this.Secured = secured; + } + + /// + /// Gets the hash code of this endpoint. + /// + /// Hash code of this endpoint. + public override readonly int GetHashCode() => 13 + (7 * this.Hostname.GetHashCode()) + (7 * this.Port); + + /// + /// Gets the string representation of this connection endpoint. + /// + /// String representation of this endpoint. + public override readonly string ToString() => $"{this.Hostname}:{this.Port}"; + + internal readonly string ToHttpString() + { + string secure = this.Secured ? "s" : ""; + return $"http{secure}://{this}"; + } + + internal readonly string ToWebSocketString() + { + string secure = this.Secured ? "s" : ""; + return $"ws{secure}://{this}/"; + } +} diff --git a/DSharpPlus/Net/Models/ApplicationCommandEditModel.cs b/DSharpPlus/Net/Models/ApplicationCommandEditModel.cs index a4929c7259..92e6cb0ac3 100644 --- a/DSharpPlus/Net/Models/ApplicationCommandEditModel.cs +++ b/DSharpPlus/Net/Models/ApplicationCommandEditModel.cs @@ -1,91 +1,91 @@ -using System; -using System.Collections.Generic; -using DSharpPlus.Entities; - -namespace DSharpPlus.Net.Models; - -public class ApplicationCommandEditModel -{ - /// - /// Sets the command's new name. - /// - public Optional Name - { - internal get => this.name; - set - { - if (value.Value.Length > 32) - { - throw new ArgumentException("Application command name cannot exceed 32 characters.", nameof(value)); - } - - this.name = value; - } - } - - private Optional name; - - /// - /// Sets the command's new description - /// - public Optional Description - { - internal get => this.description; - set - { - if (value.Value.Length > 100) - { - throw new ArgumentException("Application command description cannot exceed 100 characters.", nameof(value)); - } - - this.description = value; - } - } - - private Optional description; - - /// - /// Sets the command's new options. - /// - public Optional> Options { internal get; set; } - - /// - /// Sets whether the command is enabled by default when the application is added to a guild. - /// - public Optional DefaultPermission { internal get; set; } - - /// - /// Sets whether the command can be invoked in DMs. - /// - public Optional AllowDMUsage { internal get; set; } - - /// - /// A dictionary of localized names mapped by locale. - /// - public IReadOnlyDictionary? NameLocalizations { internal get; set; } - - /// - /// A dictionary of localized descriptions mapped by locale. - /// - public IReadOnlyDictionary? DescriptionLocalizations { internal get; set; } - - /// - /// Sets the requisite permissions for the command. - /// - public Optional DefaultMemberPermissions { internal get; set; } - - /// - /// Sets whether this command is age restricted. - /// - public Optional NSFW { internal get; set; } - - /// - /// Interaction context(s) where the command can be used. - /// - public Optional> AllowedContexts { internal get; set; } - - /// - /// Installation context(s) where the command is available. - /// - public Optional> IntegrationTypes { internal get; set; } -} +using System; +using System.Collections.Generic; +using DSharpPlus.Entities; + +namespace DSharpPlus.Net.Models; + +public class ApplicationCommandEditModel +{ + /// + /// Sets the command's new name. + /// + public Optional Name + { + internal get => this.name; + set + { + if (value.Value.Length > 32) + { + throw new ArgumentException("Application command name cannot exceed 32 characters.", nameof(value)); + } + + this.name = value; + } + } + + private Optional name; + + /// + /// Sets the command's new description + /// + public Optional Description + { + internal get => this.description; + set + { + if (value.Value.Length > 100) + { + throw new ArgumentException("Application command description cannot exceed 100 characters.", nameof(value)); + } + + this.description = value; + } + } + + private Optional description; + + /// + /// Sets the command's new options. + /// + public Optional> Options { internal get; set; } + + /// + /// Sets whether the command is enabled by default when the application is added to a guild. + /// + public Optional DefaultPermission { internal get; set; } + + /// + /// Sets whether the command can be invoked in DMs. + /// + public Optional AllowDMUsage { internal get; set; } + + /// + /// A dictionary of localized names mapped by locale. + /// + public IReadOnlyDictionary? NameLocalizations { internal get; set; } + + /// + /// A dictionary of localized descriptions mapped by locale. + /// + public IReadOnlyDictionary? DescriptionLocalizations { internal get; set; } + + /// + /// Sets the requisite permissions for the command. + /// + public Optional DefaultMemberPermissions { internal get; set; } + + /// + /// Sets whether this command is age restricted. + /// + public Optional NSFW { internal get; set; } + + /// + /// Interaction context(s) where the command can be used. + /// + public Optional> AllowedContexts { internal get; set; } + + /// + /// Installation context(s) where the command is available. + /// + public Optional> IntegrationTypes { internal get; set; } +} diff --git a/DSharpPlus/Net/Models/AutoModerationRuleEditModel.cs b/DSharpPlus/Net/Models/AutoModerationRuleEditModel.cs index 65741040b4..e6252c80de 100644 --- a/DSharpPlus/Net/Models/AutoModerationRuleEditModel.cs +++ b/DSharpPlus/Net/Models/AutoModerationRuleEditModel.cs @@ -1,42 +1,42 @@ -using System.Collections.Generic; -using DSharpPlus.Entities; - -namespace DSharpPlus.Net.Models; - -public class AutoModerationRuleEditModel : BaseEditModel -{ - /// - /// The new rule name. - /// - public Optional Name { internal get; set; } - - /// - /// The new rule event type. - /// - public Optional EventType { internal get; set; } - - /// - /// The new rule trigger metadata. - /// - public Optional TriggerMetadata { internal get; set; } - - /// - /// The new rule actions. - /// - public Optional> Actions { internal get; set; } - - /// - /// The new rule status. - /// - public Optional Enable { internal get; set; } - - /// - /// The new rule exempt roles. - /// - public Optional> ExemptRoles { internal get; set; } - - /// - /// The new rule exempt channels. - /// - public Optional> ExemptChannels { internal get; set; } -} +using System.Collections.Generic; +using DSharpPlus.Entities; + +namespace DSharpPlus.Net.Models; + +public class AutoModerationRuleEditModel : BaseEditModel +{ + /// + /// The new rule name. + /// + public Optional Name { internal get; set; } + + /// + /// The new rule event type. + /// + public Optional EventType { internal get; set; } + + /// + /// The new rule trigger metadata. + /// + public Optional TriggerMetadata { internal get; set; } + + /// + /// The new rule actions. + /// + public Optional> Actions { internal get; set; } + + /// + /// The new rule status. + /// + public Optional Enable { internal get; set; } + + /// + /// The new rule exempt roles. + /// + public Optional> ExemptRoles { internal get; set; } + + /// + /// The new rule exempt channels. + /// + public Optional> ExemptChannels { internal get; set; } +} diff --git a/DSharpPlus/Net/Models/BaseEditModel.cs b/DSharpPlus/Net/Models/BaseEditModel.cs index 1877330476..0e11a55d20 100644 --- a/DSharpPlus/Net/Models/BaseEditModel.cs +++ b/DSharpPlus/Net/Models/BaseEditModel.cs @@ -1,10 +1,10 @@ -namespace DSharpPlus.Net.Models; - - -public class BaseEditModel -{ - /// - /// Reason given in audit logs - /// - public string AuditLogReason { internal get; set; } -} +namespace DSharpPlus.Net.Models; + + +public class BaseEditModel +{ + /// + /// Reason given in audit logs + /// + public string AuditLogReason { internal get; set; } +} diff --git a/DSharpPlus/Net/Models/ChannelEditModel.cs b/DSharpPlus/Net/Models/ChannelEditModel.cs index d5a2a389e9..7be9e1a07f 100644 --- a/DSharpPlus/Net/Models/ChannelEditModel.cs +++ b/DSharpPlus/Net/Models/ChannelEditModel.cs @@ -1,110 +1,110 @@ -using System.Collections.Generic; -using DSharpPlus.Entities; - -namespace DSharpPlus.Net.Models; - -public class ChannelEditModel : BaseEditModel -{ - /// - /// Sets the channel's new name. - /// - public string Name { internal get; set; } - - /// - /// Sets the channel's new position. - /// - public int? Position { internal get; set; } - - /// - /// Sets the channel's new topic. - /// - public Optional Topic { internal get; set; } - - /// - /// Sets whether the channel is to be marked as NSFW. - /// - public bool? Nsfw { internal get; set; } - - /// - /// Sets the parent of this channel. - /// This should be channel with set to . - /// - public Optional Parent { internal get; set; } - - /// - /// Sets the voice channel's new bitrate. - /// - public int? Bitrate { internal get; set; } - - /// - /// Sets the voice channel's new user limit. - /// Setting this to 0 will disable the user limit. - /// - public int? Userlimit { internal get; set; } - - /// - /// Sets the channel's new slow mode timeout. - /// Setting this to null or 0 will disable slow mode. - /// - public Optional PerUserRateLimit { internal get; set; } - - /// - /// Sets the voice channel's region override. - /// Setting this to null will set it to automatic. - /// - public Optional RtcRegion { internal get; set; } - - /// - /// Sets the voice channel's video quality. - /// - public DiscordVideoQualityMode? QualityMode { internal get; set; } - - /// - /// Sets the channel's type. - /// This can only be used to convert between text and news channels. - /// - public Optional Type { internal get; set; } - - /// - /// Sets the channel's permission overwrites. - /// - public IEnumerable PermissionOverwrites { internal get; set; } - - /// - /// Sets the channel's auto-archive duration. - /// - public Optional DefaultAutoArchiveDuration { internal get; set; } - - /// - /// Sets the channel's flags (forum channels and posts only). - /// - public Optional Flags { internal get; set; } - - /// - /// Sets the channel's available tags. - /// - public IEnumerable AvailableTags { internal get; set; } - - /// - /// Sets the channel's default reaction, if any. - /// - public Optional DefaultReaction { internal get; set; } - - /// - /// Sets the default slowmode of newly created threads, but does not retroactively update. - /// - /// https://discord.com/developers/docs/resources/channel#modify-channel-json-params-guild-channel - public Optional DefaultThreadRateLimit { internal get; set; } - - /// - /// Sets the default sort order of posts in this channel. - /// - public Optional DefaultSortOrder { internal get; set; } - - /// - /// Sets the default layout of posts in this channel. - /// - public Optional DefaultForumLayout { internal get; set; } - - internal ChannelEditModel() { } -} +using System.Collections.Generic; +using DSharpPlus.Entities; + +namespace DSharpPlus.Net.Models; + +public class ChannelEditModel : BaseEditModel +{ + /// + /// Sets the channel's new name. + /// + public string Name { internal get; set; } + + /// + /// Sets the channel's new position. + /// + public int? Position { internal get; set; } + + /// + /// Sets the channel's new topic. + /// + public Optional Topic { internal get; set; } + + /// + /// Sets whether the channel is to be marked as NSFW. + /// + public bool? Nsfw { internal get; set; } + + /// + /// Sets the parent of this channel. + /// This should be channel with set to . + /// + public Optional Parent { internal get; set; } + + /// + /// Sets the voice channel's new bitrate. + /// + public int? Bitrate { internal get; set; } + + /// + /// Sets the voice channel's new user limit. + /// Setting this to 0 will disable the user limit. + /// + public int? Userlimit { internal get; set; } + + /// + /// Sets the channel's new slow mode timeout. + /// Setting this to null or 0 will disable slow mode. + /// + public Optional PerUserRateLimit { internal get; set; } + + /// + /// Sets the voice channel's region override. + /// Setting this to null will set it to automatic. + /// + public Optional RtcRegion { internal get; set; } + + /// + /// Sets the voice channel's video quality. + /// + public DiscordVideoQualityMode? QualityMode { internal get; set; } + + /// + /// Sets the channel's type. + /// This can only be used to convert between text and news channels. + /// + public Optional Type { internal get; set; } + + /// + /// Sets the channel's permission overwrites. + /// + public IEnumerable PermissionOverwrites { internal get; set; } + + /// + /// Sets the channel's auto-archive duration. + /// + public Optional DefaultAutoArchiveDuration { internal get; set; } + + /// + /// Sets the channel's flags (forum channels and posts only). + /// + public Optional Flags { internal get; set; } + + /// + /// Sets the channel's available tags. + /// + public IEnumerable AvailableTags { internal get; set; } + + /// + /// Sets the channel's default reaction, if any. + /// + public Optional DefaultReaction { internal get; set; } + + /// + /// Sets the default slowmode of newly created threads, but does not retroactively update. + /// + /// https://discord.com/developers/docs/resources/channel#modify-channel-json-params-guild-channel + public Optional DefaultThreadRateLimit { internal get; set; } + + /// + /// Sets the default sort order of posts in this channel. + /// + public Optional DefaultSortOrder { internal get; set; } + + /// + /// Sets the default layout of posts in this channel. + /// + public Optional DefaultForumLayout { internal get; set; } + + internal ChannelEditModel() { } +} diff --git a/DSharpPlus/Net/Models/GuildEditModel.cs b/DSharpPlus/Net/Models/GuildEditModel.cs index 4f99f27a94..17bf6da65b 100644 --- a/DSharpPlus/Net/Models/GuildEditModel.cs +++ b/DSharpPlus/Net/Models/GuildEditModel.cs @@ -1,113 +1,113 @@ -using System.Collections.Generic; -using System.IO; -using DSharpPlus.Entities; - -namespace DSharpPlus.Net.Models; - -public class GuildEditModel : BaseEditModel -{ - /// - /// The new guild name. - /// - public Optional Name { internal get; set; } - - /// - /// The new guild voice region. - /// - public Optional Region { internal get; set; } - - /// - /// The new guild icon. - /// - public Optional Icon { internal get; set; } - - /// - /// The new guild verification level. - /// - public Optional VerificationLevel { internal get; set; } - - /// - /// The new guild default message notification level. - /// - public Optional DefaultMessageNotifications { internal get; set; } - - /// - /// The new guild MFA level. - /// - public Optional MfaLevel { internal get; set; } - - /// - /// The new guild explicit content filter level. - /// - public Optional ExplicitContentFilter { internal get; set; } - - /// - /// The new AFK voice channel. - /// - public Optional AfkChannel { internal get; set; } - - /// - /// The new AFK timeout time in seconds. - /// - public Optional AfkTimeout { internal get; set; } - - /// - /// The new guild owner. - /// - public Optional Owner { internal get; set; } - - /// - /// The new guild splash. - /// - public Optional Splash { internal get; set; } - - /// - /// The new guild system channel. - /// - public Optional SystemChannel { internal get; set; } - - /// - /// The new guild rules channel. - /// - public Optional RulesChannel { internal get; set; } - - /// - /// The new guild public updates channel. - /// - public Optional PublicUpdatesChannel { internal get; set; } - - /// - /// The new guild preferred locale. - /// - public Optional PreferredLocale { internal get; set; } - - /// - /// The new description of the guild - /// - public Optional Description { get; set; } - - /// - /// The new discovery splash image of the guild - /// - public Optional DiscoverySplash { get; set; } - - /// - /// A list of guild features - /// - public Optional> Features { get; set; } - - /// - /// The new banner of the guild - /// - public Optional Banner { get; set; } - - /// - /// The new system channel flags for the guild - /// - public Optional SystemChannelFlags { get; set; } - - internal GuildEditModel() - { - - } -} +using System.Collections.Generic; +using System.IO; +using DSharpPlus.Entities; + +namespace DSharpPlus.Net.Models; + +public class GuildEditModel : BaseEditModel +{ + /// + /// The new guild name. + /// + public Optional Name { internal get; set; } + + /// + /// The new guild voice region. + /// + public Optional Region { internal get; set; } + + /// + /// The new guild icon. + /// + public Optional Icon { internal get; set; } + + /// + /// The new guild verification level. + /// + public Optional VerificationLevel { internal get; set; } + + /// + /// The new guild default message notification level. + /// + public Optional DefaultMessageNotifications { internal get; set; } + + /// + /// The new guild MFA level. + /// + public Optional MfaLevel { internal get; set; } + + /// + /// The new guild explicit content filter level. + /// + public Optional ExplicitContentFilter { internal get; set; } + + /// + /// The new AFK voice channel. + /// + public Optional AfkChannel { internal get; set; } + + /// + /// The new AFK timeout time in seconds. + /// + public Optional AfkTimeout { internal get; set; } + + /// + /// The new guild owner. + /// + public Optional Owner { internal get; set; } + + /// + /// The new guild splash. + /// + public Optional Splash { internal get; set; } + + /// + /// The new guild system channel. + /// + public Optional SystemChannel { internal get; set; } + + /// + /// The new guild rules channel. + /// + public Optional RulesChannel { internal get; set; } + + /// + /// The new guild public updates channel. + /// + public Optional PublicUpdatesChannel { internal get; set; } + + /// + /// The new guild preferred locale. + /// + public Optional PreferredLocale { internal get; set; } + + /// + /// The new description of the guild + /// + public Optional Description { get; set; } + + /// + /// The new discovery splash image of the guild + /// + public Optional DiscoverySplash { get; set; } + + /// + /// A list of guild features + /// + public Optional> Features { get; set; } + + /// + /// The new banner of the guild + /// + public Optional Banner { get; set; } + + /// + /// The new system channel flags for the guild + /// + public Optional SystemChannelFlags { get; set; } + + internal GuildEditModel() + { + + } +} diff --git a/DSharpPlus/Net/Models/MemberEditModel.cs b/DSharpPlus/Net/Models/MemberEditModel.cs index 1f6454d972..3c7a528ea7 100644 --- a/DSharpPlus/Net/Models/MemberEditModel.cs +++ b/DSharpPlus/Net/Models/MemberEditModel.cs @@ -1,44 +1,44 @@ -using System; -using System.Collections.Generic; -using DSharpPlus.Entities; - -namespace DSharpPlus.Net.Models; - -public class MemberEditModel : BaseEditModel -{ - /// - /// New nickname - /// - public Optional Nickname { internal get; set; } - /// - /// New roles - /// - public Optional> Roles { internal get; set; } - /// - /// Whether this user should be muted in voice channels - /// - public Optional Muted { internal get; set; } - /// - /// Whether this user should be deafened - /// - public Optional Deafened { internal get; set; } - /// - /// Voice channel to move this user to, set to null to kick - /// - public Optional VoiceChannel { internal get; set; } - - /// - /// Whether this member should have communication restricted - /// - public Optional CommunicationDisabledUntil { internal get; set; } - - /// - /// Which flags this member should have - /// - public Optional MemberFlags { internal get; set; } - - internal MemberEditModel() - { - - } -} +using System; +using System.Collections.Generic; +using DSharpPlus.Entities; + +namespace DSharpPlus.Net.Models; + +public class MemberEditModel : BaseEditModel +{ + /// + /// New nickname + /// + public Optional Nickname { internal get; set; } + /// + /// New roles + /// + public Optional> Roles { internal get; set; } + /// + /// Whether this user should be muted in voice channels + /// + public Optional Muted { internal get; set; } + /// + /// Whether this user should be deafened + /// + public Optional Deafened { internal get; set; } + /// + /// Voice channel to move this user to, set to null to kick + /// + public Optional VoiceChannel { internal get; set; } + + /// + /// Whether this member should have communication restricted + /// + public Optional CommunicationDisabledUntil { internal get; set; } + + /// + /// Which flags this member should have + /// + public Optional MemberFlags { internal get; set; } + + internal MemberEditModel() + { + + } +} diff --git a/DSharpPlus/Net/Models/MembershipScreeningEditModel.cs b/DSharpPlus/Net/Models/MembershipScreeningEditModel.cs index 35d0bcb55e..24b1e73579 100644 --- a/DSharpPlus/Net/Models/MembershipScreeningEditModel.cs +++ b/DSharpPlus/Net/Models/MembershipScreeningEditModel.cs @@ -1,23 +1,23 @@ -using DSharpPlus.Entities; - -namespace DSharpPlus.Net.Models; - -public class MembershipScreeningEditModel : BaseEditModel -{ - /// - /// Sets whether membership screening should be enabled for this guild - /// - public Optional Enabled { internal get; set; } - - /// - /// Sets the server description shown in the membership screening form - /// - public Optional Description { internal get; set; } - - /// - /// Sets the fields in this membership screening form - /// - public Optional Fields { internal get; set; } - - internal MembershipScreeningEditModel() { } -} +using DSharpPlus.Entities; + +namespace DSharpPlus.Net.Models; + +public class MembershipScreeningEditModel : BaseEditModel +{ + /// + /// Sets whether membership screening should be enabled for this guild + /// + public Optional Enabled { internal get; set; } + + /// + /// Sets the server description shown in the membership screening form + /// + public Optional Description { internal get; set; } + + /// + /// Sets the fields in this membership screening form + /// + public Optional Fields { internal get; set; } + + internal MembershipScreeningEditModel() { } +} diff --git a/DSharpPlus/Net/Models/RoleEditModel.cs b/DSharpPlus/Net/Models/RoleEditModel.cs index 0198616d25..a7c36171e0 100644 --- a/DSharpPlus/Net/Models/RoleEditModel.cs +++ b/DSharpPlus/Net/Models/RoleEditModel.cs @@ -1,47 +1,47 @@ -using System.IO; -using DSharpPlus.Entities; - -namespace DSharpPlus.Net.Models; - -public class RoleEditModel : BaseEditModel -{ - /// - /// New role name - /// - public string Name { internal get; set; } - /// - /// New role permissions - /// - public DiscordPermissions? Permissions { internal get; set; } - /// - /// New role color - /// - public DiscordColor? Color { internal get; set; } - /// - /// Whether new role should be hoisted - /// - public bool? Hoist { internal get; set; } //tbh what is hoist - /// - /// Whether new role should be mentionable - /// - public bool? Mentionable { internal get; set; } - - /// - /// The emoji to set for role role icon; must be unicode. - /// - public DiscordEmoji Emoji { internal get; set; } - - /// - /// The stream to use for uploading a new role icon. - /// - public Stream Icon { internal get; set; } - - internal RoleEditModel() - { - this.Name = null; - this.Permissions = null; - this.Color = null; - this.Hoist = null; - this.Mentionable = null; - } -} +using System.IO; +using DSharpPlus.Entities; + +namespace DSharpPlus.Net.Models; + +public class RoleEditModel : BaseEditModel +{ + /// + /// New role name + /// + public string Name { internal get; set; } + /// + /// New role permissions + /// + public DiscordPermissions? Permissions { internal get; set; } + /// + /// New role color + /// + public DiscordColor? Color { internal get; set; } + /// + /// Whether new role should be hoisted + /// + public bool? Hoist { internal get; set; } //tbh what is hoist + /// + /// Whether new role should be mentionable + /// + public bool? Mentionable { internal get; set; } + + /// + /// The emoji to set for role role icon; must be unicode. + /// + public DiscordEmoji Emoji { internal get; set; } + + /// + /// The stream to use for uploading a new role icon. + /// + public Stream Icon { internal get; set; } + + internal RoleEditModel() + { + this.Name = null; + this.Permissions = null; + this.Color = null; + this.Hoist = null; + this.Mentionable = null; + } +} diff --git a/DSharpPlus/Net/Models/ScheduledGuildEventEditModel.cs b/DSharpPlus/Net/Models/ScheduledGuildEventEditModel.cs index 5672fc0e03..3a8c9eada2 100644 --- a/DSharpPlus/Net/Models/ScheduledGuildEventEditModel.cs +++ b/DSharpPlus/Net/Models/ScheduledGuildEventEditModel.cs @@ -1,60 +1,60 @@ -using System; -using System.IO; -using DSharpPlus.Entities; - -namespace DSharpPlus.Net.Models; - -public class ScheduledGuildEventEditModel : BaseEditModel -{ - /// - /// The new name of the event. - /// - public Optional Name { get; set; } - - /// - /// The new description of the event. - /// - public Optional Description { get; set; } - - /// - /// The new channel ID of the event. This must be set to null for external events. - /// - public Optional Channel { get; set; } - - /// - /// The new privacy of the event. - /// - public Optional PrivacyLevel { get; set; } - - /// - /// The type of the event. - /// - public Optional Type { get; set; } - - /// - /// The new time of the event. - /// - public Optional StartTime { get; set; } - - /// - /// The new end time of the event. - /// - public Optional EndTime { get; set; } - - /// - /// The new metadata of the event. - /// - public Optional Metadata { get; set; } - - /// - /// The new status of the event. - /// - public Optional Status { get; set; } - - /// - /// The cover image for this event. - /// - public Optional CoverImage { get; set; } - - internal ScheduledGuildEventEditModel() { } -} +using System; +using System.IO; +using DSharpPlus.Entities; + +namespace DSharpPlus.Net.Models; + +public class ScheduledGuildEventEditModel : BaseEditModel +{ + /// + /// The new name of the event. + /// + public Optional Name { get; set; } + + /// + /// The new description of the event. + /// + public Optional Description { get; set; } + + /// + /// The new channel ID of the event. This must be set to null for external events. + /// + public Optional Channel { get; set; } + + /// + /// The new privacy of the event. + /// + public Optional PrivacyLevel { get; set; } + + /// + /// The type of the event. + /// + public Optional Type { get; set; } + + /// + /// The new time of the event. + /// + public Optional StartTime { get; set; } + + /// + /// The new end time of the event. + /// + public Optional EndTime { get; set; } + + /// + /// The new metadata of the event. + /// + public Optional Metadata { get; set; } + + /// + /// The new status of the event. + /// + public Optional Status { get; set; } + + /// + /// The cover image for this event. + /// + public Optional CoverImage { get; set; } + + internal ScheduledGuildEventEditModel() { } +} diff --git a/DSharpPlus/Net/Models/StageInstanceEditModel.cs b/DSharpPlus/Net/Models/StageInstanceEditModel.cs index 5be99b5618..b23e5a84cc 100644 --- a/DSharpPlus/Net/Models/StageInstanceEditModel.cs +++ b/DSharpPlus/Net/Models/StageInstanceEditModel.cs @@ -1,16 +1,16 @@ -using DSharpPlus.Entities; - -namespace DSharpPlus.Net.Models; - -public class StageInstanceEditModel : BaseEditModel -{ - /// - /// The new stage instance topic. - /// - public Optional Topic { internal get; set; } - - /// - /// The new stage instance privacy level. - /// - public Optional PrivacyLevel { internal get; set; } -} +using DSharpPlus.Entities; + +namespace DSharpPlus.Net.Models; + +public class StageInstanceEditModel : BaseEditModel +{ + /// + /// The new stage instance topic. + /// + public Optional Topic { internal get; set; } + + /// + /// The new stage instance privacy level. + /// + public Optional PrivacyLevel { internal get; set; } +} diff --git a/DSharpPlus/Net/Models/StickerEditModel.cs b/DSharpPlus/Net/Models/StickerEditModel.cs index c5e3fb0ede..da689c3811 100644 --- a/DSharpPlus/Net/Models/StickerEditModel.cs +++ b/DSharpPlus/Net/Models/StickerEditModel.cs @@ -1,12 +1,12 @@ -using DSharpPlus.Entities; - -namespace DSharpPlus.Net.Models; - -public class StickerEditModel : BaseEditModel -{ - public Optional Name { internal get; set; } - - public Optional Description { internal get; set; } - - public Optional Tags { internal get; set; } -} +using DSharpPlus.Entities; + +namespace DSharpPlus.Net.Models; + +public class StickerEditModel : BaseEditModel +{ + public Optional Name { internal get; set; } + + public Optional Description { internal get; set; } + + public Optional Tags { internal get; set; } +} diff --git a/DSharpPlus/Net/Models/ThreadChannelEditModel.cs b/DSharpPlus/Net/Models/ThreadChannelEditModel.cs index a01168ecc8..fc17f4b039 100644 --- a/DSharpPlus/Net/Models/ThreadChannelEditModel.cs +++ b/DSharpPlus/Net/Models/ThreadChannelEditModel.cs @@ -1,39 +1,39 @@ -using System.Collections.Generic; -using DSharpPlus.Entities; - -namespace DSharpPlus.Net.Models; - -public class ThreadChannelEditModel : ChannelEditModel -{ - /// - /// Sets if the thread is archived - /// - public bool? IsArchived { internal get; set; } - - /// - /// Sets AutoArchiveDuration of the thread - /// - public DiscordAutoArchiveDuration? AutoArchiveDuration { internal get; set; } - - /// - /// Sets if anyone can unarchive a thread - /// - public bool? Locked { internal get; set; } - - /// - /// Sets the applied tags for the thread - /// - public IEnumerable AppliedTags { internal get; set; } - - /// - /// Sets the flags for the channel (Either PINNED or REQUIRE_TAG) - /// - public new DiscordChannelFlags? Flags { internal get; set; } - - /// - /// Sets whether non-moderators can add other non-moderators to a thread. Only available on private threads - /// - public bool? IsInvitable { internal get; set; } - - internal ThreadChannelEditModel() { } -} +using System.Collections.Generic; +using DSharpPlus.Entities; + +namespace DSharpPlus.Net.Models; + +public class ThreadChannelEditModel : ChannelEditModel +{ + /// + /// Sets if the thread is archived + /// + public bool? IsArchived { internal get; set; } + + /// + /// Sets AutoArchiveDuration of the thread + /// + public DiscordAutoArchiveDuration? AutoArchiveDuration { internal get; set; } + + /// + /// Sets if anyone can unarchive a thread + /// + public bool? Locked { internal get; set; } + + /// + /// Sets the applied tags for the thread + /// + public IEnumerable AppliedTags { internal get; set; } + + /// + /// Sets the flags for the channel (Either PINNED or REQUIRE_TAG) + /// + public new DiscordChannelFlags? Flags { internal get; set; } + + /// + /// Sets whether non-moderators can add other non-moderators to a thread. Only available on private threads + /// + public bool? IsInvitable { internal get; set; } + + internal ThreadChannelEditModel() { } +} diff --git a/DSharpPlus/Net/Models/WelcomeScreenEditModel.cs b/DSharpPlus/Net/Models/WelcomeScreenEditModel.cs index 8455be09ef..37646007ec 100644 --- a/DSharpPlus/Net/Models/WelcomeScreenEditModel.cs +++ b/DSharpPlus/Net/Models/WelcomeScreenEditModel.cs @@ -1,22 +1,22 @@ -using System.Collections.Generic; -using DSharpPlus.Entities; - -namespace DSharpPlus.Net.Models; - -public class WelcomeScreenEditModel -{ - /// - /// Sets whether the welcome screen should be enabled. - /// - public Optional Enabled { internal get; set; } - - /// - /// Sets the welcome channels. - /// - public Optional> WelcomeChannels { internal get; set; } - - /// - /// Sets the serer description shown. - /// - public Optional Description { internal get; set; } -} +using System.Collections.Generic; +using DSharpPlus.Entities; + +namespace DSharpPlus.Net.Models; + +public class WelcomeScreenEditModel +{ + /// + /// Sets whether the welcome screen should be enabled. + /// + public Optional Enabled { internal get; set; } + + /// + /// Sets the welcome channels. + /// + public Optional> WelcomeChannels { internal get; set; } + + /// + /// Sets the serer description shown. + /// + public Optional Description { internal get; set; } +} diff --git a/DSharpPlus/Net/Rest/DiscordApiClient.cs b/DSharpPlus/Net/Rest/DiscordApiClient.cs index f25dbe371e..90b31cc825 100644 --- a/DSharpPlus/Net/Rest/DiscordApiClient.cs +++ b/DSharpPlus/Net/Rest/DiscordApiClient.cs @@ -1,7047 +1,7047 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Text; -using System.Threading.Tasks; -using DSharpPlus.Entities; -using DSharpPlus.Entities.AuditLogs; -using DSharpPlus.Exceptions; -using DSharpPlus.Metrics; -using DSharpPlus.Net.Abstractions; -using DSharpPlus.Net.Serialization; - -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; - -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace DSharpPlus.Net; - -// huge credits to dvoraks 8th symphony for being a source of sanity in the trying times of -// fixing this absolute catastrophy up at least somewhat - -public sealed class DiscordApiClient -{ - private const string REASON_HEADER_NAME = "X-Audit-Log-Reason"; - - internal BaseDiscordClient? discord; - internal RestClient rest; - - [ActivatorUtilitiesConstructor] - public DiscordApiClient(RestClient rest) => this.rest = rest; - - // This is for meta-clients, such as the webhook client - internal DiscordApiClient(TimeSpan timeout, ILogger logger) - => this.rest = new(new(), timeout, logger); - - /// - internal RequestMetricsCollection GetRequestMetrics(bool sinceLastCall = false) - => this.rest.GetRequestMetrics(sinceLastCall); - - internal void SetClient(BaseDiscordClient client) - => this.discord = client; - - private DiscordMessage PrepareMessage(JToken msgRaw) - { - TransportUser author = msgRaw["author"]!.ToDiscordObject(); - DiscordMessage message = msgRaw.ToDiscordObject(); - message.Discord = this.discord!; - PopulateMessage(author, message); - - JToken? referencedMsg = msgRaw["referenced_message"]; - if (message.MessageType == DiscordMessageType.Reply && referencedMsg is not null && message.ReferencedMessage is not null) - { - TransportUser referencedAuthor = referencedMsg["author"]!.ToDiscordObject(); - message.ReferencedMessage.Discord = this.discord!; - PopulateMessage(referencedAuthor, message.ReferencedMessage); - } - - return message; - } - - private void PopulateMessage(TransportUser author, DiscordMessage ret) - { - if (ret.Channel is null && ret.Discord is DiscordClient client) - { - ret.Channel = client.InternalGetCachedChannel(ret.ChannelId); - } - - if (ret.guildId is null || !ret.Discord.Guilds.TryGetValue(ret.guildId.Value, out DiscordGuild? guild)) - { - guild = ret.Channel?.Guild; - } - - ret.guildId ??= guild?.Id; - - // I can't think of a case where guildId will never be not null since the guildId is a gateway exclusive - // property, however if that property is added later to the rest api response, this case would be hit. - ret.Channel ??= ret.guildId is null - ? new DiscordDmChannel - { - Id = ret.ChannelId, - Discord = this.discord!, - Type = DiscordChannelType.Private - } - : new DiscordChannel - { - Id = ret.ChannelId, - GuildId = ret.guildId, - Discord = this.discord! - }; - - //If this is a webhook, it shouldn't be in the user cache. - if (author.IsBot && int.Parse(author.Discriminator) == 0) - { - ret.Author = new(author) - { - Discord = this.discord! - }; - } - else - { - // get and cache the user - if (!this.discord!.UserCache.TryGetValue(author.Id, out DiscordUser? user)) - { - user = new DiscordUser(author) - { - Discord = this.discord - }; - } - - this.discord.UserCache[author.Id] = user; - - // get the member object if applicable, if not set the message author to an user - if (guild is not null) - { - if (!guild.Members.TryGetValue(author.Id, out DiscordMember? member)) - { - member = new(user) - { - Discord = this.discord, - guild_id = guild.Id - }; - } - - ret.Author = member; - } - else - { - ret.Author = user!; - } - } - - ret.PopulateMentions(); - - ret.reactions ??= []; - foreach (DiscordReaction reaction in ret.reactions) - { - reaction.Emoji.Discord = this.discord!; - } - - if(ret.MessageSnapshots != null) - { - foreach (DiscordMessageSnapshot snapshot in ret.MessageSnapshots) - { - snapshot.Message?.PopulateMentions(); - } - } - } - - #region Guild - - internal async ValueTask> GetGuildsAsync - ( - int? limit = null, - ulong? before = null, - ulong? after = null, - bool? withCounts = null - ) - { - QueryUriBuilder builder = new($"{Endpoints.USERS}/@me/{Endpoints.GUILDS}"); - - if (limit is not null) - { - if (limit is < 1 or > 200) - { - throw new ArgumentOutOfRangeException(nameof(limit), "Limit must be a number between 1 and 200."); - } - builder.AddParameter("limit", limit.Value.ToString(CultureInfo.InvariantCulture)); - } - - if (before is not null) - { - builder.AddParameter("before", before.Value.ToString(CultureInfo.InvariantCulture)); - } - - if (after is not null) - { - builder.AddParameter("after", after.Value.ToString(CultureInfo.InvariantCulture)); - } - - if (withCounts is not null) - { - builder.AddParameter("with_counts", withCounts.Value.ToString(CultureInfo.InvariantCulture)); - } - - RestRequest request = new() - { - Route = $"/{Endpoints.USERS}/@me/{Endpoints.GUILDS}", - Url = builder.Build(), - Method = HttpMethod.Get - }; - - RestResponse response = await this.rest.ExecuteRequestAsync(request); - - JArray jArray = JArray.Parse(response.Response!); - - List guilds = new(200); - - foreach (JToken token in jArray) - { - DiscordGuild guildRest = token.ToDiscordObject(); - - if (guildRest.roles is not null) - { - foreach (DiscordRole role in guildRest.roles.Values) - { - role.guild_id = guildRest.Id; - role.Discord = this.discord!; - } - } - - guildRest.Discord = this.discord!; - guilds.Add(guildRest); - } - - return guilds; - } - - internal async ValueTask> SearchMembersAsync - ( - ulong guildId, - string name, - int? limit = null - ) - { - QueryUriBuilder builder = new($"{Endpoints.GUILDS}/{guildId}/{Endpoints.MEMBERS}/{Endpoints.SEARCH}"); - builder.AddParameter("query", name); - - if (limit is not null) - { - builder.AddParameter("limit", limit.Value.ToString(CultureInfo.InvariantCulture)); - } - - RestRequest request = new() - { - Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.MEMBERS}/{Endpoints.SEARCH}", - Url = builder.Build(), - Method = HttpMethod.Get - }; - - RestResponse response = await this.rest.ExecuteRequestAsync(request); - - JArray array = JArray.Parse(response.Response!); - IReadOnlyList transportMembers = array.ToDiscordObject>(); - - List members = []; - - foreach (TransportMember transport in transportMembers) - { - DiscordUser usr = new(transport.User) { Discord = this.discord! }; - - this.discord!.UpdateUserCache(usr); - - members.Add(new DiscordMember(transport) { Discord = this.discord, guild_id = guildId }); - } - - return members; - } - - internal async ValueTask GetGuildBanAsync - ( - ulong guildId, - ulong userId - ) - { - RestRequest request = new() - { - Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.BANS}/:user_id", - Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.BANS}/{userId}", - Method = HttpMethod.Get - }; - - RestResponse response = await this.rest.ExecuteRequestAsync(request); - - JObject json = JObject.Parse(response.Response!); - - DiscordBan ban = json.ToDiscordObject(); - - if (!this.discord!.TryGetCachedUserInternal(ban.RawUser.Id, out DiscordUser? user)) - { - user = new DiscordUser(ban.RawUser) { Discord = this.discord }; - user = this.discord.UpdateUserCache(user); - } - - ban.User = user; - - return ban; - } - - internal async ValueTask CreateGuildAsync - ( - string name, - string regionId, - Optional iconb64 = default, - DiscordVerificationLevel? verificationLevel = null, - DiscordDefaultMessageNotifications? defaultMessageNotifications = null, - DiscordSystemChannelFlags? systemChannelFlags = null - ) - { - RestGuildCreatePayload payload = new() - { - Name = name, - RegionId = regionId, - DefaultMessageNotifications = defaultMessageNotifications, - VerificationLevel = verificationLevel, - IconBase64 = iconb64, - SystemChannelFlags = systemChannelFlags - }; - - RestRequest request = new() - { - Route = $"{Endpoints.GUILDS}", - Url = $"{Endpoints.GUILDS}", - Payload = DiscordJson.SerializeObject(payload), - Method = HttpMethod.Post - }; - - RestResponse response = await this.rest.ExecuteRequestAsync(request); - - JObject json = JObject.Parse(response.Response!); - JArray rawMembers = (JArray)json["members"]!; - DiscordGuild guild = json.ToDiscordObject(); - - if (this.discord is DiscordClient dc) - { - // this looks wrong. TODO: investigate double-fired event? - await dc.OnGuildCreateEventAsync(guild, rawMembers, null!); - } - - return guild; - } - - internal async ValueTask CreateGuildFromTemplateAsync - ( - string templateCode, - string name, - Optional iconb64 = default - ) - { - RestGuildCreateFromTemplatePayload payload = new() - { - Name = name, - IconBase64 = iconb64 - }; - - RestRequest request = new() - { - Route = $"{Endpoints.GUILDS}/{Endpoints.TEMPLATES}/:template_code", - Url = $"{Endpoints.GUILDS}/{Endpoints.TEMPLATES}/{templateCode}", - Payload = DiscordJson.SerializeObject(payload), - Method = HttpMethod.Post - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - JObject json = JObject.Parse(res.Response!); - JArray rawMembers = (JArray)json["members"]!; - DiscordGuild guild = json.ToDiscordObject(); - - if (this.discord is DiscordClient dc) - { - await dc.OnGuildCreateEventAsync(guild, rawMembers, null!); - } - - return guild; - } - - internal async ValueTask DeleteGuildAsync - ( - ulong guildId - ) - { - RestRequest request = new() - { - Route = $"{Endpoints.GUILDS}/{guildId}", - Url = $"{Endpoints.GUILDS}/{guildId}", - Method = HttpMethod.Delete - }; - - _ = await this.rest.ExecuteRequestAsync(request); - } - - internal async ValueTask ModifyGuildAsync - ( - ulong guildId, - Optional name = default, - Optional region = default, - Optional verificationLevel = default, - Optional defaultMessageNotifications = default, - Optional mfaLevel = default, - Optional explicitContentFilter = default, - Optional afkChannelId = default, - Optional afkTimeout = default, - Optional iconb64 = default, - Optional ownerId = default, - Optional splashb64 = default, - Optional systemChannelId = default, - Optional banner = default, - Optional description = default, - Optional discoverySplash = default, - Optional> features = default, - Optional preferredLocale = default, - Optional publicUpdatesChannelId = default, - Optional rulesChannelId = default, - Optional systemChannelFlags = default, - string? reason = null - ) - { - RestGuildModifyPayload payload = new() - { - Name = name, - RegionId = region, - VerificationLevel = verificationLevel, - DefaultMessageNotifications = defaultMessageNotifications, - MfaLevel = mfaLevel, - ExplicitContentFilter = explicitContentFilter, - AfkChannelId = afkChannelId, - AfkTimeout = afkTimeout, - IconBase64 = iconb64, - SplashBase64 = splashb64, - OwnerId = ownerId, - SystemChannelId = systemChannelId, - Banner = banner, - Description = description, - DiscoverySplash = discoverySplash, - Features = features, - PreferredLocale = preferredLocale, - PublicUpdatesChannelId = publicUpdatesChannelId, - RulesChannelId = rulesChannelId, - SystemChannelFlags = systemChannelFlags - }; - - RestRequest request = new() - { - Route = $"{Endpoints.GUILDS}/{guildId}", - Url = $"{Endpoints.GUILDS}/{guildId}", - Method = HttpMethod.Patch, - Payload = DiscordJson.SerializeObject(payload), - Headers = string.IsNullOrWhiteSpace(reason) - ? null - : new Dictionary - { - [REASON_HEADER_NAME] = reason - } - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - JObject json = JObject.Parse(res.Response!); - JArray rawMembers = (JArray)json["members"]!; - DiscordGuild guild = json.ToDiscordObject(); - foreach (DiscordRole r in guild.roles.Values) - { - r.guild_id = guild.Id; - } - - if (this.discord is DiscordClient dc) - { - await dc.OnGuildUpdateEventAsync(guild, rawMembers!); - } - - return guild; - } - - internal async ValueTask> GetGuildBansAsync - ( - ulong guildId, - int? limit = null, - ulong? before = null, - ulong? after = null - ) - { - QueryUriBuilder builder = new($"{Endpoints.GUILDS}/{guildId}/{Endpoints.BANS}"); - - if (limit is not null) - { - builder.AddParameter("limit", limit.Value.ToString(CultureInfo.InvariantCulture)); - } - - if (before is not null) - { - builder.AddParameter("before", before.Value.ToString(CultureInfo.InvariantCulture)); - } - - if (after is not null) - { - builder.AddParameter("after", after.Value.ToString(CultureInfo.InvariantCulture)); - } - - RestRequest request = new() - { - Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.BANS}", - Url = builder.Build(), - Method = HttpMethod.Get - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - IEnumerable bansRaw = JsonConvert.DeserializeObject>(res.Response!)! - .Select(xb => - { - if (!this.discord!.TryGetCachedUserInternal(xb.RawUser.Id, out DiscordUser? user)) - { - user = new DiscordUser(xb.RawUser) { Discord = this.discord }; - user = this.discord.UpdateUserCache(user); - } - - xb.User = user; - return xb; - }); - - ReadOnlyCollection bans = new(new List(bansRaw)); - - return bans; - } - - internal async ValueTask CreateGuildBanAsync - ( - ulong guildId, - ulong userId, - int deleteMessageSeconds, - string? reason = null - ) - { - if (deleteMessageSeconds is < 0 or > 604800) - { - throw new ArgumentException("Delete message seconds must be a number between 0 and 604800 (7 Days).", nameof(deleteMessageSeconds)); - } - - QueryUriBuilder builder = new($"{Endpoints.GUILDS}/{guildId}/{Endpoints.BANS}/{userId}"); - - builder.AddParameter("delete_message_seconds", deleteMessageSeconds.ToString(CultureInfo.InvariantCulture)); - - RestRequest request = new() - { - Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.BANS}/:user_id", - Url = builder.Build(), - Method = HttpMethod.Put, - Headers = string.IsNullOrWhiteSpace(reason) - ? null - : new Dictionary - { - [REASON_HEADER_NAME] = reason - } - }; - - _ = await this.rest.ExecuteRequestAsync(request); - } - - internal async ValueTask RemoveGuildBanAsync - ( - ulong guildId, - ulong userId, - string? reason = null - ) - { - RestRequest request = new() - { - Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.BANS}/:user_id", - Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.BANS}/{userId}", - Method = HttpMethod.Delete, - Headers = string.IsNullOrWhiteSpace(reason) - ? null - : new Dictionary - { - [REASON_HEADER_NAME] = reason - } - }; - - _ = await this.rest.ExecuteRequestAsync(request); - } - - internal async ValueTask CreateGuildBulkBanAsync(ulong guildId, IEnumerable userIds, int? deleteMessagesSeconds = null, string? reason = null) - { - if (userIds.TryGetNonEnumeratedCount(out int count) && count > 200) - { - throw new ArgumentException("You can only ban up to 200 users at once."); - } - else if (userIds.Count() > 200) - { - throw new ArgumentException("You can only ban up to 200 users at once."); - } - - if (deleteMessagesSeconds is not null and (< 0 or > 604800)) - { - throw new ArgumentException("Delete message seconds must be a number between 0 and 604800 (7 days).", nameof(deleteMessagesSeconds)); - } - - RestRequest request = new() - { - Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.BULK_BAN}", - Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.BULK_BAN}", - Method = HttpMethod.Post, - Headers = string.IsNullOrWhiteSpace(reason) - ? null - : new Dictionary - { - [REASON_HEADER_NAME] = reason - }, - Payload = DiscordJson.SerializeObject(new RestGuildBulkBanPayload - { - DeleteMessageSeconds = deleteMessagesSeconds, - UserIds = userIds - }) - }; - - RestResponse response = await this.rest.ExecuteRequestAsync(request); - - DiscordBulkBan bulkBan = JsonConvert.DeserializeObject(response.Response!)!; - - List bannedUsers = new(bulkBan.BannedUserIds.Count()); - foreach (ulong userId in bulkBan.BannedUserIds) - { - if (!this.discord!.TryGetCachedUserInternal(userId, out DiscordUser? user)) - { - user = new DiscordUser(new TransportUser { Id = userId }) { Discord = this.discord }; - user = this.discord.UpdateUserCache(user); - } - - bannedUsers.Add(user); - } - bulkBan.BannedUsers = bannedUsers; - - List failedUsers = new(bulkBan.FailedUserIds.Count()); - foreach (ulong userId in bulkBan.FailedUserIds) - { - if (!this.discord!.TryGetCachedUserInternal(userId, out DiscordUser? user)) - { - user = new DiscordUser(new TransportUser { Id = userId }) { Discord = this.discord }; - user = this.discord.UpdateUserCache(user); - } - - failedUsers.Add(user); - } - bulkBan.FailedUsers = failedUsers; - - return bulkBan; - } - - internal async ValueTask LeaveGuildAsync - ( - ulong guildId - ) - { - RestRequest request = new() - { - Route = $"{Endpoints.USERS}/{Endpoints.ME}/{Endpoints.GUILDS}/{guildId}", - Url = $"{Endpoints.USERS}/{Endpoints.ME}/{Endpoints.GUILDS}/{guildId}", - Method = HttpMethod.Delete - }; - - _ = await this.rest.ExecuteRequestAsync(request); - } - - internal async ValueTask AddGuildMemberAsync - ( - ulong guildId, - ulong userId, - string accessToken, - bool? muted = null, - bool? deafened = null, - string? nick = null, - IEnumerable? roles = null - ) - { - RestGuildMemberAddPayload payload = new() - { - AccessToken = accessToken, - Nickname = nick ?? "", - Roles = roles ?? [], - Deaf = deafened ?? false, - Mute = muted ?? false - }; - - RestRequest request = new() - { - Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.MEMBERS}/:user_id", - Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.MEMBERS}/{userId}", - Method = HttpMethod.Put, - Payload = DiscordJson.SerializeObject(payload) - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - if (res.ResponseCode == HttpStatusCode.NoContent) - { - // User was already in the guild, Discord doesn't return the member object in this case - return null; - } - - TransportMember transport = JsonConvert.DeserializeObject(res.Response!)!; - - DiscordUser usr = new(transport.User) { Discord = this.discord! }; - - this.discord!.UpdateUserCache(usr); - - return new DiscordMember(transport) { Discord = this.discord!, guild_id = guildId }; - } - - internal async ValueTask> ListGuildMembersAsync - ( - ulong guildId, - int? limit = null, - ulong? after = null - ) - { - QueryUriBuilder builder = new($"{Endpoints.GUILDS}/{guildId}/{Endpoints.MEMBERS}"); - - if (limit is not null and > 0) - { - builder.AddParameter("limit", limit.Value.ToString(CultureInfo.InvariantCulture)); - } - - if (after is not null) - { - builder.AddParameter("after", after.Value.ToString(CultureInfo.InvariantCulture)); - } - - RestRequest request = new() - { - Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.MEMBERS}", - Url = builder.Build(), - Method = HttpMethod.Get - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - List rawMembers = JsonConvert.DeserializeObject>(res.Response!)!; - return new ReadOnlyCollection(rawMembers); - } - - internal async ValueTask AddGuildMemberRoleAsync - ( - ulong guildId, - ulong userId, - ulong roleId, - string? reason = null - ) - { - RestRequest request = new() - { - Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.MEMBERS}/:user_id/{Endpoints.ROLES}/:role_id", - Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.MEMBERS}/{userId}/{Endpoints.ROLES}/{roleId}", - Method = HttpMethod.Put, - Headers = string.IsNullOrWhiteSpace(reason) - ? null - : new Dictionary - { - [REASON_HEADER_NAME] = reason - } - }; - - _ = await this.rest.ExecuteRequestAsync(request); - } - - internal async ValueTask RemoveGuildMemberRoleAsync - ( - ulong guildId, - ulong userId, - ulong roleId, - string reason - ) - { - RestRequest request = new() - { - Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.MEMBERS}/:user_id/{Endpoints.ROLES}/:role_id", - Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.MEMBERS}/{userId}/{Endpoints.ROLES}/{roleId}", - Method = HttpMethod.Delete, - Headers = string.IsNullOrWhiteSpace(reason) - ? null - : new Dictionary - { - [REASON_HEADER_NAME] = reason - } - }; - - _ = await this.rest.ExecuteRequestAsync(request); - } - - internal async ValueTask ModifyGuildChannelPositionAsync - ( - ulong guildId, - IEnumerable payload, - string? reason = null - ) - { - RestRequest request = new() - { - Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.CHANNELS}", - Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.CHANNELS}", - Method = HttpMethod.Patch, - Payload = DiscordJson.SerializeObject(payload), - Headers = string.IsNullOrWhiteSpace(reason) - ? null - : new Dictionary - { - [REASON_HEADER_NAME] = reason - } - }; - - await this.rest.ExecuteRequestAsync(request); - } - - // TODO: should probably return an IReadOnlyList here, unsure as to the extent of the breaking change - internal async ValueTask ModifyGuildRolePositionsAsync - ( - ulong guildId, - IEnumerable newRolePositions, - string? reason = null - ) - { - RestRequest request = new() - { - Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.ROLES}", - Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.ROLES}", - Method = HttpMethod.Patch, - Payload = DiscordJson.SerializeObject(newRolePositions), - Headers = string.IsNullOrWhiteSpace(reason) - ? null - : new Dictionary - { - [REASON_HEADER_NAME] = reason - } - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - DiscordRole[] ret = JsonConvert.DeserializeObject(res.Response!)!; - foreach (DiscordRole role in ret) - { - role.Discord = this.discord!; - role.guild_id = guildId; - } - - return ret; - } - - internal async ValueTask GetAuditLogsAsync - ( - ulong guildId, - int limit, - ulong? after = null, - ulong? before = null, - ulong? userId = null, - DiscordAuditLogActionType? actionType = null - ) - { - QueryUriBuilder builder = new($"{Endpoints.GUILDS}/{guildId}/{Endpoints.AUDIT_LOGS}"); - - builder.AddParameter("limit", limit.ToString(CultureInfo.InvariantCulture)); - - if (after is not null) - { - builder.AddParameter("after", after.Value.ToString(CultureInfo.InvariantCulture)); - } - - if (before is not null) - { - builder.AddParameter("before", before.Value.ToString(CultureInfo.InvariantCulture)); - } - - if (userId is not null) - { - builder.AddParameter("user_id", userId.Value.ToString(CultureInfo.InvariantCulture)); - } - - if (actionType is not null) - { - builder.AddParameter("action_type", ((int)actionType.Value).ToString(CultureInfo.InvariantCulture)); - } - - RestRequest request = new() - { - Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.AUDIT_LOGS}", - Url = builder.Build(), - Method = HttpMethod.Get - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - return JsonConvert.DeserializeObject(res.Response!)!; - } - - internal async ValueTask GetGuildVanityUrlAsync - ( - ulong guildId - ) - { - RestRequest request = new() - { - Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.VANITY_URL}", - Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.VANITY_URL}", - Method = HttpMethod.Get - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - return JsonConvert.DeserializeObject(res.Response!)!; - } - - internal async ValueTask GetGuildWidgetAsync - ( - ulong guildId - ) - { - RestRequest request = new() - { - Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.WIDGET_JSON}", - Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.WIDGET_JSON}", - Method = HttpMethod.Get - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - // TODO: this should really be cleaned up - JObject json = JObject.Parse(res.Response!); - JArray rawChannels = (JArray)json["channels"]!; - - DiscordWidget ret = json.ToDiscordObject(); - ret.Discord = this.discord!; - ret.Guild = this.discord!.Guilds[guildId]; - - ret.Channels = ret.Guild is null - ? rawChannels.Select(r => new DiscordChannel - { - Id = (ulong)r["id"]!, - Name = r["name"]!.ToString(), - Position = (int)r["position"]! - }).ToList() - : rawChannels.Select(r => - { - DiscordChannel c = ret.Guild.GetChannel((ulong)r["id"]!); - c.Position = (int)r["position"]!; - return c; - }).ToList(); - - return ret; - } - - internal async ValueTask GetGuildWidgetSettingsAsync - ( - ulong guildId - ) - { - RestRequest request = new() - { - Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.WIDGET}", - Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.WIDGET}", - Method = HttpMethod.Get - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - DiscordWidgetSettings ret = JsonConvert.DeserializeObject(res.Response!)!; - ret.Guild = this.discord!.Guilds[guildId]; - - return ret; - } - - internal async ValueTask ModifyGuildWidgetSettingsAsync - ( - ulong guildId, - bool? isEnabled = null, - ulong? channelId = null, - string? reason = null - ) - { - RestGuildWidgetSettingsPayload payload = new() - { - Enabled = isEnabled, - ChannelId = channelId - }; - - RestRequest request = new() - { - Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.WIDGET}", - Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.WIDGET}", - Method = HttpMethod.Patch, - Payload = DiscordJson.SerializeObject(payload), - Headers = reason is null - ? null - : new Dictionary - { - [REASON_HEADER_NAME] = reason - } - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - DiscordWidgetSettings ret = JsonConvert.DeserializeObject(res.Response!)!; - ret.Guild = this.discord!.Guilds[guildId]; - - return ret; - } - - internal async ValueTask> GetGuildTemplatesAsync - ( - ulong guildId - ) - { - RestRequest request = new() - { - Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.TEMPLATES}", - Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.TEMPLATES}", - Method = HttpMethod.Get - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - IEnumerable templates = - JsonConvert.DeserializeObject>(res.Response!)!; - - return new ReadOnlyCollection(new List(templates)); - } - - internal async ValueTask CreateGuildTemplateAsync - ( - ulong guildId, - string name, - string description - ) - { - RestGuildTemplateCreateOrModifyPayload payload = new() - { - Name = name, - Description = description - }; - - RestRequest request = new() - { - Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.TEMPLATES}", - Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.TEMPLATES}", - Method = HttpMethod.Post, - Payload = DiscordJson.SerializeObject(payload) - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - return JsonConvert.DeserializeObject(res.Response!)!; - } - - internal async ValueTask SyncGuildTemplateAsync - ( - ulong guildId, - string templateCode - ) - { - RestRequest request = new() - { - Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.TEMPLATES}/:template_code", - Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.TEMPLATES}/{templateCode}", - Method = HttpMethod.Put - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - return JsonConvert.DeserializeObject(res.Response!)!; - } - - internal async ValueTask ModifyGuildTemplateAsync - ( - ulong guildId, - string templateCode, - string? name = null, - string? description = null - ) - { - RestGuildTemplateCreateOrModifyPayload payload = new() - { - Name = name, - Description = description - }; - - RestRequest request = new() - { - Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.TEMPLATES}/:template_code", - Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.TEMPLATES}/{templateCode}", - Method = HttpMethod.Patch, - Payload = DiscordJson.SerializeObject(payload) - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - return JsonConvert.DeserializeObject(res.Response!)!; - } - - internal async ValueTask DeleteGuildTemplateAsync - ( - ulong guildId, - string templateCode - ) - { - RestRequest request = new() - { - Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.TEMPLATES}/:template_code", - Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.TEMPLATES}/{templateCode}", - Method = HttpMethod.Delete - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - return JsonConvert.DeserializeObject(res.Response!)!; - } - - internal async ValueTask GetGuildMembershipScreeningFormAsync - ( - ulong guildId - ) - { - RestRequest request = new() - { - Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.MEMBER_VERIFICATION}", - Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.MEMBER_VERIFICATION}", - Method = HttpMethod.Get - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - return JsonConvert.DeserializeObject(res.Response!)!; - } - - internal async ValueTask ModifyGuildMembershipScreeningFormAsync - ( - ulong guildId, - Optional enabled = default, - Optional fields = default, - Optional description = default - ) - { - RestGuildMembershipScreeningFormModifyPayload payload = new() - { - Enabled = enabled, - Description = description, - Fields = fields - }; - - RestRequest request = new() - { - Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.MEMBER_VERIFICATION}", - Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.MEMBER_VERIFICATION}", - Method = HttpMethod.Patch, - Payload = DiscordJson.SerializeObject(payload) - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - return JsonConvert.DeserializeObject(res.Response!)!; - } - - internal async ValueTask GetGuildWelcomeScreenAsync - ( - ulong guildId - ) - { - RestRequest request = new() - { - Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.WELCOME_SCREEN}", - Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.WELCOME_SCREEN}", - Method = HttpMethod.Get - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - return JsonConvert.DeserializeObject(res.Response!)!; - } - - internal async ValueTask ModifyGuildWelcomeScreenAsync - ( - ulong guildId, - Optional enabled = default, - Optional> welcomeChannels = default, - Optional description = default, - string? reason = null - ) - { - RestGuildWelcomeScreenModifyPayload payload = new() - { - Enabled = enabled, - WelcomeChannels = welcomeChannels, - Description = description - }; - - RestRequest request = new() - { - Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.WELCOME_SCREEN}", - Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.WELCOME_SCREEN}", - Method = HttpMethod.Patch, - Payload = DiscordJson.SerializeObject(payload), - Headers = reason is null - ? null - : new Dictionary - { - [REASON_HEADER_NAME] = reason - } - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - return JsonConvert.DeserializeObject(res.Response!)!; - } - - internal async ValueTask GetCurrentUserVoiceStateAsync(ulong guildId) - { - RestRequest request = new() - { - Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.VOICE_STATES}/:user_id", - Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.VOICE_STATES}/{Endpoints.ME}", - Method = HttpMethod.Get - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - DiscordVoiceState result = JsonConvert.DeserializeObject(res.Response!)!; - - result.Discord = this.discord!; - - return result; - } - - internal async ValueTask GetUserVoiceStateAsync(ulong guildId, ulong userId) - { - RestRequest request = new() - { - Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.VOICE_STATES}/:user_id", - Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.VOICE_STATES}/{userId}", - Method = HttpMethod.Get - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - DiscordVoiceState result = JsonConvert.DeserializeObject(res.Response!)!; - - result.Discord = this.discord!; - - return result; - } - - internal async ValueTask UpdateCurrentUserVoiceStateAsync - ( - ulong guildId, - ulong channelId, - bool? suppress = null, - DateTimeOffset? requestToSpeakTimestamp = null - ) - { - RestGuildUpdateCurrentUserVoiceStatePayload payload = new() - { - ChannelId = channelId, - Suppress = suppress, - RequestToSpeakTimestamp = requestToSpeakTimestamp - }; - - RestRequest request = new() - { - Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.VOICE_STATES}/@me", - Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.VOICE_STATES}/@me", - Method = HttpMethod.Patch, - Payload = DiscordJson.SerializeObject(payload) - }; - - _ = await this.rest.ExecuteRequestAsync(request); - } - - internal async ValueTask UpdateUserVoiceStateAsync - ( - ulong guildId, - ulong userId, - ulong channelId, - bool? suppress = null - ) - { - RestGuildUpdateUserVoiceStatePayload payload = new() - { - ChannelId = channelId, - Suppress = suppress - }; - - RestRequest request = new() - { - Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.VOICE_STATES}/:user_id", - Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.VOICE_STATES}/{userId}", - Method = HttpMethod.Patch, - Payload = DiscordJson.SerializeObject(payload) - }; - - _ = await this.rest.ExecuteRequestAsync(request); - } - #endregion - - #region Stickers - - internal async ValueTask GetGuildStickerAsync - ( - ulong guildId, - ulong stickerId - ) - { - RestRequest request = new() - { - Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.STICKERS}/:sticker_id", - Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.STICKERS}/{stickerId}", - Method = HttpMethod.Get - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - JObject json = JObject.Parse(res.Response!); - - DiscordMessageSticker ret = json.ToDiscordObject(); - - if (json["user"] is JObject jusr) // Null = Missing stickers perm // - { - TransportUser tsr = jusr.ToDiscordObject(); - DiscordUser usr = new(tsr) { Discord = this.discord! }; - ret.User = usr; - } - - ret.Discord = this.discord!; - return ret; - } - - internal async ValueTask GetStickerAsync - ( - ulong stickerId - ) - { - RestRequest request = new() - { - Route = $"{Endpoints.STICKERS}/:sticker_id", - Url = $"{Endpoints.STICKERS}/{stickerId}", - Method = HttpMethod.Get - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - JObject json = JObject.Parse(res.Response!); - - DiscordMessageSticker ret = json.ToDiscordObject(); - - if (json["user"] is JObject jusr) // Null = Missing stickers perm // - { - TransportUser tsr = jusr.ToDiscordObject(); - DiscordUser usr = new(tsr) { Discord = this.discord! }; - ret.User = usr; - } - - ret.Discord = this.discord!; - return ret; - } - - internal async ValueTask> GetStickerPacksAsync() - { - RestRequest request = new() - { - Route = $"{Endpoints.STICKERPACKS}", - Url = $"{Endpoints.STICKERPACKS}", - Method = HttpMethod.Get - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - JArray json = (JArray)JObject.Parse(res.Response!)["sticker_packs"]!; - DiscordMessageStickerPack[] ret = json.ToDiscordObject(); - - return ret; - } - - internal async ValueTask> GetGuildStickersAsync - ( - ulong guildId - ) - { - RestRequest request = new() - { - Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.STICKERS}", - Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.STICKERS}", - Method = HttpMethod.Get - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - JArray json = JArray.Parse(res.Response!); - - DiscordMessageSticker[] ret = json.ToDiscordObject(); - - for (int i = 0; i < ret.Length; i++) - { - DiscordMessageSticker sticker = ret[i]; - sticker.Discord = this.discord!; - - if (json[i]["user"] is JObject jusr) // Null = Missing stickers perm // - { - TransportUser transportUser = jusr.ToDiscordObject(); - DiscordUser user = new(transportUser) - { - Discord = this.discord! - }; - - // The sticker would've already populated, but this is just to ensure everything is up to date - sticker.User = user; - } - } - - return ret; - } - - internal async ValueTask CreateGuildStickerAsync - ( - ulong guildId, - string name, - string description, - string tags, - DiscordMessageFile file, - string? reason = null - ) - { - MultipartRestRequest request = new() - { - Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.STICKERS}", - Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.STICKERS}", - Method = HttpMethod.Post, - Headers = reason is null - ? null - : new Dictionary() - { - [REASON_HEADER_NAME] = reason - }, - Files = new DiscordMessageFile[] - { - file - }, - Values = new Dictionary() - { - ["name"] = name, - ["description"] = description, - ["tags"] = tags, - } - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - JObject json = JObject.Parse(res.Response!); - - DiscordMessageSticker ret = json.ToDiscordObject(); - - if (json["user"] is JObject rawUser) // Null = Missing stickers perm // - { - TransportUser transportUser = rawUser.ToDiscordObject(); - - DiscordUser user = new(transportUser) - { - Discord = this.discord! - }; - - ret.User = user; - } - - ret.Discord = this.discord!; - - return ret; - } - - internal async ValueTask ModifyStickerAsync - ( - ulong guildId, - ulong stickerId, - Optional name = default, - Optional description = default, - Optional tags = default, - string? reason = null - ) - { - RestStickerModifyPayload payload = new() - { - Name = name, - Description = description, - Tags = tags - }; - - RestRequest request = new() - { - Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.STICKERS}/:sticker_id", - Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.STICKERS}/{stickerId}", - Method = HttpMethod.Patch, - Payload = DiscordJson.SerializeObject(payload), - Headers = reason is null - ? null - : new Dictionary - { - [REASON_HEADER_NAME] = reason - } - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - DiscordMessageSticker ret = JObject.Parse(res.Response!).ToDiscordObject(); - ret.Discord = this.discord!; - - return ret; - } - - internal async ValueTask DeleteStickerAsync - ( - ulong guildId, - ulong stickerId, - string? reason = null - ) - { - RestRequest request = new() - { - Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.STICKERS}/:sticker_id", - Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.STICKERS}/{stickerId}", - Method = HttpMethod.Delete, - Headers = reason is null - ? null - : new Dictionary() - { - [REASON_HEADER_NAME] = reason - } - }; - - await this.rest.ExecuteRequestAsync(request); - } - - #endregion - - #region Channel - internal async ValueTask CreateGuildChannelAsync - ( - ulong guildId, - string name, - DiscordChannelType type, - ulong? parent, - Optional topic, - int? bitrate, - int? userLimit, - IEnumerable? overwrites, - bool? nsfw, - Optional perUserRateLimit, - DiscordVideoQualityMode? qualityMode, - int? position, - string reason, - DiscordAutoArchiveDuration? defaultAutoArchiveDuration, - DefaultReaction? defaultReactionEmoji, - IEnumerable? forumTags, - DiscordDefaultSortOrder? defaultSortOrder - - ) - { - List restOverwrites = []; - if (overwrites != null) - { - foreach (DiscordOverwriteBuilder ow in overwrites) - { - restOverwrites.Add(ow.Build()); - } - } - - RestChannelCreatePayload pld = new() - { - Name = name, - Type = type, - Parent = parent, - Topic = topic, - Bitrate = bitrate, - UserLimit = userLimit, - PermissionOverwrites = restOverwrites, - Nsfw = nsfw, - PerUserRateLimit = perUserRateLimit, - QualityMode = qualityMode, - Position = position, - DefaultAutoArchiveDuration = defaultAutoArchiveDuration, - DefaultReaction = defaultReactionEmoji, - AvailableTags = forumTags, - DefaultSortOrder = defaultSortOrder - }; - - Dictionary headers = []; - if (!string.IsNullOrWhiteSpace(reason)) - { - headers[REASON_HEADER_NAME] = reason; - } - - RestRequest request = new() - { - Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.CHANNELS}", - Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.CHANNELS}", - Method = HttpMethod.Post, - Payload = DiscordJson.SerializeObject(pld), - Headers = headers - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - DiscordChannel ret = JsonConvert.DeserializeObject(res.Response!)!; - ret.Discord = this.discord!; - - foreach (DiscordOverwrite xo in ret.permissionOverwrites) - { - xo.Discord = this.discord!; - xo.channelId = ret.Id; - } - - return ret; - } - - internal async ValueTask ModifyChannelAsync - ( - ulong channelId, - string name, - int? position = null, - Optional topic = default, - bool? nsfw = null, - Optional parent = default, - int? bitrate = null, - int? userLimit = null, - Optional perUserRateLimit = default, - Optional rtcRegion = default, - DiscordVideoQualityMode? qualityMode = null, - Optional type = default, - IEnumerable? permissionOverwrites = null, - Optional flags = default, - IEnumerable? availableTags = null, - Optional defaultAutoArchiveDuration = default, - Optional defaultReactionEmoji = default, - Optional defaultPerUserRatelimit = default, - Optional defaultSortOrder = default, - Optional defaultForumLayout = default, - string? reason = null - ) - { - List? restOverwrites = null; - if (permissionOverwrites is not null) - { - restOverwrites = []; - foreach (DiscordOverwriteBuilder ow in permissionOverwrites) - { - restOverwrites.Add(ow.Build()); - } - } - - RestChannelModifyPayload pld = new() - { - Name = name, - Position = position, - Topic = topic, - Nsfw = nsfw, - Parent = parent, - Bitrate = bitrate, - UserLimit = userLimit, - PerUserRateLimit = perUserRateLimit, - RtcRegion = rtcRegion, - QualityMode = qualityMode, - Type = type, - PermissionOverwrites = restOverwrites, - Flags = flags, - AvailableTags = availableTags, - DefaultAutoArchiveDuration = defaultAutoArchiveDuration, - DefaultReaction = defaultReactionEmoji, - DefaultPerUserRateLimit = defaultPerUserRatelimit, - DefaultForumLayout = defaultForumLayout, - DefaultSortOrder = defaultSortOrder - }; - - Dictionary headers = []; - if (!string.IsNullOrWhiteSpace(reason)) - { - headers[REASON_HEADER_NAME] = reason; - } - - RestRequest request = new() - { - Route = $"{Endpoints.CHANNELS}/{channelId}", - Url = $"{Endpoints.CHANNELS}/{channelId}", - Method = HttpMethod.Patch, - Payload = DiscordJson.SerializeObject(pld), - Headers = headers - }; - - await this.rest.ExecuteRequestAsync(request); - } - - internal async ValueTask ModifyThreadChannelAsync - ( - ulong channelId, - string name, - int? position = null, - Optional topic = default, - bool? nsfw = null, - Optional parent = default, - int? bitrate = null, - int? userLimit = null, - Optional perUserRateLimit = default, - Optional rtcRegion = default, - DiscordVideoQualityMode? qualityMode = null, - Optional type = default, - IEnumerable? permissionOverwrites = null, - bool? isArchived = null, - DiscordAutoArchiveDuration? autoArchiveDuration = null, - bool? locked = null, - IEnumerable? appliedTags = null, - bool? isInvitable = null, - string? reason = null - ) - { - List? restOverwrites = null; - if (permissionOverwrites is not null) - { - restOverwrites = []; - foreach (DiscordOverwriteBuilder ow in permissionOverwrites) - { - restOverwrites.Add(ow.Build()); - } - } - - RestThreadChannelModifyPayload pld = new() - { - Name = name, - Position = position, - Topic = topic, - Nsfw = nsfw, - Parent = parent, - Bitrate = bitrate, - UserLimit = userLimit, - PerUserRateLimit = perUserRateLimit, - RtcRegion = rtcRegion, - QualityMode = qualityMode, - Type = type, - PermissionOverwrites = restOverwrites, - IsArchived = isArchived, - ArchiveDuration = autoArchiveDuration, - Locked = locked, - IsInvitable = isInvitable, - AppliedTags = appliedTags - }; - - Dictionary headers = []; - if (!string.IsNullOrWhiteSpace(reason)) - { - headers.Add(REASON_HEADER_NAME, reason); - } - - RestRequest request = new() - { - Route = $"{Endpoints.CHANNELS}/{channelId}", - Url = $"{Endpoints.CHANNELS}/{channelId}", - Method = HttpMethod.Patch, - Payload = DiscordJson.SerializeObject(pld), - Headers = headers - }; - - await this.rest.ExecuteRequestAsync(request); - } - - internal async ValueTask> GetScheduledGuildEventsAsync - ( - ulong guildId, - bool withUserCounts = false - ) - { - QueryUriBuilder url = new($"{Endpoints.GUILDS}/{guildId}/{Endpoints.EVENTS}"); - url.AddParameter("with_user_count", withUserCounts.ToString()); - - RestRequest request = new() - { - Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.EVENTS}", - Url = url.Build(), - Method = HttpMethod.Get - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - DiscordScheduledGuildEvent[] ret = JsonConvert.DeserializeObject(res.Response!)!; - - foreach (DiscordScheduledGuildEvent? scheduledGuildEvent in ret) - { - scheduledGuildEvent.Discord = this.discord!; - - if (scheduledGuildEvent.Creator is not null) - { - scheduledGuildEvent.Creator.Discord = this.discord!; - } - } - - return ret.AsReadOnly(); - } - - internal async ValueTask CreateScheduledGuildEventAsync - ( - ulong guildId, - string name, - string description, - DateTimeOffset startTime, - DiscordScheduledGuildEventType type, - DiscordScheduledGuildEventPrivacyLevel privacyLevel, - DiscordScheduledGuildEventMetadata? metadata = null, - DateTimeOffset? endTime = null, - ulong? channelId = null, - Stream? image = null, - string? reason = null - ) - { - Dictionary headers = []; - - if (!string.IsNullOrWhiteSpace(reason)) - { - headers[REASON_HEADER_NAME] = reason; - } - - RestScheduledGuildEventCreatePayload pld = new() - { - Name = name, - Description = description, - ChannelId = channelId, - StartTime = startTime, - EndTime = endTime, - Type = type, - PrivacyLevel = privacyLevel, - Metadata = metadata - }; - - if (image is not null) - { - using InlineMediaTool imageTool = new(image); - - pld.CoverImage = imageTool.GetBase64(); - } - - RestRequest request = new() - { - Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.EVENTS}", - Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.EVENTS}", - Method = HttpMethod.Post, - Payload = DiscordJson.SerializeObject(pld), - Headers = headers - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - DiscordScheduledGuildEvent ret = JsonConvert.DeserializeObject(res.Response!)!; - - ret.Discord = this.discord!; - - if (ret.Creator is not null) - { - ret.Creator.Discord = this.discord!; - } - - return ret; - } - - internal async ValueTask DeleteScheduledGuildEventAsync - ( - ulong guildId, - ulong guildScheduledEventId, - string? reason = null - ) - { - RestRequest request = new() - { - Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.EVENTS}/:guild_scheduled_event_id", - Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.EVENTS}/{guildScheduledEventId}", - Method = HttpMethod.Delete, - Headers = new Dictionary - { - [REASON_HEADER_NAME] = reason - } - }; - - await this.rest.ExecuteRequestAsync(request); - } - - internal async ValueTask> GetScheduledGuildEventUsersAsync - ( - ulong guildId, - ulong guildScheduledEventId, - bool withMembers = false, - int limit = 100, - ulong? before = null, - ulong? after = null - ) - { - string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.EVENTS}/:guild_scheduled_event_id/{Endpoints.USERS}"; - - QueryUriBuilder url = new($"{Endpoints.GUILDS}/{guildId}/{Endpoints.EVENTS}/{guildScheduledEventId}/{Endpoints.USERS}"); - - url.AddParameter("with_members", withMembers.ToString()); - - if (limit > 0) - { - url.AddParameter("limit", limit.ToString(CultureInfo.InvariantCulture)); - } - - if (before != null) - { - url.AddParameter("before", before.Value.ToString(CultureInfo.InvariantCulture)); - } - - if (after != null) - { - url.AddParameter("after", after.Value.ToString(CultureInfo.InvariantCulture)); - } - - RestRequest request = new() - { - Route = route, - Url = url.Build(), - Method = HttpMethod.Get - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - JToken jto = JToken.Parse(res.Response!); - - return (jto as JArray ?? jto["users"] as JArray)! - .Select - ( - j => (DiscordUser)j.SelectToken("member")?.ToDiscordObject()! - ?? j.SelectToken("user")!.ToDiscordObject() - ) - .ToArray(); - } - - internal async ValueTask GetScheduledGuildEventAsync - ( - ulong guildId, - ulong guildScheduledEventId - ) - { - string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.EVENTS}/:guild_scheduled_event_id"; - string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.EVENTS}/{guildScheduledEventId}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Get - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - DiscordScheduledGuildEvent ret = JsonConvert.DeserializeObject(res.Response!)!; - - ret.Discord = this.discord!; - - if (ret.Creator is not null) - { - ret.Creator.Discord = this.discord!; - } - - return ret; - } - - internal async ValueTask ModifyScheduledGuildEventAsync - ( - ulong guildId, - ulong guildScheduledEventId, - Optional name = default, - Optional description = default, - Optional channelId = default, - Optional startTime = default, - Optional endTime = default, - Optional type = default, - Optional privacyLevel = default, - Optional metadata = default, - Optional status = default, - Optional coverImage = default, - string? reason = null - ) - { - string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.EVENTS}/:guild_scheduled_event_id"; - string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.EVENTS}/{guildScheduledEventId}"; - - Dictionary headers = []; - if (!string.IsNullOrWhiteSpace(reason)) - { - headers[REASON_HEADER_NAME] = reason; - } - - RestScheduledGuildEventModifyPayload pld = new() - { - Name = name, - Description = description, - ChannelId = channelId, - StartTime = startTime, - EndTime = endTime, - Type = type, - PrivacyLevel = privacyLevel, - Metadata = metadata, - Status = status - }; - - if (coverImage.HasValue) - { - using InlineMediaTool imageTool = new(coverImage.Value); - - pld.CoverImage = imageTool.GetBase64(); - } - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Patch, - Payload = DiscordJson.SerializeObject(pld), - Headers = headers - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - DiscordScheduledGuildEvent ret = JsonConvert.DeserializeObject(res.Response!)!; - - ret.Discord = this.discord!; - - if (ret.Creator is not null) - { - ret.Creator.Discord = this.discord!; - } - - return ret; - } - - internal async ValueTask GetChannelAsync - ( - ulong channelId - ) - { - string route = $"{Endpoints.CHANNELS}/{channelId}"; - string url = $"{Endpoints.CHANNELS}/{channelId}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Get - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - DiscordChannel ret = JsonConvert.DeserializeObject(res.Response!)!; - - // this is really weird, we should consider doing this better - if (ret.IsThread) - { - ret = JsonConvert.DeserializeObject(res.Response!)!; - } - - ret.Discord = this.discord!; - foreach (DiscordOverwrite xo in ret.permissionOverwrites) - { - xo.Discord = this.discord!; - xo.channelId = ret.Id; - } - - return ret; - } - - internal async ValueTask DeleteChannelAsync - ( - ulong channelId, - string reason - ) - { - Dictionary headers = []; - if (!string.IsNullOrWhiteSpace(reason)) - { - headers[REASON_HEADER_NAME] = reason; - } - - RestRequest request = new() - { - Route = $"{Endpoints.CHANNELS}/{channelId}", - Url = new($"{Endpoints.CHANNELS}/{channelId}"), - Method = HttpMethod.Delete, - Headers = headers - }; - - await this.rest.ExecuteRequestAsync(request); - } - - internal async ValueTask GetMessageAsync - ( - ulong channelId, - ulong messageId - ) - { - string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/:message_id"; - string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/{messageId}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Get - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - DiscordMessage ret = PrepareMessage(JObject.Parse(res.Response!)); - - return ret; - } - - internal async ValueTask ForwardMessageAsync(ulong channelId, ulong originChannelId, ulong messageId) - { - RestChannelMessageCreatePayload pld = new() - { - HasContent = false, - MessageReference = new InternalDiscordMessageReference - { - MessageId = messageId, - ChannelId = originChannelId, - Type = DiscordMessageReferenceType.Forward - } - }; - - string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}"; - string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Post, - Payload = DiscordJson.SerializeObject(pld) - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - DiscordMessage ret = PrepareMessage(JObject.Parse(res.Response!)); - - return ret; - } - - internal async ValueTask CreateMessageAsync - ( - ulong channelId, - string? content, - IEnumerable? embeds, - ulong? replyMessageId, - bool mentionReply, - bool failOnInvalidReply, - bool suppressNotifications - ) - { - if (content != null && content.Length > 2000) - { - throw new ArgumentException("Message content length cannot exceed 2000 characters."); - } - - if (!embeds?.Any() ?? true) - { - if (content == null) - { - throw new ArgumentException("You must specify message content or an embed."); - } - - if (content.Length == 0) - { - throw new ArgumentException("Message content must not be empty."); - } - } - - if (embeds is not null) - { - foreach (DiscordEmbed embed in embeds) - { - if (embed.Title?.Length > 256) - { - throw new ArgumentException("Embed title length must not exceed 256 characters."); - } - - if (embed.Description?.Length > 4096) - { - throw new ArgumentException("Embed description length must not exceed 4096 characters."); - } - - if (embed.Fields?.Count > 25) - { - throw new ArgumentException("Embed field count must not exceed 25."); - } - - if (embed.Fields is not null) - { - foreach (DiscordEmbedField field in embed.Fields) - { - if (field.Name.Length > 256) - { - throw new ArgumentException("Embed field name length must not exceed 256 characters."); - } - - if (field.Value.Length > 1024) - { - throw new ArgumentException("Embed field value length must not exceed 1024 characters."); - } - } - } - - if (embed.Footer?.Text.Length > 2048) - { - throw new ArgumentException("Embed footer text length must not exceed 2048 characters."); - } - - if (embed.Author?.Name.Length > 256) - { - throw new ArgumentException("Embed author name length must not exceed 256 characters."); - } - - int totalCharacter = 0; - totalCharacter += embed.Title?.Length ?? 0; - totalCharacter += embed.Description?.Length ?? 0; - totalCharacter += embed.Fields?.Sum(xf => xf.Name.Length + xf.Value.Length) ?? 0; - totalCharacter += embed.Footer?.Text.Length ?? 0; - totalCharacter += embed.Author?.Name.Length ?? 0; - if (totalCharacter > 6000) - { - throw new ArgumentException("Embed total length must not exceed 6000 characters."); - } - - if (embed.Timestamp != null) - { - embed.Timestamp = embed.Timestamp.Value.ToUniversalTime(); - } - } - } - - RestChannelMessageCreatePayload pld = new() - { - HasContent = content != null, - Content = content, - IsTTS = false, - HasEmbed = embeds?.Any() ?? false, - Embeds = embeds, - Flags = suppressNotifications ? DiscordMessageFlags.SuppressNotifications : 0, - }; - - if (replyMessageId != null) - { - pld.MessageReference = new InternalDiscordMessageReference - { - MessageId = replyMessageId, - FailIfNotExists = failOnInvalidReply - }; - } - - if (replyMessageId != null) - { - pld.Mentions = new DiscordMentions(Mentions.All, mentionReply); - } - - string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}"; - string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Post, - Payload = DiscordJson.SerializeObject(pld) - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - DiscordMessage ret = PrepareMessage(JObject.Parse(res.Response!)); - - return ret; - } - - internal async ValueTask CreateMessageAsync - ( - ulong channelId, - DiscordMessageBuilder builder - ) - { - builder.Validate(); - - if (builder.Embeds != null) - { - foreach (DiscordEmbed embed in builder.Embeds) - { - if (embed?.Timestamp != null) - { - embed.Timestamp = embed.Timestamp.Value.ToUniversalTime(); - } - } - } - - RestChannelMessageCreatePayload pld = new() - { - HasContent = builder.Content != null, - Content = builder.Content, - StickersIds = builder.stickers?.Where(s => s != null).Select(s => s.Id).ToArray(), - IsTTS = builder.IsTTS, - HasEmbed = builder.Embeds != null, - Embeds = builder.Embeds, - Components = builder.Components, - Flags = builder.Flags, - Poll = builder.Poll?.BuildInternal(), - }; - - if (builder.ReplyId != null) - { - pld.MessageReference = new InternalDiscordMessageReference { MessageId = builder.ReplyId, FailIfNotExists = builder.FailOnInvalidReply }; - } - - pld.Mentions = new DiscordMentions(builder.Mentions ?? Mentions.None, builder.MentionOnReply); - - if (builder.Files.Count == 0) - { - string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}"; - string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Post, - Payload = DiscordJson.SerializeObject(pld) - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - DiscordMessage ret = PrepareMessage(JObject.Parse(res.Response!)); - - return ret; - } - else - { - Dictionary values = new() - { - ["payload_json"] = DiscordJson.SerializeObject(pld) - }; - - string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}"; - string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}"; - - MultipartRestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Post, - Values = values, - Files = builder.Files - }; - - RestResponse res; - try - { - res = await this.rest.ExecuteRequestAsync(request); - } - finally - { - builder.ResetFileStreamPositions(); - } - - return PrepareMessage(JObject.Parse(res.Response!)); - } - } - - internal async ValueTask> GetGuildChannelsAsync(ulong guildId) - { - string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.CHANNELS}"; - string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.CHANNELS}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Get - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - IEnumerable channelsRaw = JsonConvert.DeserializeObject>(res.Response!)! - .Select - ( - xc => - { - xc.Discord = this.discord!; - return xc; - } - ); - - foreach (DiscordChannel? ret in channelsRaw) - { - foreach (DiscordOverwrite xo in ret.permissionOverwrites) - { - xo.Discord = this.discord!; - xo.channelId = ret.Id; - } - } - - return new ReadOnlyCollection(new List(channelsRaw)); - } - - internal async ValueTask> GetChannelMessagesAsync - ( - ulong channelId, - int limit, - ulong? before = null, - ulong? after = null, - ulong? around = null - ) - { - QueryUriBuilder url = new($"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}"); - if (around is not null) - { - url.AddParameter("around", around?.ToString(CultureInfo.InvariantCulture)); - } - - if (before is not null) - { - url.AddParameter("before", before?.ToString(CultureInfo.InvariantCulture)); - } - - if (after is not null) - { - url.AddParameter("after", after?.ToString(CultureInfo.InvariantCulture)); - } - - if (limit > 0) - { - url.AddParameter("limit", limit.ToString(CultureInfo.InvariantCulture)); - } - - string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}"; - - RestRequest request = new() - { - Route = route, - Url = url.Build(), - Method = HttpMethod.Get - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - JArray msgsRaw = JArray.Parse(res.Response!); - List msgs = []; - foreach (JToken xj in msgsRaw) - { - msgs.Add(PrepareMessage(xj)); - } - - return new ReadOnlyCollection(new List(msgs)); - } - - internal async ValueTask GetChannelMessageAsync - ( - ulong channelId, - ulong messageId - ) - { - string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/:message_id"; - string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/{messageId}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Get - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - DiscordMessage ret = PrepareMessage(JObject.Parse(res.Response!)); - - return ret; - } - - internal async ValueTask EditMessageAsync - ( - ulong channelId, - ulong messageId, - Optional content = default, - Optional> embeds = default, - Optional> mentions = default, - IReadOnlyList? components = null, - IReadOnlyList? files = null, - DiscordMessageFlags? flags = null, - IEnumerable? attachments = null - ) - { - if (embeds.HasValue && embeds.Value != null) - { - foreach (DiscordEmbed embed in embeds.Value) - { - if (embed.Timestamp != null) - { - embed.Timestamp = embed.Timestamp.Value.ToUniversalTime(); - } - } - } - - RestChannelMessageEditPayload pld = new() - { - HasContent = content.HasValue, - Content = content.HasValue ? (string)content : null, - HasEmbed = embeds.HasValue && (embeds.Value?.Any() ?? false), - Embeds = embeds.HasValue && (embeds.Value?.Any() ?? false) ? embeds.Value : null, - Components = components, - Flags = flags, - Attachments = attachments, - Mentions = mentions.HasValue - ? new DiscordMentions - ( - mentions.Value ?? Mentions.None, - mentions.Value?.OfType().Any() ?? false - ) - : null - }; - - string payload = DiscordJson.SerializeObject(pld); - - string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/:message_id"; - string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/{messageId}"; - - RestResponse res; - - if (files is not null) - { - MultipartRestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Patch, - Values = new Dictionary() - { - ["payload_json"] = payload - }, - Files = (IReadOnlyList)files - }; - - res = await this.rest.ExecuteRequestAsync(request); - } - else - { - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Patch, - Payload = payload - }; - - res = await this.rest.ExecuteRequestAsync(request); - } - - DiscordMessage ret = PrepareMessage(JObject.Parse(res.Response!)); - - if (files is not null) - { - foreach (DiscordMessageFile file in files.Where(x => x.ResetPositionTo.HasValue)) - { - file.Stream.Position = file.ResetPositionTo!.Value; - } - } - - return ret; - } - - internal async ValueTask DeleteMessageAsync - ( - ulong channelId, - ulong messageId, - string? reason - ) - { - Dictionary headers = []; - if (!string.IsNullOrWhiteSpace(reason)) - { - headers[REASON_HEADER_NAME] = reason; - } - - string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/:message_id"; - string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/{messageId}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Delete, - Headers = headers - }; - - await this.rest.ExecuteRequestAsync(request); - } - - internal async ValueTask DeleteMessagesAsync - ( - ulong channelId, - IEnumerable messageIds, - string reason - ) - { - RestChannelMessageBulkDeletePayload pld = new() - { - Messages = messageIds - }; - - Dictionary headers = []; - if (!string.IsNullOrWhiteSpace(reason)) - { - headers[REASON_HEADER_NAME] = reason; - } - - string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/{Endpoints.BULK_DELETE}"; - string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/{Endpoints.BULK_DELETE}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Post, - Payload = DiscordJson.SerializeObject(pld), - Headers = headers - }; - - await this.rest.ExecuteRequestAsync(request); - } - - internal async ValueTask> GetChannelInvitesAsync - ( - ulong channelId - ) - { - string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.INVITES}"; - string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.INVITES}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Get - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - IEnumerable invitesRaw = JsonConvert.DeserializeObject>(res.Response!)! - .Select - ( - xi => - { - xi.Discord = this.discord!; - return xi; - } - ); - - return new ReadOnlyCollection(new List(invitesRaw)); - } - - internal async ValueTask CreateChannelInviteAsync - ( - ulong channelId, - int maxAge, - int maxUses, - bool temporary, - bool unique, - string reason, - DiscordInviteTargetType? targetType = null, - ulong? targetUserId = null, - ulong? targetApplicationId = null - ) - { - RestChannelInviteCreatePayload pld = new() - { - MaxAge = maxAge, - MaxUses = maxUses, - Temporary = temporary, - Unique = unique, - TargetType = targetType, - TargetUserId = targetUserId, - TargetApplicationId = targetApplicationId - }; - - Dictionary headers = []; - if (!string.IsNullOrWhiteSpace(reason)) - { - headers[REASON_HEADER_NAME] = reason; - } - - string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.INVITES}"; - string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.INVITES}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Post, - Payload = DiscordJson.SerializeObject(pld), - Headers = headers - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - DiscordInvite ret = JsonConvert.DeserializeObject(res.Response!)!; - ret.Discord = this.discord!; - - return ret; - } - - internal async ValueTask DeleteChannelPermissionAsync - ( - ulong channelId, - ulong overwriteId, - string reason - ) - { - Dictionary headers = []; - if (!string.IsNullOrWhiteSpace(reason)) - { - headers[REASON_HEADER_NAME] = reason; - } - - string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.PERMISSIONS}/:overwrite_id"; - string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.PERMISSIONS}/{overwriteId}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Delete, - Headers = headers - }; - - await this.rest.ExecuteRequestAsync(request); - } - - internal async ValueTask EditChannelPermissionsAsync - ( - ulong channelId, - ulong overwriteId, - DiscordPermissions allow, - DiscordPermissions deny, - string type, - string? reason = null - ) - { - RestChannelPermissionEditPayload pld = new() - { - Type = type switch - { - "role" => 0, - "member" => 1, - _ => throw new InvalidOperationException("Unrecognized permission overwrite target type.") - }, - Allow = allow & DiscordPermissions.All, - Deny = deny & DiscordPermissions.All - }; - - Dictionary headers = []; - if (!string.IsNullOrWhiteSpace(reason)) - { - headers[REASON_HEADER_NAME] = reason; - } - - string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.PERMISSIONS}/:overwrite_id"; - string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.PERMISSIONS}/{overwriteId}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Put, - Payload = DiscordJson.SerializeObject(pld), - Headers = headers - }; - - await this.rest.ExecuteRequestAsync(request); - } - - internal async ValueTask TriggerTypingAsync - ( - ulong channelId - ) - { - string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.TYPING}"; - string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.TYPING}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Post - }; - await this.rest.ExecuteRequestAsync(request); - } - - internal async ValueTask> GetPinnedMessagesAsync - ( - ulong channelId - ) - { - string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.PINS}"; - string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.PINS}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Get - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - JArray msgsRaw = JArray.Parse(res.Response!); - List msgs = []; - foreach (JToken xj in msgsRaw) - { - msgs.Add(PrepareMessage(xj)); - } - - return new ReadOnlyCollection(new List(msgs)); - } - - internal async ValueTask PinMessageAsync - ( - ulong channelId, - ulong messageId - ) - { - string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.PINS}/:message_id"; - string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.PINS}/{messageId}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Put - }; - - await this.rest.ExecuteRequestAsync(request); - } - - internal async ValueTask UnpinMessageAsync - ( - ulong channelId, - ulong messageId - ) - { - string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.PINS}/:message_id"; - string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.PINS}/{messageId}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Delete - }; - await this.rest.ExecuteRequestAsync(request); - } - - internal async ValueTask AddGroupDmRecipientAsync - ( - ulong channelId, - ulong userId, - string accessToken, - string nickname - ) - { - RestChannelGroupDmRecipientAddPayload pld = new() - { - AccessToken = accessToken, - Nickname = nickname - }; - - string route = $"{Endpoints.USERS}/{Endpoints.ME}/{Endpoints.CHANNELS}/{channelId}/{Endpoints.RECIPIENTS}/:user_id"; - string url = $"{Endpoints.USERS}/{Endpoints.ME}/{Endpoints.CHANNELS}/{channelId}/{Endpoints.RECIPIENTS}/{userId}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Put, - Payload = DiscordJson.SerializeObject(pld) - }; - - await this.rest.ExecuteRequestAsync(request); - } - - internal async ValueTask RemoveGroupDmRecipientAsync - ( - ulong channelId, - ulong userId - ) - { - string route = $"{Endpoints.USERS}/{Endpoints.ME}/{Endpoints.CHANNELS}/{channelId}/{Endpoints.RECIPIENTS}/:user_id"; - string url = $"{Endpoints.USERS}/{Endpoints.ME}/{Endpoints.CHANNELS}/{channelId}/{Endpoints.RECIPIENTS}/{userId}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Delete - }; - await this.rest.ExecuteRequestAsync(request); - } - - internal async ValueTask CreateGroupDmAsync - ( - IEnumerable accessTokens, - IDictionary nicks - ) - { - RestUserGroupDmCreatePayload pld = new() - { - AccessTokens = accessTokens, - Nicknames = nicks - }; - - string route = $"{Endpoints.USERS}/{Endpoints.ME}/{Endpoints.CHANNELS}"; - string url = $"{Endpoints.USERS}/{Endpoints.ME}/{Endpoints.CHANNELS}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Post, - Payload = DiscordJson.SerializeObject(pld) - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - DiscordDmChannel ret = JsonConvert.DeserializeObject(res.Response!)!; - ret.Discord = this.discord!; - - return ret; - } - - internal async ValueTask CreateDmAsync - ( - ulong recipientId - ) - { - RestUserDmCreatePayload pld = new() - { - Recipient = recipientId - }; - - string route = $"{Endpoints.USERS}{Endpoints.ME}{Endpoints.CHANNELS}"; - string url = $"{Endpoints.USERS}/{Endpoints.ME}/{Endpoints.CHANNELS}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Post, - Payload = DiscordJson.SerializeObject(pld) - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - DiscordDmChannel ret = JsonConvert.DeserializeObject(res.Response!)!; - ret.Discord = this.discord!; - - if (this.discord is DiscordClient dc) - { - _ = dc.privateChannels.TryAdd(ret.Id, ret); - } - - return ret; - } - - internal async ValueTask FollowChannelAsync - ( - ulong channelId, - ulong webhookChannelId - ) - { - FollowedChannelAddPayload pld = new() - { - WebhookChannelId = webhookChannelId - }; - - string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.FOLLOWERS}"; - string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.FOLLOWERS}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Post, - Payload = DiscordJson.SerializeObject(pld) - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - return JsonConvert.DeserializeObject(res.Response!)!; - } - - internal async ValueTask CrosspostMessageAsync - ( - ulong channelId, - ulong messageId - ) - { - string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/:message_id/{Endpoints.CROSSPOST}"; - string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/{messageId}/{Endpoints.CROSSPOST}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Post - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - return JsonConvert.DeserializeObject(res.Response!)!; - } - - internal async ValueTask CreateStageInstanceAsync - ( - ulong channelId, - string topic, - DiscordStagePrivacyLevel? privacyLevel = null, - string? reason = null - ) - { - Dictionary headers = []; - - if (!string.IsNullOrWhiteSpace(reason)) - { - headers[REASON_HEADER_NAME] = reason; - } - - RestCreateStageInstancePayload pld = new() - { - ChannelId = channelId, - Topic = topic, - PrivacyLevel = privacyLevel - }; - - string route = $"{Endpoints.STAGE_INSTANCES}"; - string url = $"{Endpoints.STAGE_INSTANCES}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Post, - Payload = DiscordJson.SerializeObject(pld), - Headers = headers - }; - - RestResponse response = await this.rest.ExecuteRequestAsync(request); - - DiscordStageInstance stage = JsonConvert.DeserializeObject(response.Response!)!; - stage.Discord = this.discord!; - - return stage; - } - - internal async ValueTask GetStageInstanceAsync - ( - ulong channelId - ) - { - string route = $"{Endpoints.STAGE_INSTANCES}/{channelId}"; - string url = $"{Endpoints.STAGE_INSTANCES}/{channelId}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Get - }; - - RestResponse response = await this.rest.ExecuteRequestAsync(request); - - DiscordStageInstance stage = JsonConvert.DeserializeObject(response.Response!)!; - stage.Discord = this.discord!; - - return stage; - } - - internal async ValueTask ModifyStageInstanceAsync - ( - ulong channelId, - Optional topic = default, - Optional privacyLevel = default, - string? reason = null - ) - { - Dictionary headers = []; - - if (!string.IsNullOrWhiteSpace(reason)) - { - headers[REASON_HEADER_NAME] = reason; - } - - RestModifyStageInstancePayload pld = new() - { - Topic = topic, - PrivacyLevel = privacyLevel - }; - - string route = $"{Endpoints.STAGE_INSTANCES}/{channelId}"; - string url = $"{Endpoints.STAGE_INSTANCES}/{channelId}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Patch, - Payload = DiscordJson.SerializeObject(pld), - Headers = headers - }; - - RestResponse response = await this.rest.ExecuteRequestAsync(request); - DiscordStageInstance stage = JsonConvert.DeserializeObject(response.Response!)!; - stage.Discord = this.discord!; - - return stage; - } - - internal async ValueTask BecomeStageInstanceSpeakerAsync - ( - ulong guildId, - ulong id, - ulong? userId = null, - DateTime? timestamp = null, - bool? suppress = null - ) - { - Dictionary headers = []; - - RestBecomeStageSpeakerInstancePayload pld = new() - { - Suppress = suppress, - ChannelId = id, - RequestToSpeakTimestamp = timestamp - }; - - string user = userId?.ToString() ?? "@me"; - string route = $"/guilds/{guildId}/{Endpoints.VOICE_STATES}/{(userId is null ? "@me" : ":user_id")}"; - string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.VOICE_STATES}/{user}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Patch, - Payload = DiscordJson.SerializeObject(pld), - Headers = headers - }; - - await this.rest.ExecuteRequestAsync(request); - } - - internal async ValueTask DeleteStageInstanceAsync - ( - ulong channelId, - string? reason = null - ) - { - Dictionary headers = []; - if (!string.IsNullOrWhiteSpace(reason)) - { - headers[REASON_HEADER_NAME] = reason; - } - - string route = $"{Endpoints.STAGE_INSTANCES}/{channelId}"; - string url = $"{Endpoints.STAGE_INSTANCES}/{channelId}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Delete, - Headers = headers - }; - - await this.rest.ExecuteRequestAsync(request); - } - - #endregion - - #region Threads - - internal async ValueTask CreateThreadFromMessageAsync - ( - ulong channelId, - ulong messageId, - string name, - DiscordAutoArchiveDuration archiveAfter, - string? reason = null - ) - { - Dictionary headers = []; - if (!string.IsNullOrWhiteSpace(reason)) - { - headers[REASON_HEADER_NAME] = reason; - } - - RestThreadCreatePayload payload = new() - { - Name = name, - ArchiveAfter = archiveAfter - }; - - string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/:message_id/{Endpoints.THREADS}"; - string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/{messageId}/{Endpoints.THREADS}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Post, - Payload = DiscordJson.SerializeObject(payload), - Headers = headers - }; - - RestResponse response = await this.rest.ExecuteRequestAsync(request); - - DiscordThreadChannel thread = JsonConvert.DeserializeObject(response.Response!)!; - thread.Discord = this.discord!; - - return thread; - } - - internal async ValueTask CreateThreadAsync - ( - ulong channelId, - string name, - DiscordAutoArchiveDuration archiveAfter, - DiscordChannelType type, - string? reason = null - ) - { - Dictionary headers = []; - if (!string.IsNullOrWhiteSpace(reason)) - { - headers[REASON_HEADER_NAME] = reason; - } - - RestThreadCreatePayload payload = new() - { - Name = name, - ArchiveAfter = archiveAfter, - Type = type - }; - - string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.THREADS}"; - string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.THREADS}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Post, - Payload = DiscordJson.SerializeObject(payload), - Headers = headers - }; - - RestResponse response = await this.rest.ExecuteRequestAsync(request); - - DiscordThreadChannel thread = JsonConvert.DeserializeObject(response.Response!)!; - thread.Discord = this.discord!; - - return thread; - } - - internal async ValueTask JoinThreadAsync - ( - ulong channelId - ) - { - string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.THREAD_MEMBERS}/{Endpoints.ME}"; - string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.THREAD_MEMBERS}/{Endpoints.ME}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Put - }; - - await this.rest.ExecuteRequestAsync(request); - } - - internal async ValueTask LeaveThreadAsync - ( - ulong channelId - ) - { - string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.THREAD_MEMBERS}/{Endpoints.ME}"; - string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.THREAD_MEMBERS}/{Endpoints.ME}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Delete - }; - - await this.rest.ExecuteRequestAsync(request); - } - - internal async ValueTask GetThreadMemberAsync - ( - ulong channelId, - ulong userId - ) - { - string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.THREAD_MEMBERS}/:user_id"; - string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.THREAD_MEMBERS}/{userId}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Get - }; - - RestResponse response = await this.rest.ExecuteRequestAsync(request); - - DiscordThreadChannelMember ret = JsonConvert.DeserializeObject(response.Response!)!; - - ret.Discord = this.discord!; - - return ret; - } - - internal async ValueTask AddThreadMemberAsync - ( - ulong channelId, - ulong userId - ) - { - string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.THREAD_MEMBERS}/:user_id"; - string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.THREAD_MEMBERS}/{userId}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Put - }; - - await this.rest.ExecuteRequestAsync(request); - } - - internal async ValueTask RemoveThreadMemberAsync - ( - ulong channelId, - ulong userId - ) - { - string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.THREAD_MEMBERS}/:user_id"; - string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.THREAD_MEMBERS}/{userId}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Delete - }; - - await this.rest.ExecuteRequestAsync(request); - } - - internal async ValueTask> ListThreadMembersAsync - ( - ulong channelId - ) - { - string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.THREAD_MEMBERS}"; - string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.THREAD_MEMBERS}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Get - }; - - RestResponse response = await this.rest.ExecuteRequestAsync(request); - - List threadMembers = JsonConvert.DeserializeObject>(response.Response!)!; - - foreach (DiscordThreadChannelMember member in threadMembers) - { - member.Discord = this.discord!; - } - - return new ReadOnlyCollection(threadMembers); - } - - internal async ValueTask ListActiveThreadsAsync - ( - ulong guildId - ) - { - string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.THREADS}/{Endpoints.ACTIVE}"; - string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.THREADS}/{Endpoints.ACTIVE}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Get - }; - - RestResponse response = await this.rest.ExecuteRequestAsync(request); - - ThreadQueryResult result = JsonConvert.DeserializeObject(response.Response!)!; - result.HasMore = false; - - foreach (DiscordThreadChannel thread in result.Threads) - { - thread.Discord = this.discord!; - } - - foreach (DiscordThreadChannelMember member in result.Members) - { - member.Discord = this.discord!; - member.guild_id = guildId; - DiscordThreadChannel? thread = result.Threads.SingleOrDefault(x => x.Id == member.ThreadId); - if (thread is not null) - { - thread.CurrentMember = member; - } - } - - return result; - } - - internal async ValueTask ListPublicArchivedThreadsAsync - ( - ulong guildId, - ulong channelId, - string before, - int limit - ) - { - QueryUriBuilder queryParams = new($"{Endpoints.CHANNELS}/{channelId}/{Endpoints.THREADS}/{Endpoints.ARCHIVED}/{Endpoints.PUBLIC}"); - if (before != null) - { - queryParams.AddParameter("before", before?.ToString(CultureInfo.InvariantCulture)); - } - - if (limit > 0) - { - queryParams.AddParameter("limit", limit.ToString(CultureInfo.InvariantCulture)); - } - - string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.THREADS}/{Endpoints.ARCHIVED}/{Endpoints.PUBLIC}"; - - RestRequest request = new() - { - Route = route, - Url = queryParams.Build(), - Method = HttpMethod.Get, - - }; - - RestResponse response = await this.rest.ExecuteRequestAsync(request); - - ThreadQueryResult result = JsonConvert.DeserializeObject(response.Response!)!; - - foreach (DiscordThreadChannel thread in result.Threads) - { - thread.Discord = this.discord!; - } - - foreach (DiscordThreadChannelMember member in result.Members) - { - member.Discord = this.discord!; - member.guild_id = guildId; - DiscordThreadChannel? thread = result.Threads.SingleOrDefault(x => x.Id == member.ThreadId); - if (thread is not null) - { - thread.CurrentMember = member; - } - } - - return result; - } - - internal async ValueTask ListPrivateArchivedThreadsAsync - ( - ulong guildId, - ulong channelId, - int limit, - string? before = null - ) - { - QueryUriBuilder queryParams = new($"{Endpoints.CHANNELS}/{channelId}/{Endpoints.THREADS}/{Endpoints.ARCHIVED}/{Endpoints.PRIVATE}"); - if (before is not null) - { - queryParams.AddParameter("before", before?.ToString(CultureInfo.InvariantCulture)); - } - - if (limit > 0) - { - queryParams.AddParameter("limit", limit.ToString(CultureInfo.InvariantCulture)); - } - - string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.THREADS}/{Endpoints.ARCHIVED}/{Endpoints.PRIVATE}"; - - RestRequest request = new() - { - Route = route, - Url = queryParams.Build(), - Method = HttpMethod.Get - }; - - RestResponse response = await this.rest.ExecuteRequestAsync(request); - - ThreadQueryResult result = JsonConvert.DeserializeObject(response.Response!)!; - - foreach (DiscordThreadChannel thread in result.Threads) - { - thread.Discord = this.discord!; - } - - foreach (DiscordThreadChannelMember member in result.Members) - { - member.Discord = this.discord!; - member.guild_id = guildId; - DiscordThreadChannel? thread = result.Threads.SingleOrDefault(x => x.Id == member.ThreadId); - if (thread is not null) - { - thread.CurrentMember = member; - } - } - - return result; - } - - internal async ValueTask ListJoinedPrivateArchivedThreadsAsync - ( - ulong guildId, - ulong channelId, - int limit, - ulong? before = null - ) - { - QueryUriBuilder queryParams = new($"{Endpoints.CHANNELS}/{channelId}/{Endpoints.THREADS}/{Endpoints.ARCHIVED}/{Endpoints.PRIVATE}/{Endpoints.ME}"); - if (before is not null) - { - queryParams.AddParameter("before", before?.ToString(CultureInfo.InvariantCulture)); - } - - if (limit > 0) - { - queryParams.AddParameter("limit", limit.ToString(CultureInfo.InvariantCulture)); - } - - string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.USERS}/{Endpoints.ME}/{Endpoints.THREADS}/{Endpoints.ARCHIVED}/{Endpoints.PUBLIC}"; - - RestRequest request = new() - { - Route = route, - Url = queryParams.Build(), - Method = HttpMethod.Get - }; - - RestResponse response = await this.rest.ExecuteRequestAsync(request); - - ThreadQueryResult result = JsonConvert.DeserializeObject(response.Response!)!; - - foreach (DiscordThreadChannel thread in result.Threads) - { - thread.Discord = this.discord!; - } - - foreach (DiscordThreadChannelMember member in result.Members) - { - member.Discord = this.discord!; - member.guild_id = guildId; - DiscordThreadChannel? thread = result.Threads.SingleOrDefault(x => x.Id == member.ThreadId); - if (thread is not null) - { - thread.CurrentMember = member; - } - } - - return result; - } - - #endregion - - #region Member - internal ValueTask GetCurrentUserAsync() - => GetUserAsync("@me"); - - internal ValueTask GetUserAsync(ulong userId) - => GetUserAsync(userId.ToString(CultureInfo.InvariantCulture)); - - internal async ValueTask GetUserAsync(string userId) - { - string route = $"{Endpoints.USERS}/:user_id"; - string url = $"{Endpoints.USERS}/{userId}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Get - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - TransportUser userRaw = JsonConvert.DeserializeObject(res.Response!)!; - DiscordUser user = new(userRaw) - { - Discord = this.discord! - }; - - return user; - } - - internal async ValueTask GetGuildMemberAsync - ( - ulong guildId, - ulong userId - ) - { - string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.MEMBERS}/:user_id"; - string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.MEMBERS}/{userId}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Get - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - TransportMember tm = JsonConvert.DeserializeObject(res.Response!)!; - - DiscordUser usr = new(tm.User) - { - Discord = this.discord! - }; - _ = this.discord!.UpdateUserCache(usr); - - return new DiscordMember(tm) - { - Discord = this.discord, - guild_id = guildId - }; - } - - internal async ValueTask RemoveGuildMemberAsync - ( - ulong guildId, - ulong userId, - string? reason = null - ) - { - string url = new($"{Endpoints.GUILDS}/{guildId}/{Endpoints.MEMBERS}/{userId}"); - string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.MEMBERS}/:user_id"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Delete, - Headers = string.IsNullOrWhiteSpace(reason) - ? null - : new Dictionary - { - [REASON_HEADER_NAME] = reason - } - }; - - await this.rest.ExecuteRequestAsync(request); - } - - internal async ValueTask ModifyCurrentUserAsync - ( - string username, - Optional base64Avatar = default, - Optional base64Banner = default - ) - { - RestUserUpdateCurrentPayload pld = new() - { - Username = username, - AvatarBase64 = base64Avatar.HasValue ? base64Avatar.Value : null, - AvatarSet = base64Avatar.HasValue, - BannerBase64 = base64Banner.HasValue ? base64Banner.Value : null, - BannerSet = base64Banner.HasValue - }; - - string route = $"{Endpoints.USERS}/{Endpoints.ME}"; - string url = $"{Endpoints.USERS}/{Endpoints.ME}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Patch, - Payload = DiscordJson.SerializeObject(pld) - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - TransportUser userRaw = JsonConvert.DeserializeObject(res.Response!)!; - - return userRaw; - } - - internal async ValueTask> GetCurrentUserGuildsAsync - ( - int limit = 100, - ulong? before = null, - ulong? after = null - ) - { - string route = $"{Endpoints.USERS}/{Endpoints.ME}/{Endpoints.GUILDS}"; - QueryUriBuilder url = new($"{Endpoints.USERS}/{Endpoints.ME}/{Endpoints.GUILDS}"); - url.AddParameter($"limit", limit.ToString(CultureInfo.InvariantCulture)); - - if (before != null) - { - url.AddParameter("before", before.Value.ToString(CultureInfo.InvariantCulture)); - } - - if (after != null) - { - url.AddParameter("after", after.Value.ToString(CultureInfo.InvariantCulture)); - } - - RestRequest request = new() - { - Route = route, - Url = url.Build(), - Method = HttpMethod.Get - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - if (this.discord is DiscordClient) - { - IEnumerable guildsRaw = JsonConvert.DeserializeObject>(res.Response!)!; - IEnumerable guilds = guildsRaw.Select - ( - xug => (this.discord as DiscordClient)?.guilds[xug.Id] - ) - .Where(static guild => guild is not null)!; - return new ReadOnlyCollection(new List(guilds)); - } - else - { - List guildsRaw = [.. JsonConvert.DeserializeObject>(res.Response!)!]; - foreach (DiscordGuild guild in guildsRaw) - { - guild.Discord = this.discord!; - - } - return new ReadOnlyCollection(guildsRaw); - } - } - - internal async ValueTask GetCurrentUserGuildMemberAsync - ( - ulong guildId - ) - { - string route = $"{Endpoints.USERS}/{Endpoints.ME}/{Endpoints.GUILDS}/{guildId}/member"; - - RestRequest request = new() - { - Route = route, - Url = route, - Method = HttpMethod.Get - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - TransportMember tm = JsonConvert.DeserializeObject(res.Response!)!; - - DiscordUser usr = new(tm.User) - { - Discord = this.discord! - }; - _ = this.discord!.UpdateUserCache(usr); - - return new DiscordMember(tm) - { - Discord = this.discord, - guild_id = guildId - }; - } - - internal async ValueTask ModifyGuildMemberAsync - ( - ulong guildId, - ulong userId, - Optional nick = default, - Optional> roleIds = default, - Optional mute = default, - Optional deaf = default, - Optional voiceChannelId = default, - Optional communicationDisabledUntil = default, - Optional memberFlags = default, - string? reason = null - ) - { - Dictionary headers = []; - if (!string.IsNullOrWhiteSpace(reason)) - { - headers[REASON_HEADER_NAME] = reason; - } - - RestGuildMemberModifyPayload pld = new() - { - Nickname = nick, - RoleIds = roleIds, - Deafen = deaf, - Mute = mute, - VoiceChannelId = voiceChannelId, - CommunicationDisabledUntil = communicationDisabledUntil - }; - - string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.MEMBERS}/:user_id"; - string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.MEMBERS}/{userId}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Patch, - Payload = DiscordJson.SerializeObject(pld), - Headers = headers - }; - - await this.rest.ExecuteRequestAsync(request); - } - - internal async ValueTask ModifyCurrentMemberAsync - ( - ulong guildId, - string nick, - string? reason = null - ) - { - Dictionary headers = []; - if (!string.IsNullOrWhiteSpace(reason)) - { - headers[REASON_HEADER_NAME] = reason; - } - - RestGuildMemberModifyPayload pld = new() - { - Nickname = nick - }; - - string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.MEMBERS}/{Endpoints.ME}"; - string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.MEMBERS}/{Endpoints.ME}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Patch, - Payload = DiscordJson.SerializeObject(pld), - Headers = headers - }; - - await this.rest.ExecuteRequestAsync(request); - } - #endregion - - #region Roles - internal async ValueTask GetGuildRoleAsync - ( - ulong guildId, - ulong roleId - ) - { - string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.ROLES}/:role_id"; - string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.ROLES}/{roleId}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Get - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - DiscordRole role = JsonConvert.DeserializeObject(res.Response!)!; - role.Discord = this.discord!; - role.guild_id = guildId; - - return role; - } - - internal async ValueTask> GetGuildRolesAsync - ( - ulong guildId - ) - { - string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.ROLES}"; - string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.ROLES}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Get - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - IEnumerable rolesRaw = JsonConvert.DeserializeObject>(res.Response!)! - .Select - ( - xr => - { - xr.Discord = this.discord!; - xr.guild_id = guildId; - return xr; - } - ); - - return new ReadOnlyCollection(new List(rolesRaw)); - } - - internal async ValueTask GetGuildAsync - ( - ulong guildId, - bool? withCounts - ) - { - QueryUriBuilder urlparams = new($"{Endpoints.GUILDS}/{guildId}"); - if (withCounts.HasValue) - { - urlparams.AddParameter("with_counts", withCounts?.ToString()); - } - - string route = $"{Endpoints.GUILDS}/{guildId}"; - - RestRequest request = new() - { - Route = route, - Url = urlparams.Build(), - Method = HttpMethod.Get - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - JObject json = JObject.Parse(res.Response!); - JArray rawMembers = (JArray)json["members"]!; - DiscordGuild guildRest = json.ToDiscordObject(); - foreach (DiscordRole role in guildRest.roles.Values) - { - role.guild_id = guildRest.Id; - } - - if (this.discord is DiscordClient discordClient) - { - await discordClient.OnGuildUpdateEventAsync(guildRest, rawMembers); - return discordClient.guilds[guildRest.Id]; - } - else - { - guildRest.Discord = this.discord!; - return guildRest; - } - } - - internal async ValueTask ModifyGuildRoleAsync - ( - ulong guildId, - ulong roleId, - string? name = null, - DiscordPermissions? permissions = null, - int? color = null, - bool? hoist = null, - bool? mentionable = null, - Stream? icon = null, - string? emoji = null, - string? reason = null - ) - { - string? image = null; - - if (icon != null) - { - using InlineMediaTool it = new(icon); - image = it.GetBase64(); - } - - RestGuildRolePayload pld = new() - { - Name = name, - Permissions = permissions & DiscordPermissions.All, - Color = color, - Hoist = hoist, - Mentionable = mentionable, - Emoji = emoji, - Icon = image - }; - - Dictionary headers = []; - if (!string.IsNullOrWhiteSpace(reason)) - { - headers[REASON_HEADER_NAME] = reason; - } - - string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.ROLES}/:role_id"; - string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.ROLES}/{roleId}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Patch, - Payload = DiscordJson.SerializeObject(pld), - Headers = headers - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - DiscordRole ret = JsonConvert.DeserializeObject(res.Response!)!; - ret.Discord = this.discord!; - ret.guild_id = guildId; - - return ret; - } - - internal async ValueTask DeleteRoleAsync - ( - ulong guildId, - ulong roleId, - string? reason = null - ) - { - Dictionary headers = []; - if (!string.IsNullOrWhiteSpace(reason)) - { - headers[REASON_HEADER_NAME] = reason; - } - - string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.ROLES}/:role_id"; - string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.ROLES}/{roleId}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Delete, - Headers = headers - }; - - await this.rest.ExecuteRequestAsync(request); - } - - internal async ValueTask CreateGuildRoleAsync - ( - ulong guildId, - string name, - DiscordPermissions? permissions = null, - int? color = null, - bool? hoist = null, - bool? mentionable = null, - Stream? icon = null, - string? emoji = null, - string? reason = null - ) - { - string? image = null; - - if (icon != null) - { - using InlineMediaTool it = new(icon); - image = it.GetBase64(); - } - - RestGuildRolePayload pld = new() - { - Name = name, - Permissions = permissions & DiscordPermissions.All, - Color = color, - Hoist = hoist, - Mentionable = mentionable, - Emoji = emoji, - Icon = image - }; - - Dictionary headers = []; - if (!string.IsNullOrWhiteSpace(reason)) - { - headers[REASON_HEADER_NAME] = reason; - } - - string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.ROLES}"; - string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.ROLES}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Post, - Payload = DiscordJson.SerializeObject(pld), - Headers = headers - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - DiscordRole ret = JsonConvert.DeserializeObject(res.Response!)!; - ret.Discord = this.discord!; - ret.guild_id = guildId; - - return ret; - } - #endregion - - #region Prune - internal async ValueTask GetGuildPruneCountAsync - ( - ulong guildId, - int days, - IEnumerable? includeRoles = null - ) - { - if (days is < 0 or > 30) - { - throw new ArgumentException("Prune inactivity days must be a number between 0 and 30.", nameof(days)); - } - - QueryUriBuilder urlparams = new($"{Endpoints.GUILDS}/{guildId}/{Endpoints.PRUNE}"); - urlparams.AddParameter("days", days.ToString(CultureInfo.InvariantCulture)); - - StringBuilder sb = new(); - - if (includeRoles is not null) - { - ulong[] roleArray = includeRoles.ToArray(); - int roleArrayCount = roleArray.Length; - - for (int i = 0; i < roleArrayCount; i++) - { - sb.Append($"&include_roles={roleArray[i]}"); - } - } - - string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.PRUNE}"; - - RestRequest request = new() - { - Route = route, - Url = urlparams.Build(), - Method = HttpMethod.Get - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - RestGuildPruneResultPayload pruned = JsonConvert.DeserializeObject(res.Response!)!; - - return pruned.Pruned!.Value; - } - - internal async ValueTask BeginGuildPruneAsync - ( - ulong guildId, - int days, - bool computePruneCount, - IEnumerable? includeRoles = null, - string? reason = null - ) - { - if (days is < 0 or > 30) - { - throw new ArgumentException("Prune inactivity days must be a number between 0 and 30.", nameof(days)); - } - - QueryUriBuilder urlparams = new($"{Endpoints.GUILDS}/{guildId}/{Endpoints.PRUNE}"); - urlparams.AddParameter("days", days.ToString(CultureInfo.InvariantCulture)); - urlparams.AddParameter("compute_prune_count", computePruneCount.ToString()); - - StringBuilder sb = new(); - - if (includeRoles is not null) - { - foreach (ulong id in includeRoles) - { - sb.Append($"&include_roles={id}"); - } - } - - Dictionary headers = []; - if (string.IsNullOrWhiteSpace(reason)) - { - headers[REASON_HEADER_NAME] = reason!; - } - - string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.PRUNE}"; - - RestRequest request = new() - { - Route = route, - Url = urlparams.Build() + sb.ToString(), - Method = HttpMethod.Post, - Headers = headers - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - RestGuildPruneResultPayload pruned = JsonConvert.DeserializeObject(res.Response!)!; - - return pruned.Pruned; - } - #endregion - - #region GuildVarious - internal async ValueTask GetTemplateAsync - ( - string code - ) - { - string route = $"{Endpoints.GUILDS}/{Endpoints.TEMPLATES}/:code"; - string url = $"{Endpoints.GUILDS}/{Endpoints.TEMPLATES}/{code}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Get - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - DiscordGuildTemplate templatesRaw = JsonConvert.DeserializeObject(res.Response!)!; - - return templatesRaw; - } - - internal async ValueTask> GetGuildIntegrationsAsync - ( - ulong guildId - ) - { - string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.INTEGRATIONS}"; - string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.INTEGRATIONS}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Get - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - IEnumerable integrationsRaw = - JsonConvert.DeserializeObject>(res.Response!)! - .Select - ( - xi => - { - xi.Discord = this.discord!; - return xi; - } - ); - - return new ReadOnlyCollection(new List(integrationsRaw)); - } - - internal async ValueTask GetGuildPreviewAsync - ( - ulong guildId - ) - { - string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.PREVIEW}"; - string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.PREVIEW}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Get - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - DiscordGuildPreview ret = JsonConvert.DeserializeObject(res.Response!)!; - ret.Discord = this.discord!; - - return ret; - } - - internal async ValueTask CreateGuildIntegrationAsync - ( - ulong guildId, - string type, - ulong id - ) - { - RestGuildIntegrationAttachPayload pld = new() - { - Type = type, - Id = id - }; - - string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.INTEGRATIONS}"; - string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.INTEGRATIONS}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Post, - Payload = DiscordJson.SerializeObject(pld) - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - DiscordIntegration ret = JsonConvert.DeserializeObject(res.Response!)!; - ret.Discord = this.discord!; - - return ret; - } - - internal async ValueTask ModifyGuildIntegrationAsync - ( - ulong guildId, - ulong integrationId, - int expireBehaviour, - int expireGracePeriod, - bool enableEmoticons - ) - { - RestGuildIntegrationModifyPayload pld = new() - { - ExpireBehavior = expireBehaviour, - ExpireGracePeriod = expireGracePeriod, - EnableEmoticons = enableEmoticons - }; - - string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.INTEGRATIONS}/:integration_id"; - string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.INTEGRATIONS}/{integrationId}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Patch, - Payload = DiscordJson.SerializeObject(pld) - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - DiscordIntegration ret = JsonConvert.DeserializeObject(res.Response!)!; - ret.Discord = this.discord!; - - return ret; - } - - internal async ValueTask DeleteGuildIntegrationAsync - ( - ulong guildId, - ulong integrationId, - string? reason = null - ) - { - Dictionary headers = []; - if (string.IsNullOrWhiteSpace(reason)) - { - headers[REASON_HEADER_NAME] = reason!; - } - - string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.INTEGRATIONS}/:integration_id"; - string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.INTEGRATIONS}/{integrationId}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Delete, - Headers = headers - }; - - await this.rest.ExecuteRequestAsync(request); - } - - internal async ValueTask SyncGuildIntegrationAsync - ( - ulong guildId, - ulong integrationId - ) - { - string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.INTEGRATIONS}/:integration_id/{Endpoints.SYNC}"; - string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.INTEGRATIONS}/{integrationId}/{Endpoints.SYNC}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Post - }; - - await this.rest.ExecuteRequestAsync(request); - } - - internal async ValueTask> GetGuildVoiceRegionsAsync - ( - ulong guildId - ) - { - string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.REGIONS}"; - string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.REGIONS}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Get - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - IEnumerable regionsRaw = JsonConvert.DeserializeObject>(res.Response!)!; - - return new ReadOnlyCollection(new List(regionsRaw)); - } - - internal async ValueTask> GetGuildInvitesAsync - ( - ulong guildId - ) - { - string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.INVITES}"; - string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.INVITES}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Get - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - IEnumerable invitesRaw = - JsonConvert.DeserializeObject>(res.Response!)! - .Select - ( - xi => - { - xi.Discord = this.discord!; - return xi; - } - ); - - return new ReadOnlyCollection(new List(invitesRaw)); - } - #endregion - - #region Invite - internal async ValueTask GetInviteAsync - ( - string inviteCode, - bool? withCounts = null, - bool? withExpiration = null - ) - { - Dictionary urlparams = []; - if (withCounts.HasValue) - { - urlparams["with_counts"] = withCounts?.ToString()!; - urlparams["with_expiration"] = withExpiration?.ToString()!; - } - - string route = $"{Endpoints.INVITES}/:invite_code"; - string url = $"{Endpoints.INVITES}/{inviteCode}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Get - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - DiscordInvite ret = JsonConvert.DeserializeObject(res.Response!)!; - ret.Discord = this.discord!; - - return ret; - } - - internal async ValueTask DeleteInviteAsync - ( - string inviteCode, - string? reason = null - ) - { - Dictionary headers = []; - if (!string.IsNullOrWhiteSpace(reason)) - { - headers[REASON_HEADER_NAME] = reason; - } - - string route = $"{Endpoints.INVITES}/:invite_code"; - string url = $"{Endpoints.INVITES}/{inviteCode}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Delete, - Headers = headers - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - DiscordInvite ret = JsonConvert.DeserializeObject(res.Response!)!; - ret.Discord = this.discord!; - - return ret; - } - #endregion - - #region Connections - internal async ValueTask> GetUsersConnectionsAsync() - { - string route = $"{Endpoints.USERS}/{Endpoints.ME}/{Endpoints.CONNECTIONS}"; - string url = $"{Endpoints.USERS}/{Endpoints.ME}/{Endpoints.CONNECTIONS}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Get - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - IEnumerable connectionsRaw = - JsonConvert.DeserializeObject>(res.Response!)! - .Select - ( - xc => - { - xc.Discord = this.discord!; - return xc; - } - ); - - return new ReadOnlyCollection(new List(connectionsRaw)); - } - #endregion - - #region Voice - internal async ValueTask> ListVoiceRegionsAsync() - { - string route = $"{Endpoints.VOICE}/{Endpoints.REGIONS}"; - string url = $"{Endpoints.VOICE}/{Endpoints.REGIONS}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Get - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - IEnumerable regions = - JsonConvert.DeserializeObject>(res.Response!)!; - - return new ReadOnlyCollection(new List(regions)); - } - #endregion - - #region Webhooks - internal async ValueTask CreateWebhookAsync - ( - ulong channelId, - string name, - Optional base64Avatar = default, - string? reason = null - ) - { - RestWebhookPayload pld = new() - { - Name = name, - AvatarBase64 = base64Avatar.HasValue ? base64Avatar.Value : null, - AvatarSet = base64Avatar.HasValue - }; - - Dictionary headers = []; - if (!string.IsNullOrWhiteSpace(reason)) - { - headers[REASON_HEADER_NAME] = reason; - } - - string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.WEBHOOKS}"; - string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.WEBHOOKS}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Post, - Payload = DiscordJson.SerializeObject(pld), - Headers = headers - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - DiscordWebhook ret = JsonConvert.DeserializeObject(res.Response!)!; - ret.Discord = this.discord!; - ret.ApiClient = this; - - return ret; - } - - internal async ValueTask> GetChannelWebhooksAsync - ( - ulong channelId - ) - { - string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.WEBHOOKS}"; - string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.WEBHOOKS}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Get - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - IEnumerable webhooksRaw = - JsonConvert - .DeserializeObject>(res.Response!)! - .Select - ( - xw => - { - xw.Discord = this.discord!; - xw.ApiClient = this; - return xw; - } - ); - - return new ReadOnlyCollection(new List(webhooksRaw)); - } - - internal async ValueTask> GetGuildWebhooksAsync - ( - ulong guildId - ) - { - string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.WEBHOOKS}"; - string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.WEBHOOKS}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Get - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - IEnumerable webhooksRaw = - JsonConvert - .DeserializeObject>(res.Response!)! - .Select - ( - xw => - { - xw.Discord = this.discord!; - xw.ApiClient = this; - return xw; - } - ); - - return new ReadOnlyCollection(new List(webhooksRaw)); - } - - internal async ValueTask GetWebhookAsync - ( - ulong webhookId - ) - { - string route = $"{Endpoints.WEBHOOKS}/{webhookId}"; - string url = $"{Endpoints.WEBHOOKS}/{webhookId}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Get - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - DiscordWebhook ret = JsonConvert.DeserializeObject(res.Response!)!; - ret.Discord = this.discord!; - ret.ApiClient = this; - - return ret; - } - - // Auth header not required - internal async ValueTask GetWebhookWithTokenAsync - ( - ulong webhookId, - string webhookToken - ) - { - string route = $"{Endpoints.WEBHOOKS}/{webhookId}/:webhook_token"; - string url = $"{Endpoints.WEBHOOKS}/{webhookId}/{webhookToken}"; - - RestRequest request = new() - { - Route = route, - Url = url, - IsExemptFromGlobalLimit = true, - Method = HttpMethod.Get - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - DiscordWebhook ret = JsonConvert.DeserializeObject(res.Response!)!; - ret.Token = webhookToken; - ret.Id = webhookId; - ret.Discord = this.discord!; - ret.ApiClient = this; - - return ret; - } - - internal async ValueTask GetWebhookMessageAsync - ( - ulong webhookId, - string webhookToken, - ulong messageId - ) - { - string route = $"{Endpoints.WEBHOOKS}/{webhookId}/:webhook_token/{Endpoints.MESSAGES}/:message_id"; - string url = $"{Endpoints.WEBHOOKS}/{webhookId}/{webhookToken}/{Endpoints.MESSAGES}/{messageId}"; - - RestRequest request = new() - { - Route = route, - Url = url, - IsExemptFromGlobalLimit = true, - Method = HttpMethod.Get - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - DiscordMessage ret = JsonConvert.DeserializeObject(res.Response!)!; - ret.Discord = this.discord!; - return ret; - } - - internal async ValueTask ModifyWebhookAsync - ( - ulong webhookId, - ulong channelId, - string? name = null, - Optional base64Avatar = default, - string? reason = null - ) - { - RestWebhookPayload pld = new() - { - Name = name, - AvatarBase64 = base64Avatar.HasValue ? base64Avatar.Value : null, - AvatarSet = base64Avatar.HasValue, - ChannelId = channelId - }; - - Dictionary headers = []; - if (!string.IsNullOrWhiteSpace(reason)) - { - headers[REASON_HEADER_NAME] = reason; - } - - string route = $"{Endpoints.WEBHOOKS}/{webhookId}"; - string url = $"{Endpoints.WEBHOOKS}/{webhookId}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Patch, - Payload = DiscordJson.SerializeObject(pld), - Headers = headers - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - DiscordWebhook ret = JsonConvert.DeserializeObject(res.Response!)!; - ret.Discord = this.discord!; - ret.ApiClient = this; - - return ret; - } - - internal async ValueTask ModifyWebhookAsync - ( - ulong webhookId, - string webhookToken, - string? name = null, - string? base64Avatar = null, - string? reason = null - ) - { - RestWebhookPayload pld = new() - { - Name = name, - AvatarBase64 = base64Avatar - }; - - Dictionary headers = []; - if (!string.IsNullOrWhiteSpace(reason)) - { - headers[REASON_HEADER_NAME] = reason; - } - - string route = $"{Endpoints.WEBHOOKS}/{webhookId}/:webhook_token"; - string url = $"{Endpoints.WEBHOOKS}/{webhookId}/{webhookToken}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Patch, - Payload = DiscordJson.SerializeObject(pld), - IsExemptFromGlobalLimit = true, - Headers = headers - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - DiscordWebhook ret = JsonConvert.DeserializeObject(res.Response!)!; - ret.Discord = this.discord!; - ret.ApiClient = this; - - return ret; - } - - internal async ValueTask DeleteWebhookAsync - ( - ulong webhookId, - string? reason = null - ) - { - Dictionary headers = []; - if (!string.IsNullOrWhiteSpace(reason)) - { - headers[REASON_HEADER_NAME] = reason; - } - - string route = $"{Endpoints.WEBHOOKS}/{webhookId}"; - string url = $"{Endpoints.WEBHOOKS}/{webhookId}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Delete, - Headers = headers - }; - - await this.rest.ExecuteRequestAsync(request); - } - - internal async ValueTask DeleteWebhookAsync - ( - ulong webhookId, - string webhookToken, - string? reason = null - ) - { - Dictionary headers = []; - if (!string.IsNullOrWhiteSpace(reason)) - { - headers[REASON_HEADER_NAME] = reason; - } - - string route = $"{Endpoints.WEBHOOKS}/{webhookId}/:webhook_token"; - string url = $"{Endpoints.WEBHOOKS}/{webhookId}/{webhookToken}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Delete, - IsExemptFromGlobalLimit = true, - Headers = headers - }; - - await this.rest.ExecuteRequestAsync(request); - } - - internal async ValueTask ExecuteWebhookAsync - ( - ulong webhookId, - string webhookToken, - DiscordWebhookBuilder builder - ) - { - builder.Validate(); - - if (builder.Embeds != null) - { - foreach (DiscordEmbed embed in builder.Embeds) - { - if (embed.Timestamp != null) - { - embed.Timestamp = embed.Timestamp.Value.ToUniversalTime(); - } - } - } - - Dictionary values = []; - RestWebhookExecutePayload pld = new() - { - Content = builder.Content, - Username = builder.Username.HasValue ? builder.Username.Value : null, - AvatarUrl = builder.AvatarUrl.HasValue ? builder.AvatarUrl.Value : null, - IsTTS = builder.IsTTS, - Embeds = builder.Embeds, - Flags = builder.Flags, - Components = builder.Components, - Poll = builder.Poll?.BuildInternal(), - }; - - if (builder.Mentions != null) - { - pld.Mentions = new DiscordMentions(builder.Mentions, builder.Mentions.Any()); - } - - if (!string.IsNullOrEmpty(builder.Content) || builder.Embeds?.Count > 0 || builder.IsTTS == true || builder.Mentions != null) - { - values["payload_json"] = DiscordJson.SerializeObject(pld); - } - - string route = $"{Endpoints.WEBHOOKS}/{webhookId}/:webhook_token"; - QueryUriBuilder url = new($"{Endpoints.WEBHOOKS}/{webhookId}/{webhookToken}"); - url.AddParameter("wait", "true"); - url.AddParameter("with_components", "true"); - - if (builder.ThreadId.HasValue) - { - url.AddParameter("thread_id", builder.ThreadId.Value.ToString()); - } - - MultipartRestRequest request = new() - { - Route = route, - Url = url.Build(), - Method = HttpMethod.Post, - Values = values, - Files = builder.Files, - IsExemptFromGlobalLimit = true - }; - - RestResponse res; - try - { - res = await this.rest.ExecuteRequestAsync(request); - } - finally - { - builder.ResetFileStreamPositions(); - } - DiscordMessage ret = JsonConvert.DeserializeObject(res.Response!)!; - - ret.Discord = this.discord!; - return ret; - } - - internal async ValueTask ExecuteWebhookSlackAsync - ( - ulong webhookId, - string webhookToken, - string jsonPayload - ) - { - string route = $"{Endpoints.WEBHOOKS}/{webhookId}/:webhook_token/{Endpoints.SLACK}"; - QueryUriBuilder url = new($"{Endpoints.WEBHOOKS}/{webhookId}/{webhookToken}/{Endpoints.SLACK}"); - - RestRequest request = new() - { - Route = route, - Url = url.Build(), - Method = HttpMethod.Post, - Payload = jsonPayload, - IsExemptFromGlobalLimit = true - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - DiscordMessage ret = JsonConvert.DeserializeObject(res.Response!)!; - ret.Discord = this.discord!; - return ret; - } - - internal async ValueTask ExecuteWebhookGithubAsync - ( - ulong webhookId, - string webhookToken, - string jsonPayload - ) - { - string route = $"{Endpoints.WEBHOOKS}/{webhookId}/:webhook_token{Endpoints.GITHUB}"; - QueryUriBuilder url = new($"{Endpoints.WEBHOOKS}/{webhookId}/{webhookToken}{Endpoints.GITHUB}"); - url.AddParameter("wait", "true"); - - RestRequest request = new() - { - Route = route, - Url = url.Build(), - Method = HttpMethod.Post, - Payload = jsonPayload, - IsExemptFromGlobalLimit = true - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - DiscordMessage ret = JsonConvert.DeserializeObject(res.Response!)!; - ret.Discord = this.discord!; - return ret; - } - - internal async ValueTask EditWebhookMessageAsync - ( - ulong webhookId, - string webhookToken, - ulong messageId, - DiscordWebhookBuilder builder, - IEnumerable? attachments = null - ) - { - builder.Validate(true); - - DiscordMentions? mentions = builder.Mentions != null ? new DiscordMentions(builder.Mentions, builder.Mentions.Any()) : null; - - RestWebhookMessageEditPayload pld = new() - { - Content = builder.Content, - Embeds = builder.Embeds, - Mentions = mentions, - Flags = builder.Flags, - Components = builder.Components, - Attachments = attachments - }; - - string route = $"{Endpoints.WEBHOOKS}/{webhookId}/:webhook_token/{Endpoints.MESSAGES}/:message_id"; - string url = $"{Endpoints.WEBHOOKS}/{webhookId}/{webhookToken}/{Endpoints.MESSAGES}/{messageId}"; - - Dictionary values = new() - { - ["payload_json"] = DiscordJson.SerializeObject(pld) - }; - - MultipartRestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Patch, - Values = values, - Files = builder.Files, - IsExemptFromGlobalLimit = true - }; - - RestResponse res; - try - { - res = await this.rest.ExecuteRequestAsync(request); - } - finally - { - builder.ResetFileStreamPositions(); - } - - return PrepareMessage(JObject.Parse(res.Response!)); - } - - internal async ValueTask DeleteWebhookMessageAsync - ( - ulong webhookId, - string webhookToken, - ulong messageId - ) - { - string route = $"{Endpoints.WEBHOOKS}/{webhookId}/:webhook_token/{Endpoints.MESSAGES}/:message_id"; - string url = $"{Endpoints.WEBHOOKS}/{webhookId}/{webhookToken}/{Endpoints.MESSAGES}/{messageId}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Delete, - IsExemptFromGlobalLimit = true - }; - - await this.rest.ExecuteRequestAsync(request); - } - #endregion - - #region Reactions - internal async ValueTask CreateReactionAsync - ( - ulong channelId, - ulong messageId, - string emoji - ) - { - string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/:message_id/{Endpoints.REACTIONS}/:emoji/{Endpoints.ME}"; - string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/{messageId}/{Endpoints.REACTIONS}/{emoji}/{Endpoints.ME}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Put - }; - - await this.rest.ExecuteRequestAsync(request); - } - - internal async ValueTask DeleteOwnReactionAsync - ( - ulong channelId, - ulong messageId, - string emoji - ) - { - string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/:message_id/{Endpoints.REACTIONS}/:emoji/{Endpoints.ME}"; - string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/{messageId}/{Endpoints.REACTIONS}/{emoji}/{Endpoints.ME}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Delete - }; - - await this.rest.ExecuteRequestAsync(request); - } - - internal async ValueTask DeleteUserReactionAsync - ( - ulong channelId, - ulong messageId, - ulong userId, - string emoji, - string? reason - ) - { - Dictionary headers = []; - if (!string.IsNullOrWhiteSpace(reason)) - { - headers[REASON_HEADER_NAME] = reason; - } - - string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/:message_id/{Endpoints.REACTIONS}/:emoji/:user_id"; - string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/{messageId}/{Endpoints.REACTIONS}/{emoji}/{userId}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Delete, - Headers = headers - }; - - await this.rest.ExecuteRequestAsync(request); - } - - internal async ValueTask> GetReactionsAsync - ( - ulong channelId, - ulong messageId, - string emoji, - ulong? afterId = null, - int limit = 25 - ) - { - QueryUriBuilder urlparams = new($"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/{messageId}/{Endpoints.REACTIONS}/{emoji}"); - if (afterId.HasValue) - { - urlparams.AddParameter("after", afterId.Value.ToString(CultureInfo.InvariantCulture)); - } - - urlparams.AddParameter("limit", limit.ToString(CultureInfo.InvariantCulture)); - - string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/:message_id/{Endpoints.REACTIONS}/:emoji"; - - RestRequest request = new() - { - Route = route, - Url = urlparams.Build(), - Method = HttpMethod.Get - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - IEnumerable usersRaw = JsonConvert.DeserializeObject>(res.Response!)!; - List users = []; - foreach (TransportUser xr in usersRaw) - { - DiscordUser usr = new(xr) - { - Discord = this.discord! - }; - usr = this.discord!.UpdateUserCache(usr); - - users.Add(usr); - } - - return new ReadOnlyCollection(new List(users)); - } - - internal async ValueTask DeleteAllReactionsAsync - ( - ulong channelId, - ulong messageId, - string? reason = null - ) - { - Dictionary headers = []; - if (!string.IsNullOrWhiteSpace(reason)) - { - headers[REASON_HEADER_NAME] = reason; - } - - string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/:message_id/{Endpoints.REACTIONS}"; - string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/{messageId}/{Endpoints.REACTIONS}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Delete, - Headers = headers - }; - - await this.rest.ExecuteRequestAsync(request); - } - - internal async ValueTask DeleteReactionsEmojiAsync - ( - ulong channelId, - ulong messageId, - string emoji - ) - { - string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/:message_id/{Endpoints.REACTIONS}/:emoji"; - string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/{messageId}/{Endpoints.REACTIONS}/{emoji}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Delete - }; - - await this.rest.ExecuteRequestAsync(request); - } - #endregion - - #region Polls - - internal async ValueTask> GetPollAnswerVotersAsync - ( - ulong channelId, - ulong messageId, - int answerId, - ulong? after, - int? limit - ) - { - string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.POLLS}/:message_id/{Endpoints.ANSWERS}/:answer_id"; - QueryUriBuilder url = new($"{Endpoints.CHANNELS}/{channelId}/{Endpoints.POLLS}/{messageId}/{Endpoints.ANSWERS}/{answerId}"); - - if (limit > 0) - { - url.AddParameter("limit", limit.Value.ToString(CultureInfo.InvariantCulture)); - } - - if (after > 0) - { - url.AddParameter("after", after.Value.ToString(CultureInfo.InvariantCulture)); - } - - RestRequest request = new() - { - Route = route, - Url = url.Build(), - Method = HttpMethod.Get - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - JToken jto = JToken.Parse(res.Response!); - - return (jto as JArray ?? jto["users"] as JArray)! - .Select(j => j.ToDiscordObject()) - .ToList(); - } - - internal async ValueTask EndPollAsync - ( - ulong channelId, - ulong messageId - ) - { - string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.POLLS}/:message_id/{Endpoints.EXPIRE}"; - string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.POLLS}/{messageId}/{Endpoints.EXPIRE}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Post - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - DiscordMessage ret = PrepareMessage(JObject.Parse(res.Response!)); - - return ret; - } - - #endregion - - #region Emoji - internal async ValueTask> GetGuildEmojisAsync - ( - ulong guildId - ) - { - string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.EMOJIS}"; - string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.EMOJIS}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Get - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - IEnumerable emojisRaw = JsonConvert.DeserializeObject>(res.Response!)!; - - this.discord!.Guilds.TryGetValue(guildId, out DiscordGuild? guild); - Dictionary users = []; - List emojis = []; - foreach (JObject rawEmoji in emojisRaw) - { - DiscordGuildEmoji discordGuildEmoji = rawEmoji.ToDiscordObject(); - - if (guild is not null) - { - discordGuildEmoji.Guild = guild; - } - - TransportUser? rawUser = rawEmoji["user"]?.ToDiscordObject(); - if (rawUser != null) - { - if (!users.ContainsKey(rawUser.Id)) - { - DiscordUser user = guild is not null && guild.Members.TryGetValue(rawUser.Id, out DiscordMember? member) ? member : new DiscordUser(rawUser); - users[user.Id] = user; - } - - discordGuildEmoji.User = users[rawUser.Id]; - } - - emojis.Add(discordGuildEmoji); - } - - return new ReadOnlyCollection(emojis); - } - - internal async ValueTask GetGuildEmojiAsync - ( - ulong guildId, - ulong emojiId - ) - { - string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.EMOJIS}/:emoji_id"; - string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.EMOJIS}/{emojiId}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Get - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - this.discord!.Guilds.TryGetValue(guildId, out DiscordGuild? guild); - - JObject emojiRaw = JObject.Parse(res.Response!); - DiscordGuildEmoji emoji = emojiRaw.ToDiscordObject(); - - if (guild is not null) - { - emoji.Guild = guild; - } - - TransportUser? rawUser = emojiRaw["user"]?.ToDiscordObject(); - if (rawUser != null) - { - emoji.User = guild is not null && guild.Members.TryGetValue(rawUser.Id, out DiscordMember? member) ? member : new DiscordUser(rawUser); - } - - return emoji; - } - - internal async ValueTask CreateGuildEmojiAsync - ( - ulong guildId, - string name, - string imageb64, - IEnumerable? roles = null, - string? reason = null - ) - { - RestGuildEmojiCreatePayload pld = new() - { - Name = name, - ImageB64 = imageb64, - Roles = roles?.ToArray() - }; - - Dictionary headers = []; - if (!string.IsNullOrWhiteSpace(reason)) - { - headers[REASON_HEADER_NAME] = reason; - } - - string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.EMOJIS}"; - string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.EMOJIS}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Post, - Payload = DiscordJson.SerializeObject(pld), - Headers = headers - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - this.discord!.Guilds.TryGetValue(guildId, out DiscordGuild? guild); - - JObject emojiRaw = JObject.Parse(res.Response!); - DiscordGuildEmoji emoji = emojiRaw.ToDiscordObject(); - - if (guild is not null) - { - emoji.Guild = guild; - } - - TransportUser? rawUser = emojiRaw["user"]?.ToDiscordObject(); - emoji.User = rawUser != null - ? guild is not null && guild.Members.TryGetValue(rawUser.Id, out DiscordMember? member) ? member : new DiscordUser(rawUser) - : this.discord.CurrentUser; - - return emoji; - } - - internal async ValueTask ModifyGuildEmojiAsync - ( - ulong guildId, - ulong emojiId, - string? name = null, - IEnumerable? roles = null, - string? reason = null - ) - { - RestGuildEmojiModifyPayload pld = new() - { - Name = name, - Roles = roles?.ToArray() - }; - - Dictionary headers = []; - if (!string.IsNullOrWhiteSpace(reason)) - { - headers[REASON_HEADER_NAME] = reason; - } - - string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.EMOJIS}/:emoji_id"; - string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.EMOJIS}/{emojiId}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Patch, - Payload = DiscordJson.SerializeObject(pld), - Headers = headers - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - this.discord!.Guilds.TryGetValue(guildId, out DiscordGuild? guild); - - JObject emojiRaw = JObject.Parse(res.Response!); - DiscordGuildEmoji emoji = emojiRaw.ToDiscordObject(); - - if (guild is not null) - { - emoji.Guild = guild; - } - - TransportUser? rawUser = emojiRaw["user"]?.ToDiscordObject(); - if (rawUser != null) - { - emoji.User = guild is not null && guild.Members.TryGetValue(rawUser.Id, out DiscordMember? member) ? member : new DiscordUser(rawUser); - } - - return emoji; - } - - internal async ValueTask DeleteGuildEmojiAsync - ( - ulong guildId, - ulong emojiId, - string? reason = null - ) - { - Dictionary headers = []; - if (!string.IsNullOrWhiteSpace(reason)) - { - headers[REASON_HEADER_NAME] = reason; - } - - string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.EMOJIS}/:emoji_id"; - string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.EMOJIS}/{emojiId}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Delete, - Headers = headers - }; - - await this.rest.ExecuteRequestAsync(request); - } - #endregion - - #region Application Commands - internal async ValueTask> GetGlobalApplicationCommandsAsync - ( - ulong applicationId, - bool withLocalizations = false - ) - { - string route = $"{Endpoints.APPLICATIONS}/:application_id/{Endpoints.COMMANDS}"; - QueryUriBuilder builder = new($"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.COMMANDS}"); - - if (withLocalizations) - { - builder.AddParameter("with_localizations", "true"); - } - - RestRequest request = new() - { - Route = route, - Url = builder.Build(), - Method = HttpMethod.Get - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - IEnumerable ret = JsonConvert.DeserializeObject>(res.Response!)!; - foreach (DiscordApplicationCommand app in ret) - { - app.Discord = this.discord!; - } - - return ret.ToList(); - } - - internal async ValueTask> BulkOverwriteGlobalApplicationCommandsAsync - ( - ulong applicationId, - IEnumerable commands - ) - { - List pld = []; - foreach (DiscordApplicationCommand command in commands) - { - pld.Add(new RestApplicationCommandCreatePayload - { - Type = command.Type, - Name = command.Name, - Description = command.Description, - Options = command.Options, - DefaultPermission = command.DefaultPermission, - NameLocalizations = command.NameLocalizations, - DescriptionLocalizations = command.DescriptionLocalizations, - AllowDMUsage = command.AllowDMUsage, - DefaultMemberPermissions = command.DefaultMemberPermissions, - NSFW = command.NSFW, - AllowedContexts = command.Contexts, - InstallTypes = command.IntegrationTypes, - }); - } - - string route = $"{Endpoints.APPLICATIONS}/:application_id/{Endpoints.COMMANDS}"; - string url = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.COMMANDS}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Put, - Payload = DiscordJson.SerializeObject(pld) - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - IEnumerable ret = JsonConvert.DeserializeObject>(res.Response!)!; - foreach (DiscordApplicationCommand app in ret) - { - app.Discord = this.discord!; - } - - return ret.ToList(); - } - - internal async ValueTask CreateGlobalApplicationCommandAsync - ( - ulong applicationId, - DiscordApplicationCommand command - ) - { - RestApplicationCommandCreatePayload pld = new() - { - Type = command.Type, - Name = command.Name, - Description = command.Description, - Options = command.Options, - DefaultPermission = command.DefaultPermission, - NameLocalizations = command.NameLocalizations, - DescriptionLocalizations = command.DescriptionLocalizations, - AllowDMUsage = command.AllowDMUsage, - DefaultMemberPermissions = command.DefaultMemberPermissions, - NSFW = command.NSFW, - AllowedContexts = command.Contexts, - InstallTypes = command.IntegrationTypes, - }; - - string route = $"{Endpoints.APPLICATIONS}/:application_id/{Endpoints.COMMANDS}"; - string url = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.COMMANDS}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Post, - Payload = DiscordJson.SerializeObject(pld) - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - DiscordApplicationCommand ret = JsonConvert.DeserializeObject(res.Response!)!; - ret.Discord = this.discord!; - - return ret; - } - - internal async ValueTask GetGlobalApplicationCommandAsync - ( - ulong applicationId, - ulong commandId - ) - { - string route = $"{Endpoints.APPLICATIONS}/:application_id/{Endpoints.COMMANDS}/:command_id"; - string url = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.COMMANDS}/{commandId}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Get - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - DiscordApplicationCommand ret = JsonConvert.DeserializeObject(res.Response!)!; - ret.Discord = this.discord!; - - return ret; - } - - internal async ValueTask EditGlobalApplicationCommandAsync - ( - ulong applicationId, - ulong commandId, - Optional name = default, - Optional description = default, - Optional> options = default, - Optional defaultPermission = default, - Optional nsfw = default, - IReadOnlyDictionary? nameLocalizations = null, - IReadOnlyDictionary? descriptionLocalizations = null, - Optional allowDmUsage = default, - Optional defaultMemberPermissions = default, - Optional> allowedContexts = default, - Optional> installTypes = default - ) - { - RestApplicationCommandEditPayload pld = new() - { - Name = name, - Description = description, - Options = options, - DefaultPermission = defaultPermission, - NameLocalizations = nameLocalizations, - DescriptionLocalizations = descriptionLocalizations, - AllowDMUsage = allowDmUsage, - DefaultMemberPermissions = defaultMemberPermissions, - NSFW = nsfw, - AllowedContexts = allowedContexts, - InstallTypes = installTypes, - }; - - string route = $"{Endpoints.APPLICATIONS}/:application_id/{Endpoints.COMMANDS}/:command_id"; - string url = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.COMMANDS}/{commandId}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Patch, - Payload = DiscordJson.SerializeObject(pld) - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - DiscordApplicationCommand ret = JsonConvert.DeserializeObject(res.Response!)!; - ret.Discord = this.discord!; - - return ret; - } - - internal async ValueTask DeleteGlobalApplicationCommandAsync - ( - ulong applicationId, - ulong commandId - ) - { - string route = $"{Endpoints.APPLICATIONS}/:application_id/{Endpoints.COMMANDS}/:command_id"; - string url = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.COMMANDS}/{commandId}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Delete - }; - - await this.rest.ExecuteRequestAsync(request); - } - - internal async ValueTask> GetGuildApplicationCommandsAsync - ( - ulong applicationId, - ulong guildId, - bool withLocalizations = false - ) - { - string route = $"{Endpoints.APPLICATIONS}/:application_id/{Endpoints.GUILDS}/:guild_id/{Endpoints.COMMANDS}"; - QueryUriBuilder builder = new($"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.GUILDS}/{guildId}/{Endpoints.COMMANDS}"); - - if (withLocalizations) - { - builder.AddParameter("with_localizations", "true"); - } - - RestRequest request = new() - { - Route = route, - Url = builder.Build(), - Method = HttpMethod.Get - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - IEnumerable ret = JsonConvert.DeserializeObject>(res.Response!)!; - foreach (DiscordApplicationCommand app in ret) - { - app.Discord = this.discord!; - } - - return ret.ToList(); - } - - internal async ValueTask> BulkOverwriteGuildApplicationCommandsAsync - ( - ulong applicationId, - ulong guildId, - IEnumerable commands - ) - { - List pld = []; - foreach (DiscordApplicationCommand command in commands) - { - pld.Add(new RestApplicationCommandCreatePayload - { - Type = command.Type, - Name = command.Name, - Description = command.Description, - Options = command.Options, - DefaultPermission = command.DefaultPermission, - NameLocalizations = command.NameLocalizations, - DescriptionLocalizations = command.DescriptionLocalizations, - AllowDMUsage = command.AllowDMUsage, - DefaultMemberPermissions = command.DefaultMemberPermissions, - NSFW = command.NSFW - }); - } - - string route = $"{Endpoints.APPLICATIONS}/:application_id/{Endpoints.GUILDS}/:guild_id/{Endpoints.COMMANDS}"; - string url = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.GUILDS}/{guildId}/{Endpoints.COMMANDS}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Put, - Payload = DiscordJson.SerializeObject(pld) - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - IEnumerable ret = JsonConvert.DeserializeObject>(res.Response!)!; - foreach (DiscordApplicationCommand app in ret) - { - app.Discord = this.discord!; - } - - return ret.ToList(); - } - - internal async ValueTask CreateGuildApplicationCommandAsync - ( - ulong applicationId, - ulong guildId, - DiscordApplicationCommand command - ) - { - RestApplicationCommandCreatePayload pld = new() - { - Type = command.Type, - Name = command.Name, - Description = command.Description, - Options = command.Options, - DefaultPermission = command.DefaultPermission, - NameLocalizations = command.NameLocalizations, - DescriptionLocalizations = command.DescriptionLocalizations, - AllowDMUsage = command.AllowDMUsage, - DefaultMemberPermissions = command.DefaultMemberPermissions, - NSFW = command.NSFW - }; - - string route = $"{Endpoints.APPLICATIONS}/:application_id/{Endpoints.GUILDS}/:guild_id/{Endpoints.COMMANDS}"; - string url = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.GUILDS}/{guildId}/{Endpoints.COMMANDS}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Post, - Payload = DiscordJson.SerializeObject(pld) - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - DiscordApplicationCommand ret = JsonConvert.DeserializeObject(res.Response!)!; - ret.Discord = this.discord!; - - return ret; - } - - internal async ValueTask GetGuildApplicationCommandAsync - ( - ulong applicationId, - ulong guildId, - ulong commandId - ) - { - string route = $"{Endpoints.APPLICATIONS}/:application_id/{Endpoints.GUILDS}/:guild_id/{Endpoints.COMMANDS}/:command_id"; - string url = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.GUILDS}/{guildId}/{Endpoints.COMMANDS}/{commandId}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Get - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - DiscordApplicationCommand ret = JsonConvert.DeserializeObject(res.Response!)!; - ret.Discord = this.discord!; - - return ret; - } - - internal async ValueTask EditGuildApplicationCommandAsync - ( - ulong applicationId, - ulong guildId, - ulong commandId, - Optional name = default, - Optional description = default, - Optional> options = default, - Optional defaultPermission = default, - Optional nsfw = default, - IReadOnlyDictionary? nameLocalizations = null, - IReadOnlyDictionary? descriptionLocalizations = null, - Optional allowDmUsage = default, - Optional defaultMemberPermissions = default, - Optional> allowedContexts = default, - Optional> installTypes = default - ) - { - RestApplicationCommandEditPayload pld = new() - { - Name = name, - Description = description, - Options = options, - DefaultPermission = defaultPermission, - NameLocalizations = nameLocalizations, - DescriptionLocalizations = descriptionLocalizations, - AllowDMUsage = allowDmUsage, - DefaultMemberPermissions = defaultMemberPermissions, - NSFW = nsfw, - AllowedContexts = allowedContexts, - InstallTypes = installTypes - }; - - string route = $"{Endpoints.APPLICATIONS}/:application_id/{Endpoints.GUILDS}/:guild_id/{Endpoints.COMMANDS}/:command_id"; - string url = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.GUILDS}/{guildId}/{Endpoints.COMMANDS}/{commandId}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Patch, - Payload = DiscordJson.SerializeObject(pld) - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - DiscordApplicationCommand ret = JsonConvert.DeserializeObject(res.Response!)!; - ret.Discord = this.discord!; - - return ret; - } - - internal async ValueTask DeleteGuildApplicationCommandAsync - ( - ulong applicationId, - ulong guildId, - ulong commandId - ) - { - string route = $"{Endpoints.APPLICATIONS}/:application_id/{Endpoints.GUILDS}/:guild_id/{Endpoints.COMMANDS}/:command_id"; - string url = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.GUILDS}/{guildId}/{Endpoints.COMMANDS}/{commandId}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Delete - }; - - await this.rest.ExecuteRequestAsync(request); - } - - internal async ValueTask CreateInteractionResponseAsync - ( - ulong interactionId, - string interactionToken, - DiscordInteractionResponseType type, - DiscordInteractionResponseBuilder? builder - ) - { - if (builder?.Embeds != null) - { - foreach (DiscordEmbed embed in builder.Embeds) - { - if (embed.Timestamp is not null) - { - embed.Timestamp = embed.Timestamp.Value.ToUniversalTime(); - } - } - } - - DiscordInteractionResponsePayload payload = new() - { - Type = type, - Data = builder is not null - ? new DiscordInteractionApplicationCommandCallbackData - { - Content = builder.Content, - Title = builder.Title, - CustomId = builder.CustomId, - Embeds = builder.Embeds, - IsTTS = builder.IsTTS, - Mentions = new DiscordMentions(builder.Mentions ?? Mentions.All, builder.Mentions?.Any() ?? false), - Flags = builder.Flags, - Components = builder.Components, - Choices = builder.Choices, - Poll = builder.Poll?.BuildInternal(), - } - : null - }; - - Dictionary values = []; - - if (builder != null) - { - if (!string.IsNullOrEmpty(builder.Content) || builder.Embeds?.Count > 0 || builder.IsTTS == true || builder.Mentions != null) - { - values["payload_json"] = DiscordJson.SerializeObject(payload); - } - } - - string route = $"{Endpoints.INTERACTIONS}/{interactionId}/:interaction_token/{Endpoints.CALLBACK}"; - string url = $"{Endpoints.INTERACTIONS}/{interactionId}/{interactionToken}/{Endpoints.CALLBACK}"; - - if (builder is not null && builder.Files.Count != 0) - { - MultipartRestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Post, - Values = values, - Files = builder.Files, - IsExemptFromAllLimits = true - }; - - try - { - await this.rest.ExecuteRequestAsync(request); - } - finally - { - builder.ResetFileStreamPositions(); - } - } - else - { - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Post, - Payload = DiscordJson.SerializeObject(payload), - IsExemptFromGlobalLimit = true - }; - - await this.rest.ExecuteRequestAsync(request); - } - } - - internal async ValueTask GetOriginalInteractionResponseAsync - ( - ulong applicationId, - string interactionToken - ) - { - string route = $"{Endpoints.WEBHOOKS}/:application_id/{interactionToken}/{Endpoints.MESSAGES}/{Endpoints.ORIGINAL}"; - string url = $"{Endpoints.WEBHOOKS}/{applicationId}/{interactionToken}/{Endpoints.MESSAGES}/{Endpoints.ORIGINAL}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Get, - IsExemptFromGlobalLimit = true - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - DiscordMessage ret = JsonConvert.DeserializeObject(res.Response!)!; - - ret.Channel = (this.discord as DiscordClient).InternalGetCachedChannel(ret.ChannelId); - ret.Discord = this.discord!; - - return ret; - } - - internal async ValueTask EditOriginalInteractionResponseAsync - ( - ulong applicationId, - string interactionToken, - DiscordWebhookBuilder builder, - IEnumerable attachments - ) - { - { - builder.Validate(true); - - DiscordMentions? mentions = builder.Mentions != null ? new DiscordMentions(builder.Mentions, builder.Mentions.Any()) : null; - - RestWebhookMessageEditPayload pld = new() - { - Content = builder.Content, - Embeds = builder.Embeds, - Mentions = mentions, - Flags = builder.Flags, - Components = builder.Components, - Attachments = attachments - }; - - string route = $"{Endpoints.WEBHOOKS}/:application_id/{interactionToken}/{Endpoints.MESSAGES}/@original"; - string url = $"{Endpoints.WEBHOOKS}/{applicationId}/{interactionToken}/{Endpoints.MESSAGES}/@original"; - - Dictionary values = new() - { - ["payload_json"] = DiscordJson.SerializeObject(pld) - }; - - MultipartRestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Patch, - Values = values, - Files = builder.Files, - IsExemptFromAllLimits = true - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - DiscordMessage ret = JsonConvert.DeserializeObject(res.Response!)!; - ret.Discord = this.discord!; - - foreach (DiscordMessageFile file in builder.Files.Where(x => x.ResetPositionTo.HasValue)) - { - file.Stream.Position = file.ResetPositionTo!.Value; - } - - return ret; - } - } - - internal async ValueTask DeleteOriginalInteractionResponseAsync - ( - ulong applicationId, - string interactionToken - ) - { - string route = $"{Endpoints.WEBHOOKS}/:application_id/{interactionToken}/{Endpoints.MESSAGES}/@original"; - string url = $"{Endpoints.WEBHOOKS}/{applicationId}/{interactionToken}/{Endpoints.MESSAGES}/@original"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Delete, - IsExemptFromAllLimits = true - }; - - await this.rest.ExecuteRequestAsync(request); - } - - internal async ValueTask CreateFollowupMessageAsync - ( - ulong applicationId, - string interactionToken, - DiscordFollowupMessageBuilder builder - ) - { - builder.Validate(); - - if (builder.Embeds != null) - { - foreach (DiscordEmbed embed in builder.Embeds) - { - if (embed.Timestamp != null) - { - embed.Timestamp = embed.Timestamp.Value.ToUniversalTime(); - } - } - } - - Dictionary values = []; - RestFollowupMessageCreatePayload pld = new() - { - Content = builder.Content, - IsTTS = builder.IsTTS, - Embeds = builder.Embeds, - Flags = builder.Flags, - Components = builder.Components - }; - - if (builder.Mentions != null) - { - pld.Mentions = new DiscordMentions(builder.Mentions, builder.Mentions.Any()); - } - - if (!string.IsNullOrEmpty(builder.Content) || builder.Embeds?.Count > 0 || builder.IsTTS == true || builder.Mentions != null) - { - values["payload_json"] = DiscordJson.SerializeObject(pld); - } - - string route = $"{Endpoints.WEBHOOKS}/:application_id/{interactionToken}"; - string url = $"{Endpoints.WEBHOOKS}/{applicationId}/{interactionToken}"; - - MultipartRestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Post, - Values = values, - Files = builder.Files, - IsExemptFromAllLimits = true - }; - - RestResponse res; - try - { - res = await this.rest.ExecuteRequestAsync(request); - } - finally - { - builder.ResetFileStreamPositions(); - } - DiscordMessage ret = JsonConvert.DeserializeObject(res.Response!)!; - - ret.Discord = this.discord!; - return ret; - } - - internal ValueTask GetFollowupMessageAsync - ( - ulong applicationId, - string interactionToken, - ulong messageId - ) - => GetWebhookMessageAsync(applicationId, interactionToken, messageId); - - internal ValueTask EditFollowupMessageAsync - ( - ulong applicationId, - string interactionToken, - ulong messageId, - DiscordWebhookBuilder builder, - IEnumerable attachments - ) - => EditWebhookMessageAsync(applicationId, interactionToken, messageId, builder, attachments); - - internal ValueTask DeleteFollowupMessageAsync(ulong applicationId, string interactionToken, ulong messageId) - => DeleteWebhookMessageAsync(applicationId, interactionToken, messageId); - - internal async ValueTask> GetGuildApplicationCommandPermissionsAsync - ( - ulong applicationId, - ulong guildId - ) - { - string route = $"{Endpoints.APPLICATIONS}/:application_id/{Endpoints.GUILDS}/:guild_id/{Endpoints.COMMANDS}/{Endpoints.PERMISSIONS}"; - string url = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.GUILDS}/{guildId}/{Endpoints.COMMANDS}/{Endpoints.PERMISSIONS}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Get - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - IEnumerable ret = JsonConvert.DeserializeObject>(res.Response!)!; - - foreach (DiscordGuildApplicationCommandPermissions perm in ret) - { - perm.Discord = this.discord!; - } - - return ret.ToList(); - } - - internal async ValueTask GetApplicationCommandPermissionsAsync - ( - ulong applicationId, - ulong guildId, - ulong commandId - ) - { - string route = $"{Endpoints.APPLICATIONS}/:application_id/{Endpoints.GUILDS}/:guild_id/{Endpoints.COMMANDS}/:command_id/{Endpoints.PERMISSIONS}"; - string url = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.GUILDS}/{guildId}/{Endpoints.COMMANDS}/{commandId}/{Endpoints.PERMISSIONS}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Get - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - DiscordGuildApplicationCommandPermissions ret = JsonConvert.DeserializeObject(res.Response!)!; - - ret.Discord = this.discord!; - return ret; - } - - internal async ValueTask EditApplicationCommandPermissionsAsync - ( - ulong applicationId, - ulong guildId, - ulong commandId, - IEnumerable permissions - ) - { - - RestEditApplicationCommandPermissionsPayload pld = new() - { - Permissions = permissions - }; - - string route = $"{Endpoints.APPLICATIONS}/:application_id/{Endpoints.GUILDS}/:guild_id/{Endpoints.COMMANDS}/:command_id/{Endpoints.PERMISSIONS}"; - string url = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.GUILDS}/{guildId}/{Endpoints.COMMANDS}/{commandId}/{Endpoints.PERMISSIONS}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Put, - Payload = DiscordJson.SerializeObject(pld) - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - DiscordGuildApplicationCommandPermissions ret = - JsonConvert.DeserializeObject(res.Response!)!; - - ret.Discord = this.discord!; - return ret; - } - - internal async ValueTask> BatchEditApplicationCommandPermissionsAsync - ( - ulong applicationId, - ulong guildId, - IEnumerable permissions - ) - { - string route = $"{Endpoints.APPLICATIONS}/:application_id/{Endpoints.GUILDS}/:guild_id/{Endpoints.COMMANDS}/{Endpoints.PERMISSIONS}"; - string url = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.GUILDS}/{guildId}/{Endpoints.COMMANDS}/{Endpoints.PERMISSIONS}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Put, - Payload = DiscordJson.SerializeObject(permissions) - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - IEnumerable ret = - JsonConvert.DeserializeObject>(res.Response!)!; - - foreach (DiscordGuildApplicationCommandPermissions perm in ret) - { - perm.Discord = this.discord!; - } - - return ret.ToList(); - } - #endregion - - #region Misc - internal ValueTask GetCurrentApplicationInfoAsync() - => GetApplicationInfoAsync("@me"); - - internal ValueTask GetApplicationInfoAsync - ( - ulong applicationId - ) - => GetApplicationInfoAsync(applicationId.ToString(CultureInfo.InvariantCulture)); - - private async ValueTask GetApplicationInfoAsync - ( - string applicationId - ) - { - string route = $"{Endpoints.OAUTH2}/{Endpoints.APPLICATIONS}/:application_id"; - string url = $"{Endpoints.OAUTH2}/{Endpoints.APPLICATIONS}/{applicationId}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Get - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - return JsonConvert.DeserializeObject(res.Response!)!; - } - - internal async ValueTask> GetApplicationAssetsAsync - ( - DiscordApplication application - ) - { - string route = $"{Endpoints.OAUTH2}/{Endpoints.APPLICATIONS}/:application_id/{Endpoints.ASSETS}"; - string url = $"{Endpoints.OAUTH2}/{Endpoints.APPLICATIONS}/{application.Id}/{Endpoints.ASSETS}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Get - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - IEnumerable assets = JsonConvert.DeserializeObject>(res.Response!)!; - foreach (DiscordApplicationAsset asset in assets) - { - asset.Discord = application.Discord; - asset.Application = application; - } - - return new ReadOnlyCollection(new List(assets)); - } - - internal async ValueTask GetGatewayInfoAsync() - { - Dictionary headers = []; - string route = $"{Endpoints.GATEWAY}/{Endpoints.BOT}"; - string url = route; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Get, - Headers = headers - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - - GatewayInfo info = JObject.Parse(res.Response!).ToDiscordObject(); - info.SessionBucket.ResetAfter = DateTimeOffset.UtcNow + TimeSpan.FromMilliseconds(info.SessionBucket.ResetAfterInternal); - return info; - } - #endregion - - internal async ValueTask CreateApplicationEmojiAsync(ulong applicationId, string name, string image) - { - string route = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.EMOJIS}"; - string url = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.EMOJIS}"; - - RestApplicationEmojiCreatePayload pld = new() - { - Name = name, - ImageB64 = image - }; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Post, - Payload = DiscordJson.SerializeObject(pld) - }; - - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - DiscordEmoji emoji = JsonConvert.DeserializeObject(res.Response!)!; - emoji.Discord = this.discord!; - - return emoji; - } - - internal async ValueTask ModifyApplicationEmojiAsync(ulong applicationId, ulong emojiId, string name) - { - string route = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.EMOJIS}/{emojiId}"; - string url = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.EMOJIS}/{emojiId}"; - - RestApplicationEmojiModifyPayload pld = new() - { - Name = name, - }; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Patch, - Payload = DiscordJson.SerializeObject(pld) - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - DiscordEmoji emoji = JsonConvert.DeserializeObject(res.Response!)!; - - emoji.Discord = this.discord!; - - return emoji; - } - - internal async ValueTask DeleteApplicationEmojiAsync(ulong applicationId, ulong emojiId) - { - string route = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.EMOJIS}/{emojiId}"; - string url = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.EMOJIS}/{emojiId}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Delete - }; - - await this.rest.ExecuteRequestAsync(request); - } - - internal async ValueTask GetApplicationEmojiAsync(ulong applicationId, ulong emojiId) - { - string route = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.EMOJIS}/{emojiId}"; - string url = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.EMOJIS}/{emojiId}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Get - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - DiscordEmoji emoji = JsonConvert.DeserializeObject(res.Response!)!; - emoji.Discord = this.discord!; - - return emoji; - } - - internal async ValueTask> GetApplicationEmojisAsync(ulong applicationId) - { - string route = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.EMOJIS}"; - string url = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.EMOJIS}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Get - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - IEnumerable emojis = JObject.Parse(res.Response!)["items"]!.ToDiscordObject(); - - foreach (DiscordEmoji emoji in emojis) - { - emoji.Discord = this.discord!; - emoji.User!.Discord = this.discord!; - } - - return emojis.ToList(); - } - - internal async ValueTask CreateForumPostAsync - ( - ulong channelId, - string name, - DiscordMessageBuilder message, - DiscordAutoArchiveDuration? autoArchiveDuration = null, - int? rateLimitPerUser = null, - IEnumerable? appliedTags = null - ) - { - string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.THREADS}"; - string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.THREADS}"; - - RestForumPostCreatePayload pld = new() - { - Name = name, - ArchiveAfter = autoArchiveDuration, - RateLimitPerUser = rateLimitPerUser, - Message = new RestChannelMessageCreatePayload - { - Content = message.Content, - HasContent = !string.IsNullOrWhiteSpace(message.Content), - Embeds = message.Embeds, - HasEmbed = message.Embeds.Count > 0, - Mentions = new DiscordMentions(message.Mentions, message.Mentions.Any()), - Components = message.Components, - StickersIds = message.Stickers?.Select(s => s.Id) ?? Array.Empty(), - }, - AppliedTags = appliedTags - }; - - JObject ret; - RestResponse res; - if (message.Files.Count is 0) - { - RestRequest req = new() - { - Route = route, - Url = url, - Method = HttpMethod.Post, - Payload = DiscordJson.SerializeObject(pld) - }; - - res = await this.rest.ExecuteRequestAsync(req); - ret = JObject.Parse(res.Response!); - } - else - { - Dictionary values = new() - { - ["payload_json"] = DiscordJson.SerializeObject(pld) - }; - - MultipartRestRequest req = new() - { - Route = route, - Url = url, - Method = HttpMethod.Post, - Values = values, - Files = message.Files - }; - - res = await this.rest.ExecuteRequestAsync(req); - ret = JObject.Parse(res.Response!); - } - - JToken? msgToken = ret["message"]; - ret.Remove("message"); - - DiscordMessage msg = PrepareMessage(msgToken!); - // We know the return type; deserialize directly. - DiscordThreadChannel chn = ret.ToDiscordObject(); - chn.Discord = this.discord!; - - return new DiscordForumPostStarter(chn, msg); - } - - /// - /// Internal method to create an auto-moderation rule in a guild. - /// - /// The id of the guild where the rule will be created. - /// The rule name. - /// The Discord event that will trigger the rule. - /// The rule trigger. - /// The trigger metadata. - /// The actions that will run when a rule is triggered. - /// Whenever the rule is enabled or not. - /// The exempted roles that will not trigger the rule. - /// The exempted channels that will not trigger the rule. - /// The reason for audits logs. - /// The created rule. - internal async ValueTask CreateGuildAutoModerationRuleAsync - ( - ulong guildId, - string name, - DiscordRuleEventType eventType, - DiscordRuleTriggerType triggerType, - DiscordRuleTriggerMetadata triggerMetadata, - IReadOnlyList actions, - Optional enabled = default, - Optional> exemptRoles = default, - Optional> exemptChannels = default, - string? reason = null - ) - { - string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.AUTO_MODERATION}/{Endpoints.RULES}"; - string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.AUTO_MODERATION}/{Endpoints.RULES}"; - - Dictionary headers = []; - if (!string.IsNullOrWhiteSpace(reason)) - { - headers[REASON_HEADER_NAME] = reason; - } - - string payload = DiscordJson.SerializeObject(new - { - guild_id = guildId, - name, - event_type = eventType, - trigger_type = triggerType, - trigger_metadata = triggerMetadata, - actions, - enabled, - exempt_roles = exemptRoles.Value.Select(x => x.Id).ToArray(), - exempt_channels = exemptChannels.Value.Select(x => x.Id).ToArray() - }); - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Post, - Headers = headers, - Payload = payload - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - DiscordAutoModerationRule rule = JsonConvert.DeserializeObject(res.Response!)!; - - return rule; - } - - /// - /// Internal method to get an auto-moderation rule in a guild. - /// - /// The guild id where the rule is in. - /// The rule id. - /// The rule found. - internal async ValueTask GetGuildAutoModerationRuleAsync - ( - ulong guildId, - ulong ruleId - ) - { - string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.AUTO_MODERATION}/{Endpoints.RULES}/:rule_id"; - string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.AUTO_MODERATION}/{Endpoints.RULES}/{ruleId}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Get - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - DiscordAutoModerationRule rule = JsonConvert.DeserializeObject(res.Response!)!; - - return rule; - } - - /// - /// Internal method to get all auto-moderation rules in a guild. - /// - /// The guild id where rules are in. - /// The rules found. - internal async ValueTask> GetGuildAutoModerationRulesAsync - ( - ulong guildId - ) - { - string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.AUTO_MODERATION}/{Endpoints.RULES}"; - string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.AUTO_MODERATION}/{Endpoints.RULES}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Get - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - IReadOnlyList rules = JsonConvert.DeserializeObject>(res.Response!)!; - - return rules; - } - - /// - /// Internal method to modify an auto-moderation rule in a guild. - /// - /// The id of the guild where the rule will be modified. - /// The id of the rule that will be modified. - /// The rule name. - /// The Discord event that will trigger the rule. - /// The trigger metadata. - /// The actions that will run when a rule is triggered. - /// Whenever the rule is enabled or not. - /// The exempted roles that will not trigger the rule. - /// The exempted channels that will not trigger the rule. - /// The reason for audits logs. - /// The modified rule. - internal async ValueTask ModifyGuildAutoModerationRuleAsync - ( - ulong guildId, - ulong ruleId, - Optional name, - Optional eventType, - Optional triggerMetadata, - Optional> actions, - Optional enabled, - Optional> exemptRoles, - Optional> exemptChannels, - string? reason = null - ) - { - string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.AUTO_MODERATION}/{Endpoints.RULES}/:rule_id"; - string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.AUTO_MODERATION}/{Endpoints.RULES}/{ruleId}"; - - Dictionary headers = []; - if (!string.IsNullOrWhiteSpace(reason)) - { - headers[REASON_HEADER_NAME] = reason; - } - - string payload = DiscordJson.SerializeObject(new - { - name, - event_type = eventType, - trigger_metadata = triggerMetadata, - actions, - enabled, - exempt_roles = exemptRoles.Value.Select(x => x.Id).ToArray(), - exempt_channels = exemptChannels.Value.Select(x => x.Id).ToArray() - }); - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Patch, - Headers = headers, - Payload = payload - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - DiscordAutoModerationRule rule = JsonConvert.DeserializeObject(res.Response!)!; - - return rule; - } - - /// - /// Internal method to delete an auto-moderation rule in a guild. - /// - /// The id of the guild where the rule is in. - /// The rule id that will be deleted. - /// The reason for audits logs. - internal async ValueTask DeleteGuildAutoModerationRuleAsync - ( - ulong guildId, - ulong ruleId, - string? reason = null - ) - { - string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.AUTO_MODERATION}/{Endpoints.RULES}/:rule_id"; - string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.AUTO_MODERATION}/{Endpoints.RULES}/{ruleId}"; - - Dictionary headers = []; - if (!string.IsNullOrWhiteSpace(reason)) - { - headers[REASON_HEADER_NAME] = reason; - } - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Delete, - Headers = headers - }; - - await this.rest.ExecuteRequestAsync(request); - } - - /// - /// Internal method to get all SKUs belonging to a specific application - /// - /// Id of the application of which SKUs should be returned - /// Returns a list of SKUs - internal async ValueTask> ListStockKeepingUnitsAsync(ulong applicationId) - { - string route = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.SKUS}"; - string url = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.SKUS}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Get - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - IReadOnlyList stockKeepingUnits = JsonConvert.DeserializeObject>(res.Response!)!; - - return stockKeepingUnits; - } - - /// - /// Returns all entitlements for a given app. - /// - /// Application ID to look up entitlements for - /// User ID to look up entitlements for - /// Optional list of SKU IDs to check entitlements for - /// Retrieve entitlements before this entitlement ID - /// Retrieve entitlements after this entitlement ID - /// Guild ID to look up entitlements for - /// Whether or not ended entitlements should be omitted - /// Number of entitlements to return, 1-100, default 100 - /// Returns the list of entitlments. Sorted by id descending (depending on discord) - internal async ValueTask> ListEntitlementsAsync - ( - ulong applicationId, - ulong? userId = null, - IEnumerable? skuIds = null, - ulong? before = null, - ulong? after = null, - ulong? guildId = null, - bool? excludeEnded = null, - int? limit = 100 - ) - { - string route = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.ENTITLEMENTS}"; - string url = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.ENTITLEMENTS}"; - - QueryUriBuilder builder = new(url); - - if (userId is not null) - { - builder.AddParameter("user_id", userId.ToString()); - } - - if (skuIds is not null) - { - builder.AddParameter("sku_ids", string.Join(",", skuIds.Select(x => x.ToString()))); - } - - if (before is not null) - { - builder.AddParameter("before", before.ToString()); - } - - if (after is not null) - { - builder.AddParameter("after", after.ToString()); - } - - if (limit is not null) - { - ArgumentOutOfRangeException.ThrowIfGreaterThan(limit.Value, 100); - ArgumentOutOfRangeException.ThrowIfNegativeOrZero(limit.Value); - - builder.AddParameter("limit", limit.ToString()); - } - - if (guildId is not null) - { - builder.AddParameter("guild_id", guildId.ToString()); - } - - if (excludeEnded is not null) - { - builder.AddParameter("exclude_ended", excludeEnded.ToString()); - } - - RestRequest request = new() - { - Route = route, - Url = builder.ToString(), - Method = HttpMethod.Get - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - IReadOnlyList entitlements = JsonConvert.DeserializeObject>(res.Response!)!; - - return entitlements; - } - - /// - /// For One-Time Purchase consumable SKUs, marks a given entitlement for the user as consumed. - /// - /// The id of the application the entitlement belongs to - /// The id of the entitlement which will be marked as consumed - internal async ValueTask ConsumeEntitlementAsync(ulong applicationId, ulong entitlementId) - { - string route = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.ENTITLEMENTS}/:entitlementId/{Endpoints.CONSUME}"; - string url = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.ENTITLEMENTS}/{entitlementId}/{Endpoints.CONSUME}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Post - }; - - await this.rest.ExecuteRequestAsync(request); - } - - /// - /// Create a test entitlement which can be granted to a user or a guild - /// - /// The id of the application the SKU belongs to - /// The id of the SKU the entitlement belongs to - /// The id of the entity which should recieve the entitlement - /// The type of the entity which should recieve the entitlement - /// Returns a partial entitlment - internal async ValueTask CreateTestEntitlementAsync - ( - ulong applicationId, - ulong skuId, - ulong ownerId, - DiscordTestEntitlementOwnerType ownerType - ) - { - string route = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.ENTITLEMENTS}"; - string url = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.ENTITLEMENTS}"; - - string payload = DiscordJson.SerializeObject( - new RestCreateTestEntitlementPayload() { SkuId = skuId, OwnerId = ownerId, OwnerType = ownerType }); - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Post, - Payload = payload - }; - - RestResponse res = await this.rest.ExecuteRequestAsync(request); - DiscordEntitlement entitlement = JsonConvert.DeserializeObject(res.Response!)!; - - return entitlement; - } - - /// - /// Deletes a test entitlement - /// - /// The id of the application the entitlement belongs to - /// The id of the test entitlement which should be removed - internal async ValueTask DeleteTestEntitlementAsync - ( - ulong applicationId, - ulong entitlementId - ) - { - string route = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.ENTITLEMENTS}/:entitlementId"; - string url = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.ENTITLEMENTS}/{entitlementId}"; - - RestRequest request = new() - { - Route = route, - Url = url, - Method = HttpMethod.Delete - }; - - await this.rest.ExecuteRequestAsync(request); - } -} +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using DSharpPlus.Entities; +using DSharpPlus.Entities.AuditLogs; +using DSharpPlus.Exceptions; +using DSharpPlus.Metrics; +using DSharpPlus.Net.Abstractions; +using DSharpPlus.Net.Serialization; + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace DSharpPlus.Net; + +// huge credits to dvoraks 8th symphony for being a source of sanity in the trying times of +// fixing this absolute catastrophy up at least somewhat + +public sealed class DiscordApiClient +{ + private const string REASON_HEADER_NAME = "X-Audit-Log-Reason"; + + internal BaseDiscordClient? discord; + internal RestClient rest; + + [ActivatorUtilitiesConstructor] + public DiscordApiClient(RestClient rest) => this.rest = rest; + + // This is for meta-clients, such as the webhook client + internal DiscordApiClient(TimeSpan timeout, ILogger logger) + => this.rest = new(new(), timeout, logger); + + /// + internal RequestMetricsCollection GetRequestMetrics(bool sinceLastCall = false) + => this.rest.GetRequestMetrics(sinceLastCall); + + internal void SetClient(BaseDiscordClient client) + => this.discord = client; + + private DiscordMessage PrepareMessage(JToken msgRaw) + { + TransportUser author = msgRaw["author"]!.ToDiscordObject(); + DiscordMessage message = msgRaw.ToDiscordObject(); + message.Discord = this.discord!; + PopulateMessage(author, message); + + JToken? referencedMsg = msgRaw["referenced_message"]; + if (message.MessageType == DiscordMessageType.Reply && referencedMsg is not null && message.ReferencedMessage is not null) + { + TransportUser referencedAuthor = referencedMsg["author"]!.ToDiscordObject(); + message.ReferencedMessage.Discord = this.discord!; + PopulateMessage(referencedAuthor, message.ReferencedMessage); + } + + return message; + } + + private void PopulateMessage(TransportUser author, DiscordMessage ret) + { + if (ret.Channel is null && ret.Discord is DiscordClient client) + { + ret.Channel = client.InternalGetCachedChannel(ret.ChannelId); + } + + if (ret.guildId is null || !ret.Discord.Guilds.TryGetValue(ret.guildId.Value, out DiscordGuild? guild)) + { + guild = ret.Channel?.Guild; + } + + ret.guildId ??= guild?.Id; + + // I can't think of a case where guildId will never be not null since the guildId is a gateway exclusive + // property, however if that property is added later to the rest api response, this case would be hit. + ret.Channel ??= ret.guildId is null + ? new DiscordDmChannel + { + Id = ret.ChannelId, + Discord = this.discord!, + Type = DiscordChannelType.Private + } + : new DiscordChannel + { + Id = ret.ChannelId, + GuildId = ret.guildId, + Discord = this.discord! + }; + + //If this is a webhook, it shouldn't be in the user cache. + if (author.IsBot && int.Parse(author.Discriminator) == 0) + { + ret.Author = new(author) + { + Discord = this.discord! + }; + } + else + { + // get and cache the user + if (!this.discord!.UserCache.TryGetValue(author.Id, out DiscordUser? user)) + { + user = new DiscordUser(author) + { + Discord = this.discord + }; + } + + this.discord.UserCache[author.Id] = user; + + // get the member object if applicable, if not set the message author to an user + if (guild is not null) + { + if (!guild.Members.TryGetValue(author.Id, out DiscordMember? member)) + { + member = new(user) + { + Discord = this.discord, + guild_id = guild.Id + }; + } + + ret.Author = member; + } + else + { + ret.Author = user!; + } + } + + ret.PopulateMentions(); + + ret.reactions ??= []; + foreach (DiscordReaction reaction in ret.reactions) + { + reaction.Emoji.Discord = this.discord!; + } + + if(ret.MessageSnapshots != null) + { + foreach (DiscordMessageSnapshot snapshot in ret.MessageSnapshots) + { + snapshot.Message?.PopulateMentions(); + } + } + } + + #region Guild + + internal async ValueTask> GetGuildsAsync + ( + int? limit = null, + ulong? before = null, + ulong? after = null, + bool? withCounts = null + ) + { + QueryUriBuilder builder = new($"{Endpoints.USERS}/@me/{Endpoints.GUILDS}"); + + if (limit is not null) + { + if (limit is < 1 or > 200) + { + throw new ArgumentOutOfRangeException(nameof(limit), "Limit must be a number between 1 and 200."); + } + builder.AddParameter("limit", limit.Value.ToString(CultureInfo.InvariantCulture)); + } + + if (before is not null) + { + builder.AddParameter("before", before.Value.ToString(CultureInfo.InvariantCulture)); + } + + if (after is not null) + { + builder.AddParameter("after", after.Value.ToString(CultureInfo.InvariantCulture)); + } + + if (withCounts is not null) + { + builder.AddParameter("with_counts", withCounts.Value.ToString(CultureInfo.InvariantCulture)); + } + + RestRequest request = new() + { + Route = $"/{Endpoints.USERS}/@me/{Endpoints.GUILDS}", + Url = builder.Build(), + Method = HttpMethod.Get + }; + + RestResponse response = await this.rest.ExecuteRequestAsync(request); + + JArray jArray = JArray.Parse(response.Response!); + + List guilds = new(200); + + foreach (JToken token in jArray) + { + DiscordGuild guildRest = token.ToDiscordObject(); + + if (guildRest.roles is not null) + { + foreach (DiscordRole role in guildRest.roles.Values) + { + role.guild_id = guildRest.Id; + role.Discord = this.discord!; + } + } + + guildRest.Discord = this.discord!; + guilds.Add(guildRest); + } + + return guilds; + } + + internal async ValueTask> SearchMembersAsync + ( + ulong guildId, + string name, + int? limit = null + ) + { + QueryUriBuilder builder = new($"{Endpoints.GUILDS}/{guildId}/{Endpoints.MEMBERS}/{Endpoints.SEARCH}"); + builder.AddParameter("query", name); + + if (limit is not null) + { + builder.AddParameter("limit", limit.Value.ToString(CultureInfo.InvariantCulture)); + } + + RestRequest request = new() + { + Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.MEMBERS}/{Endpoints.SEARCH}", + Url = builder.Build(), + Method = HttpMethod.Get + }; + + RestResponse response = await this.rest.ExecuteRequestAsync(request); + + JArray array = JArray.Parse(response.Response!); + IReadOnlyList transportMembers = array.ToDiscordObject>(); + + List members = []; + + foreach (TransportMember transport in transportMembers) + { + DiscordUser usr = new(transport.User) { Discord = this.discord! }; + + this.discord!.UpdateUserCache(usr); + + members.Add(new DiscordMember(transport) { Discord = this.discord, guild_id = guildId }); + } + + return members; + } + + internal async ValueTask GetGuildBanAsync + ( + ulong guildId, + ulong userId + ) + { + RestRequest request = new() + { + Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.BANS}/:user_id", + Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.BANS}/{userId}", + Method = HttpMethod.Get + }; + + RestResponse response = await this.rest.ExecuteRequestAsync(request); + + JObject json = JObject.Parse(response.Response!); + + DiscordBan ban = json.ToDiscordObject(); + + if (!this.discord!.TryGetCachedUserInternal(ban.RawUser.Id, out DiscordUser? user)) + { + user = new DiscordUser(ban.RawUser) { Discord = this.discord }; + user = this.discord.UpdateUserCache(user); + } + + ban.User = user; + + return ban; + } + + internal async ValueTask CreateGuildAsync + ( + string name, + string regionId, + Optional iconb64 = default, + DiscordVerificationLevel? verificationLevel = null, + DiscordDefaultMessageNotifications? defaultMessageNotifications = null, + DiscordSystemChannelFlags? systemChannelFlags = null + ) + { + RestGuildCreatePayload payload = new() + { + Name = name, + RegionId = regionId, + DefaultMessageNotifications = defaultMessageNotifications, + VerificationLevel = verificationLevel, + IconBase64 = iconb64, + SystemChannelFlags = systemChannelFlags + }; + + RestRequest request = new() + { + Route = $"{Endpoints.GUILDS}", + Url = $"{Endpoints.GUILDS}", + Payload = DiscordJson.SerializeObject(payload), + Method = HttpMethod.Post + }; + + RestResponse response = await this.rest.ExecuteRequestAsync(request); + + JObject json = JObject.Parse(response.Response!); + JArray rawMembers = (JArray)json["members"]!; + DiscordGuild guild = json.ToDiscordObject(); + + if (this.discord is DiscordClient dc) + { + // this looks wrong. TODO: investigate double-fired event? + await dc.OnGuildCreateEventAsync(guild, rawMembers, null!); + } + + return guild; + } + + internal async ValueTask CreateGuildFromTemplateAsync + ( + string templateCode, + string name, + Optional iconb64 = default + ) + { + RestGuildCreateFromTemplatePayload payload = new() + { + Name = name, + IconBase64 = iconb64 + }; + + RestRequest request = new() + { + Route = $"{Endpoints.GUILDS}/{Endpoints.TEMPLATES}/:template_code", + Url = $"{Endpoints.GUILDS}/{Endpoints.TEMPLATES}/{templateCode}", + Payload = DiscordJson.SerializeObject(payload), + Method = HttpMethod.Post + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + JObject json = JObject.Parse(res.Response!); + JArray rawMembers = (JArray)json["members"]!; + DiscordGuild guild = json.ToDiscordObject(); + + if (this.discord is DiscordClient dc) + { + await dc.OnGuildCreateEventAsync(guild, rawMembers, null!); + } + + return guild; + } + + internal async ValueTask DeleteGuildAsync + ( + ulong guildId + ) + { + RestRequest request = new() + { + Route = $"{Endpoints.GUILDS}/{guildId}", + Url = $"{Endpoints.GUILDS}/{guildId}", + Method = HttpMethod.Delete + }; + + _ = await this.rest.ExecuteRequestAsync(request); + } + + internal async ValueTask ModifyGuildAsync + ( + ulong guildId, + Optional name = default, + Optional region = default, + Optional verificationLevel = default, + Optional defaultMessageNotifications = default, + Optional mfaLevel = default, + Optional explicitContentFilter = default, + Optional afkChannelId = default, + Optional afkTimeout = default, + Optional iconb64 = default, + Optional ownerId = default, + Optional splashb64 = default, + Optional systemChannelId = default, + Optional banner = default, + Optional description = default, + Optional discoverySplash = default, + Optional> features = default, + Optional preferredLocale = default, + Optional publicUpdatesChannelId = default, + Optional rulesChannelId = default, + Optional systemChannelFlags = default, + string? reason = null + ) + { + RestGuildModifyPayload payload = new() + { + Name = name, + RegionId = region, + VerificationLevel = verificationLevel, + DefaultMessageNotifications = defaultMessageNotifications, + MfaLevel = mfaLevel, + ExplicitContentFilter = explicitContentFilter, + AfkChannelId = afkChannelId, + AfkTimeout = afkTimeout, + IconBase64 = iconb64, + SplashBase64 = splashb64, + OwnerId = ownerId, + SystemChannelId = systemChannelId, + Banner = banner, + Description = description, + DiscoverySplash = discoverySplash, + Features = features, + PreferredLocale = preferredLocale, + PublicUpdatesChannelId = publicUpdatesChannelId, + RulesChannelId = rulesChannelId, + SystemChannelFlags = systemChannelFlags + }; + + RestRequest request = new() + { + Route = $"{Endpoints.GUILDS}/{guildId}", + Url = $"{Endpoints.GUILDS}/{guildId}", + Method = HttpMethod.Patch, + Payload = DiscordJson.SerializeObject(payload), + Headers = string.IsNullOrWhiteSpace(reason) + ? null + : new Dictionary + { + [REASON_HEADER_NAME] = reason + } + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + JObject json = JObject.Parse(res.Response!); + JArray rawMembers = (JArray)json["members"]!; + DiscordGuild guild = json.ToDiscordObject(); + foreach (DiscordRole r in guild.roles.Values) + { + r.guild_id = guild.Id; + } + + if (this.discord is DiscordClient dc) + { + await dc.OnGuildUpdateEventAsync(guild, rawMembers!); + } + + return guild; + } + + internal async ValueTask> GetGuildBansAsync + ( + ulong guildId, + int? limit = null, + ulong? before = null, + ulong? after = null + ) + { + QueryUriBuilder builder = new($"{Endpoints.GUILDS}/{guildId}/{Endpoints.BANS}"); + + if (limit is not null) + { + builder.AddParameter("limit", limit.Value.ToString(CultureInfo.InvariantCulture)); + } + + if (before is not null) + { + builder.AddParameter("before", before.Value.ToString(CultureInfo.InvariantCulture)); + } + + if (after is not null) + { + builder.AddParameter("after", after.Value.ToString(CultureInfo.InvariantCulture)); + } + + RestRequest request = new() + { + Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.BANS}", + Url = builder.Build(), + Method = HttpMethod.Get + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + IEnumerable bansRaw = JsonConvert.DeserializeObject>(res.Response!)! + .Select(xb => + { + if (!this.discord!.TryGetCachedUserInternal(xb.RawUser.Id, out DiscordUser? user)) + { + user = new DiscordUser(xb.RawUser) { Discord = this.discord }; + user = this.discord.UpdateUserCache(user); + } + + xb.User = user; + return xb; + }); + + ReadOnlyCollection bans = new(new List(bansRaw)); + + return bans; + } + + internal async ValueTask CreateGuildBanAsync + ( + ulong guildId, + ulong userId, + int deleteMessageSeconds, + string? reason = null + ) + { + if (deleteMessageSeconds is < 0 or > 604800) + { + throw new ArgumentException("Delete message seconds must be a number between 0 and 604800 (7 Days).", nameof(deleteMessageSeconds)); + } + + QueryUriBuilder builder = new($"{Endpoints.GUILDS}/{guildId}/{Endpoints.BANS}/{userId}"); + + builder.AddParameter("delete_message_seconds", deleteMessageSeconds.ToString(CultureInfo.InvariantCulture)); + + RestRequest request = new() + { + Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.BANS}/:user_id", + Url = builder.Build(), + Method = HttpMethod.Put, + Headers = string.IsNullOrWhiteSpace(reason) + ? null + : new Dictionary + { + [REASON_HEADER_NAME] = reason + } + }; + + _ = await this.rest.ExecuteRequestAsync(request); + } + + internal async ValueTask RemoveGuildBanAsync + ( + ulong guildId, + ulong userId, + string? reason = null + ) + { + RestRequest request = new() + { + Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.BANS}/:user_id", + Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.BANS}/{userId}", + Method = HttpMethod.Delete, + Headers = string.IsNullOrWhiteSpace(reason) + ? null + : new Dictionary + { + [REASON_HEADER_NAME] = reason + } + }; + + _ = await this.rest.ExecuteRequestAsync(request); + } + + internal async ValueTask CreateGuildBulkBanAsync(ulong guildId, IEnumerable userIds, int? deleteMessagesSeconds = null, string? reason = null) + { + if (userIds.TryGetNonEnumeratedCount(out int count) && count > 200) + { + throw new ArgumentException("You can only ban up to 200 users at once."); + } + else if (userIds.Count() > 200) + { + throw new ArgumentException("You can only ban up to 200 users at once."); + } + + if (deleteMessagesSeconds is not null and (< 0 or > 604800)) + { + throw new ArgumentException("Delete message seconds must be a number between 0 and 604800 (7 days).", nameof(deleteMessagesSeconds)); + } + + RestRequest request = new() + { + Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.BULK_BAN}", + Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.BULK_BAN}", + Method = HttpMethod.Post, + Headers = string.IsNullOrWhiteSpace(reason) + ? null + : new Dictionary + { + [REASON_HEADER_NAME] = reason + }, + Payload = DiscordJson.SerializeObject(new RestGuildBulkBanPayload + { + DeleteMessageSeconds = deleteMessagesSeconds, + UserIds = userIds + }) + }; + + RestResponse response = await this.rest.ExecuteRequestAsync(request); + + DiscordBulkBan bulkBan = JsonConvert.DeserializeObject(response.Response!)!; + + List bannedUsers = new(bulkBan.BannedUserIds.Count()); + foreach (ulong userId in bulkBan.BannedUserIds) + { + if (!this.discord!.TryGetCachedUserInternal(userId, out DiscordUser? user)) + { + user = new DiscordUser(new TransportUser { Id = userId }) { Discord = this.discord }; + user = this.discord.UpdateUserCache(user); + } + + bannedUsers.Add(user); + } + bulkBan.BannedUsers = bannedUsers; + + List failedUsers = new(bulkBan.FailedUserIds.Count()); + foreach (ulong userId in bulkBan.FailedUserIds) + { + if (!this.discord!.TryGetCachedUserInternal(userId, out DiscordUser? user)) + { + user = new DiscordUser(new TransportUser { Id = userId }) { Discord = this.discord }; + user = this.discord.UpdateUserCache(user); + } + + failedUsers.Add(user); + } + bulkBan.FailedUsers = failedUsers; + + return bulkBan; + } + + internal async ValueTask LeaveGuildAsync + ( + ulong guildId + ) + { + RestRequest request = new() + { + Route = $"{Endpoints.USERS}/{Endpoints.ME}/{Endpoints.GUILDS}/{guildId}", + Url = $"{Endpoints.USERS}/{Endpoints.ME}/{Endpoints.GUILDS}/{guildId}", + Method = HttpMethod.Delete + }; + + _ = await this.rest.ExecuteRequestAsync(request); + } + + internal async ValueTask AddGuildMemberAsync + ( + ulong guildId, + ulong userId, + string accessToken, + bool? muted = null, + bool? deafened = null, + string? nick = null, + IEnumerable? roles = null + ) + { + RestGuildMemberAddPayload payload = new() + { + AccessToken = accessToken, + Nickname = nick ?? "", + Roles = roles ?? [], + Deaf = deafened ?? false, + Mute = muted ?? false + }; + + RestRequest request = new() + { + Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.MEMBERS}/:user_id", + Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.MEMBERS}/{userId}", + Method = HttpMethod.Put, + Payload = DiscordJson.SerializeObject(payload) + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + if (res.ResponseCode == HttpStatusCode.NoContent) + { + // User was already in the guild, Discord doesn't return the member object in this case + return null; + } + + TransportMember transport = JsonConvert.DeserializeObject(res.Response!)!; + + DiscordUser usr = new(transport.User) { Discord = this.discord! }; + + this.discord!.UpdateUserCache(usr); + + return new DiscordMember(transport) { Discord = this.discord!, guild_id = guildId }; + } + + internal async ValueTask> ListGuildMembersAsync + ( + ulong guildId, + int? limit = null, + ulong? after = null + ) + { + QueryUriBuilder builder = new($"{Endpoints.GUILDS}/{guildId}/{Endpoints.MEMBERS}"); + + if (limit is not null and > 0) + { + builder.AddParameter("limit", limit.Value.ToString(CultureInfo.InvariantCulture)); + } + + if (after is not null) + { + builder.AddParameter("after", after.Value.ToString(CultureInfo.InvariantCulture)); + } + + RestRequest request = new() + { + Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.MEMBERS}", + Url = builder.Build(), + Method = HttpMethod.Get + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + List rawMembers = JsonConvert.DeserializeObject>(res.Response!)!; + return new ReadOnlyCollection(rawMembers); + } + + internal async ValueTask AddGuildMemberRoleAsync + ( + ulong guildId, + ulong userId, + ulong roleId, + string? reason = null + ) + { + RestRequest request = new() + { + Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.MEMBERS}/:user_id/{Endpoints.ROLES}/:role_id", + Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.MEMBERS}/{userId}/{Endpoints.ROLES}/{roleId}", + Method = HttpMethod.Put, + Headers = string.IsNullOrWhiteSpace(reason) + ? null + : new Dictionary + { + [REASON_HEADER_NAME] = reason + } + }; + + _ = await this.rest.ExecuteRequestAsync(request); + } + + internal async ValueTask RemoveGuildMemberRoleAsync + ( + ulong guildId, + ulong userId, + ulong roleId, + string reason + ) + { + RestRequest request = new() + { + Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.MEMBERS}/:user_id/{Endpoints.ROLES}/:role_id", + Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.MEMBERS}/{userId}/{Endpoints.ROLES}/{roleId}", + Method = HttpMethod.Delete, + Headers = string.IsNullOrWhiteSpace(reason) + ? null + : new Dictionary + { + [REASON_HEADER_NAME] = reason + } + }; + + _ = await this.rest.ExecuteRequestAsync(request); + } + + internal async ValueTask ModifyGuildChannelPositionAsync + ( + ulong guildId, + IEnumerable payload, + string? reason = null + ) + { + RestRequest request = new() + { + Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.CHANNELS}", + Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.CHANNELS}", + Method = HttpMethod.Patch, + Payload = DiscordJson.SerializeObject(payload), + Headers = string.IsNullOrWhiteSpace(reason) + ? null + : new Dictionary + { + [REASON_HEADER_NAME] = reason + } + }; + + await this.rest.ExecuteRequestAsync(request); + } + + // TODO: should probably return an IReadOnlyList here, unsure as to the extent of the breaking change + internal async ValueTask ModifyGuildRolePositionsAsync + ( + ulong guildId, + IEnumerable newRolePositions, + string? reason = null + ) + { + RestRequest request = new() + { + Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.ROLES}", + Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.ROLES}", + Method = HttpMethod.Patch, + Payload = DiscordJson.SerializeObject(newRolePositions), + Headers = string.IsNullOrWhiteSpace(reason) + ? null + : new Dictionary + { + [REASON_HEADER_NAME] = reason + } + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + DiscordRole[] ret = JsonConvert.DeserializeObject(res.Response!)!; + foreach (DiscordRole role in ret) + { + role.Discord = this.discord!; + role.guild_id = guildId; + } + + return ret; + } + + internal async ValueTask GetAuditLogsAsync + ( + ulong guildId, + int limit, + ulong? after = null, + ulong? before = null, + ulong? userId = null, + DiscordAuditLogActionType? actionType = null + ) + { + QueryUriBuilder builder = new($"{Endpoints.GUILDS}/{guildId}/{Endpoints.AUDIT_LOGS}"); + + builder.AddParameter("limit", limit.ToString(CultureInfo.InvariantCulture)); + + if (after is not null) + { + builder.AddParameter("after", after.Value.ToString(CultureInfo.InvariantCulture)); + } + + if (before is not null) + { + builder.AddParameter("before", before.Value.ToString(CultureInfo.InvariantCulture)); + } + + if (userId is not null) + { + builder.AddParameter("user_id", userId.Value.ToString(CultureInfo.InvariantCulture)); + } + + if (actionType is not null) + { + builder.AddParameter("action_type", ((int)actionType.Value).ToString(CultureInfo.InvariantCulture)); + } + + RestRequest request = new() + { + Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.AUDIT_LOGS}", + Url = builder.Build(), + Method = HttpMethod.Get + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + return JsonConvert.DeserializeObject(res.Response!)!; + } + + internal async ValueTask GetGuildVanityUrlAsync + ( + ulong guildId + ) + { + RestRequest request = new() + { + Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.VANITY_URL}", + Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.VANITY_URL}", + Method = HttpMethod.Get + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + return JsonConvert.DeserializeObject(res.Response!)!; + } + + internal async ValueTask GetGuildWidgetAsync + ( + ulong guildId + ) + { + RestRequest request = new() + { + Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.WIDGET_JSON}", + Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.WIDGET_JSON}", + Method = HttpMethod.Get + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + // TODO: this should really be cleaned up + JObject json = JObject.Parse(res.Response!); + JArray rawChannels = (JArray)json["channels"]!; + + DiscordWidget ret = json.ToDiscordObject(); + ret.Discord = this.discord!; + ret.Guild = this.discord!.Guilds[guildId]; + + ret.Channels = ret.Guild is null + ? rawChannels.Select(r => new DiscordChannel + { + Id = (ulong)r["id"]!, + Name = r["name"]!.ToString(), + Position = (int)r["position"]! + }).ToList() + : rawChannels.Select(r => + { + DiscordChannel c = ret.Guild.GetChannel((ulong)r["id"]!); + c.Position = (int)r["position"]!; + return c; + }).ToList(); + + return ret; + } + + internal async ValueTask GetGuildWidgetSettingsAsync + ( + ulong guildId + ) + { + RestRequest request = new() + { + Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.WIDGET}", + Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.WIDGET}", + Method = HttpMethod.Get + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + DiscordWidgetSettings ret = JsonConvert.DeserializeObject(res.Response!)!; + ret.Guild = this.discord!.Guilds[guildId]; + + return ret; + } + + internal async ValueTask ModifyGuildWidgetSettingsAsync + ( + ulong guildId, + bool? isEnabled = null, + ulong? channelId = null, + string? reason = null + ) + { + RestGuildWidgetSettingsPayload payload = new() + { + Enabled = isEnabled, + ChannelId = channelId + }; + + RestRequest request = new() + { + Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.WIDGET}", + Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.WIDGET}", + Method = HttpMethod.Patch, + Payload = DiscordJson.SerializeObject(payload), + Headers = reason is null + ? null + : new Dictionary + { + [REASON_HEADER_NAME] = reason + } + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + DiscordWidgetSettings ret = JsonConvert.DeserializeObject(res.Response!)!; + ret.Guild = this.discord!.Guilds[guildId]; + + return ret; + } + + internal async ValueTask> GetGuildTemplatesAsync + ( + ulong guildId + ) + { + RestRequest request = new() + { + Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.TEMPLATES}", + Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.TEMPLATES}", + Method = HttpMethod.Get + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + IEnumerable templates = + JsonConvert.DeserializeObject>(res.Response!)!; + + return new ReadOnlyCollection(new List(templates)); + } + + internal async ValueTask CreateGuildTemplateAsync + ( + ulong guildId, + string name, + string description + ) + { + RestGuildTemplateCreateOrModifyPayload payload = new() + { + Name = name, + Description = description + }; + + RestRequest request = new() + { + Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.TEMPLATES}", + Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.TEMPLATES}", + Method = HttpMethod.Post, + Payload = DiscordJson.SerializeObject(payload) + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + return JsonConvert.DeserializeObject(res.Response!)!; + } + + internal async ValueTask SyncGuildTemplateAsync + ( + ulong guildId, + string templateCode + ) + { + RestRequest request = new() + { + Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.TEMPLATES}/:template_code", + Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.TEMPLATES}/{templateCode}", + Method = HttpMethod.Put + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + return JsonConvert.DeserializeObject(res.Response!)!; + } + + internal async ValueTask ModifyGuildTemplateAsync + ( + ulong guildId, + string templateCode, + string? name = null, + string? description = null + ) + { + RestGuildTemplateCreateOrModifyPayload payload = new() + { + Name = name, + Description = description + }; + + RestRequest request = new() + { + Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.TEMPLATES}/:template_code", + Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.TEMPLATES}/{templateCode}", + Method = HttpMethod.Patch, + Payload = DiscordJson.SerializeObject(payload) + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + return JsonConvert.DeserializeObject(res.Response!)!; + } + + internal async ValueTask DeleteGuildTemplateAsync + ( + ulong guildId, + string templateCode + ) + { + RestRequest request = new() + { + Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.TEMPLATES}/:template_code", + Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.TEMPLATES}/{templateCode}", + Method = HttpMethod.Delete + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + return JsonConvert.DeserializeObject(res.Response!)!; + } + + internal async ValueTask GetGuildMembershipScreeningFormAsync + ( + ulong guildId + ) + { + RestRequest request = new() + { + Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.MEMBER_VERIFICATION}", + Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.MEMBER_VERIFICATION}", + Method = HttpMethod.Get + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + return JsonConvert.DeserializeObject(res.Response!)!; + } + + internal async ValueTask ModifyGuildMembershipScreeningFormAsync + ( + ulong guildId, + Optional enabled = default, + Optional fields = default, + Optional description = default + ) + { + RestGuildMembershipScreeningFormModifyPayload payload = new() + { + Enabled = enabled, + Description = description, + Fields = fields + }; + + RestRequest request = new() + { + Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.MEMBER_VERIFICATION}", + Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.MEMBER_VERIFICATION}", + Method = HttpMethod.Patch, + Payload = DiscordJson.SerializeObject(payload) + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + return JsonConvert.DeserializeObject(res.Response!)!; + } + + internal async ValueTask GetGuildWelcomeScreenAsync + ( + ulong guildId + ) + { + RestRequest request = new() + { + Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.WELCOME_SCREEN}", + Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.WELCOME_SCREEN}", + Method = HttpMethod.Get + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + return JsonConvert.DeserializeObject(res.Response!)!; + } + + internal async ValueTask ModifyGuildWelcomeScreenAsync + ( + ulong guildId, + Optional enabled = default, + Optional> welcomeChannels = default, + Optional description = default, + string? reason = null + ) + { + RestGuildWelcomeScreenModifyPayload payload = new() + { + Enabled = enabled, + WelcomeChannels = welcomeChannels, + Description = description + }; + + RestRequest request = new() + { + Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.WELCOME_SCREEN}", + Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.WELCOME_SCREEN}", + Method = HttpMethod.Patch, + Payload = DiscordJson.SerializeObject(payload), + Headers = reason is null + ? null + : new Dictionary + { + [REASON_HEADER_NAME] = reason + } + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + return JsonConvert.DeserializeObject(res.Response!)!; + } + + internal async ValueTask GetCurrentUserVoiceStateAsync(ulong guildId) + { + RestRequest request = new() + { + Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.VOICE_STATES}/:user_id", + Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.VOICE_STATES}/{Endpoints.ME}", + Method = HttpMethod.Get + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + DiscordVoiceState result = JsonConvert.DeserializeObject(res.Response!)!; + + result.Discord = this.discord!; + + return result; + } + + internal async ValueTask GetUserVoiceStateAsync(ulong guildId, ulong userId) + { + RestRequest request = new() + { + Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.VOICE_STATES}/:user_id", + Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.VOICE_STATES}/{userId}", + Method = HttpMethod.Get + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + DiscordVoiceState result = JsonConvert.DeserializeObject(res.Response!)!; + + result.Discord = this.discord!; + + return result; + } + + internal async ValueTask UpdateCurrentUserVoiceStateAsync + ( + ulong guildId, + ulong channelId, + bool? suppress = null, + DateTimeOffset? requestToSpeakTimestamp = null + ) + { + RestGuildUpdateCurrentUserVoiceStatePayload payload = new() + { + ChannelId = channelId, + Suppress = suppress, + RequestToSpeakTimestamp = requestToSpeakTimestamp + }; + + RestRequest request = new() + { + Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.VOICE_STATES}/@me", + Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.VOICE_STATES}/@me", + Method = HttpMethod.Patch, + Payload = DiscordJson.SerializeObject(payload) + }; + + _ = await this.rest.ExecuteRequestAsync(request); + } + + internal async ValueTask UpdateUserVoiceStateAsync + ( + ulong guildId, + ulong userId, + ulong channelId, + bool? suppress = null + ) + { + RestGuildUpdateUserVoiceStatePayload payload = new() + { + ChannelId = channelId, + Suppress = suppress + }; + + RestRequest request = new() + { + Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.VOICE_STATES}/:user_id", + Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.VOICE_STATES}/{userId}", + Method = HttpMethod.Patch, + Payload = DiscordJson.SerializeObject(payload) + }; + + _ = await this.rest.ExecuteRequestAsync(request); + } + #endregion + + #region Stickers + + internal async ValueTask GetGuildStickerAsync + ( + ulong guildId, + ulong stickerId + ) + { + RestRequest request = new() + { + Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.STICKERS}/:sticker_id", + Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.STICKERS}/{stickerId}", + Method = HttpMethod.Get + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + JObject json = JObject.Parse(res.Response!); + + DiscordMessageSticker ret = json.ToDiscordObject(); + + if (json["user"] is JObject jusr) // Null = Missing stickers perm // + { + TransportUser tsr = jusr.ToDiscordObject(); + DiscordUser usr = new(tsr) { Discord = this.discord! }; + ret.User = usr; + } + + ret.Discord = this.discord!; + return ret; + } + + internal async ValueTask GetStickerAsync + ( + ulong stickerId + ) + { + RestRequest request = new() + { + Route = $"{Endpoints.STICKERS}/:sticker_id", + Url = $"{Endpoints.STICKERS}/{stickerId}", + Method = HttpMethod.Get + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + JObject json = JObject.Parse(res.Response!); + + DiscordMessageSticker ret = json.ToDiscordObject(); + + if (json["user"] is JObject jusr) // Null = Missing stickers perm // + { + TransportUser tsr = jusr.ToDiscordObject(); + DiscordUser usr = new(tsr) { Discord = this.discord! }; + ret.User = usr; + } + + ret.Discord = this.discord!; + return ret; + } + + internal async ValueTask> GetStickerPacksAsync() + { + RestRequest request = new() + { + Route = $"{Endpoints.STICKERPACKS}", + Url = $"{Endpoints.STICKERPACKS}", + Method = HttpMethod.Get + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + JArray json = (JArray)JObject.Parse(res.Response!)["sticker_packs"]!; + DiscordMessageStickerPack[] ret = json.ToDiscordObject(); + + return ret; + } + + internal async ValueTask> GetGuildStickersAsync + ( + ulong guildId + ) + { + RestRequest request = new() + { + Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.STICKERS}", + Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.STICKERS}", + Method = HttpMethod.Get + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + JArray json = JArray.Parse(res.Response!); + + DiscordMessageSticker[] ret = json.ToDiscordObject(); + + for (int i = 0; i < ret.Length; i++) + { + DiscordMessageSticker sticker = ret[i]; + sticker.Discord = this.discord!; + + if (json[i]["user"] is JObject jusr) // Null = Missing stickers perm // + { + TransportUser transportUser = jusr.ToDiscordObject(); + DiscordUser user = new(transportUser) + { + Discord = this.discord! + }; + + // The sticker would've already populated, but this is just to ensure everything is up to date + sticker.User = user; + } + } + + return ret; + } + + internal async ValueTask CreateGuildStickerAsync + ( + ulong guildId, + string name, + string description, + string tags, + DiscordMessageFile file, + string? reason = null + ) + { + MultipartRestRequest request = new() + { + Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.STICKERS}", + Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.STICKERS}", + Method = HttpMethod.Post, + Headers = reason is null + ? null + : new Dictionary() + { + [REASON_HEADER_NAME] = reason + }, + Files = new DiscordMessageFile[] + { + file + }, + Values = new Dictionary() + { + ["name"] = name, + ["description"] = description, + ["tags"] = tags, + } + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + JObject json = JObject.Parse(res.Response!); + + DiscordMessageSticker ret = json.ToDiscordObject(); + + if (json["user"] is JObject rawUser) // Null = Missing stickers perm // + { + TransportUser transportUser = rawUser.ToDiscordObject(); + + DiscordUser user = new(transportUser) + { + Discord = this.discord! + }; + + ret.User = user; + } + + ret.Discord = this.discord!; + + return ret; + } + + internal async ValueTask ModifyStickerAsync + ( + ulong guildId, + ulong stickerId, + Optional name = default, + Optional description = default, + Optional tags = default, + string? reason = null + ) + { + RestStickerModifyPayload payload = new() + { + Name = name, + Description = description, + Tags = tags + }; + + RestRequest request = new() + { + Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.STICKERS}/:sticker_id", + Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.STICKERS}/{stickerId}", + Method = HttpMethod.Patch, + Payload = DiscordJson.SerializeObject(payload), + Headers = reason is null + ? null + : new Dictionary + { + [REASON_HEADER_NAME] = reason + } + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + DiscordMessageSticker ret = JObject.Parse(res.Response!).ToDiscordObject(); + ret.Discord = this.discord!; + + return ret; + } + + internal async ValueTask DeleteStickerAsync + ( + ulong guildId, + ulong stickerId, + string? reason = null + ) + { + RestRequest request = new() + { + Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.STICKERS}/:sticker_id", + Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.STICKERS}/{stickerId}", + Method = HttpMethod.Delete, + Headers = reason is null + ? null + : new Dictionary() + { + [REASON_HEADER_NAME] = reason + } + }; + + await this.rest.ExecuteRequestAsync(request); + } + + #endregion + + #region Channel + internal async ValueTask CreateGuildChannelAsync + ( + ulong guildId, + string name, + DiscordChannelType type, + ulong? parent, + Optional topic, + int? bitrate, + int? userLimit, + IEnumerable? overwrites, + bool? nsfw, + Optional perUserRateLimit, + DiscordVideoQualityMode? qualityMode, + int? position, + string reason, + DiscordAutoArchiveDuration? defaultAutoArchiveDuration, + DefaultReaction? defaultReactionEmoji, + IEnumerable? forumTags, + DiscordDefaultSortOrder? defaultSortOrder + + ) + { + List restOverwrites = []; + if (overwrites != null) + { + foreach (DiscordOverwriteBuilder ow in overwrites) + { + restOverwrites.Add(ow.Build()); + } + } + + RestChannelCreatePayload pld = new() + { + Name = name, + Type = type, + Parent = parent, + Topic = topic, + Bitrate = bitrate, + UserLimit = userLimit, + PermissionOverwrites = restOverwrites, + Nsfw = nsfw, + PerUserRateLimit = perUserRateLimit, + QualityMode = qualityMode, + Position = position, + DefaultAutoArchiveDuration = defaultAutoArchiveDuration, + DefaultReaction = defaultReactionEmoji, + AvailableTags = forumTags, + DefaultSortOrder = defaultSortOrder + }; + + Dictionary headers = []; + if (!string.IsNullOrWhiteSpace(reason)) + { + headers[REASON_HEADER_NAME] = reason; + } + + RestRequest request = new() + { + Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.CHANNELS}", + Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.CHANNELS}", + Method = HttpMethod.Post, + Payload = DiscordJson.SerializeObject(pld), + Headers = headers + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + DiscordChannel ret = JsonConvert.DeserializeObject(res.Response!)!; + ret.Discord = this.discord!; + + foreach (DiscordOverwrite xo in ret.permissionOverwrites) + { + xo.Discord = this.discord!; + xo.channelId = ret.Id; + } + + return ret; + } + + internal async ValueTask ModifyChannelAsync + ( + ulong channelId, + string name, + int? position = null, + Optional topic = default, + bool? nsfw = null, + Optional parent = default, + int? bitrate = null, + int? userLimit = null, + Optional perUserRateLimit = default, + Optional rtcRegion = default, + DiscordVideoQualityMode? qualityMode = null, + Optional type = default, + IEnumerable? permissionOverwrites = null, + Optional flags = default, + IEnumerable? availableTags = null, + Optional defaultAutoArchiveDuration = default, + Optional defaultReactionEmoji = default, + Optional defaultPerUserRatelimit = default, + Optional defaultSortOrder = default, + Optional defaultForumLayout = default, + string? reason = null + ) + { + List? restOverwrites = null; + if (permissionOverwrites is not null) + { + restOverwrites = []; + foreach (DiscordOverwriteBuilder ow in permissionOverwrites) + { + restOverwrites.Add(ow.Build()); + } + } + + RestChannelModifyPayload pld = new() + { + Name = name, + Position = position, + Topic = topic, + Nsfw = nsfw, + Parent = parent, + Bitrate = bitrate, + UserLimit = userLimit, + PerUserRateLimit = perUserRateLimit, + RtcRegion = rtcRegion, + QualityMode = qualityMode, + Type = type, + PermissionOverwrites = restOverwrites, + Flags = flags, + AvailableTags = availableTags, + DefaultAutoArchiveDuration = defaultAutoArchiveDuration, + DefaultReaction = defaultReactionEmoji, + DefaultPerUserRateLimit = defaultPerUserRatelimit, + DefaultForumLayout = defaultForumLayout, + DefaultSortOrder = defaultSortOrder + }; + + Dictionary headers = []; + if (!string.IsNullOrWhiteSpace(reason)) + { + headers[REASON_HEADER_NAME] = reason; + } + + RestRequest request = new() + { + Route = $"{Endpoints.CHANNELS}/{channelId}", + Url = $"{Endpoints.CHANNELS}/{channelId}", + Method = HttpMethod.Patch, + Payload = DiscordJson.SerializeObject(pld), + Headers = headers + }; + + await this.rest.ExecuteRequestAsync(request); + } + + internal async ValueTask ModifyThreadChannelAsync + ( + ulong channelId, + string name, + int? position = null, + Optional topic = default, + bool? nsfw = null, + Optional parent = default, + int? bitrate = null, + int? userLimit = null, + Optional perUserRateLimit = default, + Optional rtcRegion = default, + DiscordVideoQualityMode? qualityMode = null, + Optional type = default, + IEnumerable? permissionOverwrites = null, + bool? isArchived = null, + DiscordAutoArchiveDuration? autoArchiveDuration = null, + bool? locked = null, + IEnumerable? appliedTags = null, + bool? isInvitable = null, + string? reason = null + ) + { + List? restOverwrites = null; + if (permissionOverwrites is not null) + { + restOverwrites = []; + foreach (DiscordOverwriteBuilder ow in permissionOverwrites) + { + restOverwrites.Add(ow.Build()); + } + } + + RestThreadChannelModifyPayload pld = new() + { + Name = name, + Position = position, + Topic = topic, + Nsfw = nsfw, + Parent = parent, + Bitrate = bitrate, + UserLimit = userLimit, + PerUserRateLimit = perUserRateLimit, + RtcRegion = rtcRegion, + QualityMode = qualityMode, + Type = type, + PermissionOverwrites = restOverwrites, + IsArchived = isArchived, + ArchiveDuration = autoArchiveDuration, + Locked = locked, + IsInvitable = isInvitable, + AppliedTags = appliedTags + }; + + Dictionary headers = []; + if (!string.IsNullOrWhiteSpace(reason)) + { + headers.Add(REASON_HEADER_NAME, reason); + } + + RestRequest request = new() + { + Route = $"{Endpoints.CHANNELS}/{channelId}", + Url = $"{Endpoints.CHANNELS}/{channelId}", + Method = HttpMethod.Patch, + Payload = DiscordJson.SerializeObject(pld), + Headers = headers + }; + + await this.rest.ExecuteRequestAsync(request); + } + + internal async ValueTask> GetScheduledGuildEventsAsync + ( + ulong guildId, + bool withUserCounts = false + ) + { + QueryUriBuilder url = new($"{Endpoints.GUILDS}/{guildId}/{Endpoints.EVENTS}"); + url.AddParameter("with_user_count", withUserCounts.ToString()); + + RestRequest request = new() + { + Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.EVENTS}", + Url = url.Build(), + Method = HttpMethod.Get + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + DiscordScheduledGuildEvent[] ret = JsonConvert.DeserializeObject(res.Response!)!; + + foreach (DiscordScheduledGuildEvent? scheduledGuildEvent in ret) + { + scheduledGuildEvent.Discord = this.discord!; + + if (scheduledGuildEvent.Creator is not null) + { + scheduledGuildEvent.Creator.Discord = this.discord!; + } + } + + return ret.AsReadOnly(); + } + + internal async ValueTask CreateScheduledGuildEventAsync + ( + ulong guildId, + string name, + string description, + DateTimeOffset startTime, + DiscordScheduledGuildEventType type, + DiscordScheduledGuildEventPrivacyLevel privacyLevel, + DiscordScheduledGuildEventMetadata? metadata = null, + DateTimeOffset? endTime = null, + ulong? channelId = null, + Stream? image = null, + string? reason = null + ) + { + Dictionary headers = []; + + if (!string.IsNullOrWhiteSpace(reason)) + { + headers[REASON_HEADER_NAME] = reason; + } + + RestScheduledGuildEventCreatePayload pld = new() + { + Name = name, + Description = description, + ChannelId = channelId, + StartTime = startTime, + EndTime = endTime, + Type = type, + PrivacyLevel = privacyLevel, + Metadata = metadata + }; + + if (image is not null) + { + using InlineMediaTool imageTool = new(image); + + pld.CoverImage = imageTool.GetBase64(); + } + + RestRequest request = new() + { + Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.EVENTS}", + Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.EVENTS}", + Method = HttpMethod.Post, + Payload = DiscordJson.SerializeObject(pld), + Headers = headers + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + DiscordScheduledGuildEvent ret = JsonConvert.DeserializeObject(res.Response!)!; + + ret.Discord = this.discord!; + + if (ret.Creator is not null) + { + ret.Creator.Discord = this.discord!; + } + + return ret; + } + + internal async ValueTask DeleteScheduledGuildEventAsync + ( + ulong guildId, + ulong guildScheduledEventId, + string? reason = null + ) + { + RestRequest request = new() + { + Route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.EVENTS}/:guild_scheduled_event_id", + Url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.EVENTS}/{guildScheduledEventId}", + Method = HttpMethod.Delete, + Headers = new Dictionary + { + [REASON_HEADER_NAME] = reason + } + }; + + await this.rest.ExecuteRequestAsync(request); + } + + internal async ValueTask> GetScheduledGuildEventUsersAsync + ( + ulong guildId, + ulong guildScheduledEventId, + bool withMembers = false, + int limit = 100, + ulong? before = null, + ulong? after = null + ) + { + string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.EVENTS}/:guild_scheduled_event_id/{Endpoints.USERS}"; + + QueryUriBuilder url = new($"{Endpoints.GUILDS}/{guildId}/{Endpoints.EVENTS}/{guildScheduledEventId}/{Endpoints.USERS}"); + + url.AddParameter("with_members", withMembers.ToString()); + + if (limit > 0) + { + url.AddParameter("limit", limit.ToString(CultureInfo.InvariantCulture)); + } + + if (before != null) + { + url.AddParameter("before", before.Value.ToString(CultureInfo.InvariantCulture)); + } + + if (after != null) + { + url.AddParameter("after", after.Value.ToString(CultureInfo.InvariantCulture)); + } + + RestRequest request = new() + { + Route = route, + Url = url.Build(), + Method = HttpMethod.Get + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + JToken jto = JToken.Parse(res.Response!); + + return (jto as JArray ?? jto["users"] as JArray)! + .Select + ( + j => (DiscordUser)j.SelectToken("member")?.ToDiscordObject()! + ?? j.SelectToken("user")!.ToDiscordObject() + ) + .ToArray(); + } + + internal async ValueTask GetScheduledGuildEventAsync + ( + ulong guildId, + ulong guildScheduledEventId + ) + { + string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.EVENTS}/:guild_scheduled_event_id"; + string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.EVENTS}/{guildScheduledEventId}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Get + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + DiscordScheduledGuildEvent ret = JsonConvert.DeserializeObject(res.Response!)!; + + ret.Discord = this.discord!; + + if (ret.Creator is not null) + { + ret.Creator.Discord = this.discord!; + } + + return ret; + } + + internal async ValueTask ModifyScheduledGuildEventAsync + ( + ulong guildId, + ulong guildScheduledEventId, + Optional name = default, + Optional description = default, + Optional channelId = default, + Optional startTime = default, + Optional endTime = default, + Optional type = default, + Optional privacyLevel = default, + Optional metadata = default, + Optional status = default, + Optional coverImage = default, + string? reason = null + ) + { + string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.EVENTS}/:guild_scheduled_event_id"; + string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.EVENTS}/{guildScheduledEventId}"; + + Dictionary headers = []; + if (!string.IsNullOrWhiteSpace(reason)) + { + headers[REASON_HEADER_NAME] = reason; + } + + RestScheduledGuildEventModifyPayload pld = new() + { + Name = name, + Description = description, + ChannelId = channelId, + StartTime = startTime, + EndTime = endTime, + Type = type, + PrivacyLevel = privacyLevel, + Metadata = metadata, + Status = status + }; + + if (coverImage.HasValue) + { + using InlineMediaTool imageTool = new(coverImage.Value); + + pld.CoverImage = imageTool.GetBase64(); + } + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Patch, + Payload = DiscordJson.SerializeObject(pld), + Headers = headers + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + DiscordScheduledGuildEvent ret = JsonConvert.DeserializeObject(res.Response!)!; + + ret.Discord = this.discord!; + + if (ret.Creator is not null) + { + ret.Creator.Discord = this.discord!; + } + + return ret; + } + + internal async ValueTask GetChannelAsync + ( + ulong channelId + ) + { + string route = $"{Endpoints.CHANNELS}/{channelId}"; + string url = $"{Endpoints.CHANNELS}/{channelId}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Get + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + DiscordChannel ret = JsonConvert.DeserializeObject(res.Response!)!; + + // this is really weird, we should consider doing this better + if (ret.IsThread) + { + ret = JsonConvert.DeserializeObject(res.Response!)!; + } + + ret.Discord = this.discord!; + foreach (DiscordOverwrite xo in ret.permissionOverwrites) + { + xo.Discord = this.discord!; + xo.channelId = ret.Id; + } + + return ret; + } + + internal async ValueTask DeleteChannelAsync + ( + ulong channelId, + string reason + ) + { + Dictionary headers = []; + if (!string.IsNullOrWhiteSpace(reason)) + { + headers[REASON_HEADER_NAME] = reason; + } + + RestRequest request = new() + { + Route = $"{Endpoints.CHANNELS}/{channelId}", + Url = new($"{Endpoints.CHANNELS}/{channelId}"), + Method = HttpMethod.Delete, + Headers = headers + }; + + await this.rest.ExecuteRequestAsync(request); + } + + internal async ValueTask GetMessageAsync + ( + ulong channelId, + ulong messageId + ) + { + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/:message_id"; + string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/{messageId}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Get + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + DiscordMessage ret = PrepareMessage(JObject.Parse(res.Response!)); + + return ret; + } + + internal async ValueTask ForwardMessageAsync(ulong channelId, ulong originChannelId, ulong messageId) + { + RestChannelMessageCreatePayload pld = new() + { + HasContent = false, + MessageReference = new InternalDiscordMessageReference + { + MessageId = messageId, + ChannelId = originChannelId, + Type = DiscordMessageReferenceType.Forward + } + }; + + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}"; + string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Post, + Payload = DiscordJson.SerializeObject(pld) + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + DiscordMessage ret = PrepareMessage(JObject.Parse(res.Response!)); + + return ret; + } + + internal async ValueTask CreateMessageAsync + ( + ulong channelId, + string? content, + IEnumerable? embeds, + ulong? replyMessageId, + bool mentionReply, + bool failOnInvalidReply, + bool suppressNotifications + ) + { + if (content != null && content.Length > 2000) + { + throw new ArgumentException("Message content length cannot exceed 2000 characters."); + } + + if (!embeds?.Any() ?? true) + { + if (content == null) + { + throw new ArgumentException("You must specify message content or an embed."); + } + + if (content.Length == 0) + { + throw new ArgumentException("Message content must not be empty."); + } + } + + if (embeds is not null) + { + foreach (DiscordEmbed embed in embeds) + { + if (embed.Title?.Length > 256) + { + throw new ArgumentException("Embed title length must not exceed 256 characters."); + } + + if (embed.Description?.Length > 4096) + { + throw new ArgumentException("Embed description length must not exceed 4096 characters."); + } + + if (embed.Fields?.Count > 25) + { + throw new ArgumentException("Embed field count must not exceed 25."); + } + + if (embed.Fields is not null) + { + foreach (DiscordEmbedField field in embed.Fields) + { + if (field.Name.Length > 256) + { + throw new ArgumentException("Embed field name length must not exceed 256 characters."); + } + + if (field.Value.Length > 1024) + { + throw new ArgumentException("Embed field value length must not exceed 1024 characters."); + } + } + } + + if (embed.Footer?.Text.Length > 2048) + { + throw new ArgumentException("Embed footer text length must not exceed 2048 characters."); + } + + if (embed.Author?.Name.Length > 256) + { + throw new ArgumentException("Embed author name length must not exceed 256 characters."); + } + + int totalCharacter = 0; + totalCharacter += embed.Title?.Length ?? 0; + totalCharacter += embed.Description?.Length ?? 0; + totalCharacter += embed.Fields?.Sum(xf => xf.Name.Length + xf.Value.Length) ?? 0; + totalCharacter += embed.Footer?.Text.Length ?? 0; + totalCharacter += embed.Author?.Name.Length ?? 0; + if (totalCharacter > 6000) + { + throw new ArgumentException("Embed total length must not exceed 6000 characters."); + } + + if (embed.Timestamp != null) + { + embed.Timestamp = embed.Timestamp.Value.ToUniversalTime(); + } + } + } + + RestChannelMessageCreatePayload pld = new() + { + HasContent = content != null, + Content = content, + IsTTS = false, + HasEmbed = embeds?.Any() ?? false, + Embeds = embeds, + Flags = suppressNotifications ? DiscordMessageFlags.SuppressNotifications : 0, + }; + + if (replyMessageId != null) + { + pld.MessageReference = new InternalDiscordMessageReference + { + MessageId = replyMessageId, + FailIfNotExists = failOnInvalidReply + }; + } + + if (replyMessageId != null) + { + pld.Mentions = new DiscordMentions(Mentions.All, mentionReply); + } + + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}"; + string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Post, + Payload = DiscordJson.SerializeObject(pld) + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + DiscordMessage ret = PrepareMessage(JObject.Parse(res.Response!)); + + return ret; + } + + internal async ValueTask CreateMessageAsync + ( + ulong channelId, + DiscordMessageBuilder builder + ) + { + builder.Validate(); + + if (builder.Embeds != null) + { + foreach (DiscordEmbed embed in builder.Embeds) + { + if (embed?.Timestamp != null) + { + embed.Timestamp = embed.Timestamp.Value.ToUniversalTime(); + } + } + } + + RestChannelMessageCreatePayload pld = new() + { + HasContent = builder.Content != null, + Content = builder.Content, + StickersIds = builder.stickers?.Where(s => s != null).Select(s => s.Id).ToArray(), + IsTTS = builder.IsTTS, + HasEmbed = builder.Embeds != null, + Embeds = builder.Embeds, + Components = builder.Components, + Flags = builder.Flags, + Poll = builder.Poll?.BuildInternal(), + }; + + if (builder.ReplyId != null) + { + pld.MessageReference = new InternalDiscordMessageReference { MessageId = builder.ReplyId, FailIfNotExists = builder.FailOnInvalidReply }; + } + + pld.Mentions = new DiscordMentions(builder.Mentions ?? Mentions.None, builder.MentionOnReply); + + if (builder.Files.Count == 0) + { + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}"; + string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Post, + Payload = DiscordJson.SerializeObject(pld) + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + DiscordMessage ret = PrepareMessage(JObject.Parse(res.Response!)); + + return ret; + } + else + { + Dictionary values = new() + { + ["payload_json"] = DiscordJson.SerializeObject(pld) + }; + + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}"; + string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}"; + + MultipartRestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Post, + Values = values, + Files = builder.Files + }; + + RestResponse res; + try + { + res = await this.rest.ExecuteRequestAsync(request); + } + finally + { + builder.ResetFileStreamPositions(); + } + + return PrepareMessage(JObject.Parse(res.Response!)); + } + } + + internal async ValueTask> GetGuildChannelsAsync(ulong guildId) + { + string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.CHANNELS}"; + string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.CHANNELS}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Get + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + IEnumerable channelsRaw = JsonConvert.DeserializeObject>(res.Response!)! + .Select + ( + xc => + { + xc.Discord = this.discord!; + return xc; + } + ); + + foreach (DiscordChannel? ret in channelsRaw) + { + foreach (DiscordOverwrite xo in ret.permissionOverwrites) + { + xo.Discord = this.discord!; + xo.channelId = ret.Id; + } + } + + return new ReadOnlyCollection(new List(channelsRaw)); + } + + internal async ValueTask> GetChannelMessagesAsync + ( + ulong channelId, + int limit, + ulong? before = null, + ulong? after = null, + ulong? around = null + ) + { + QueryUriBuilder url = new($"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}"); + if (around is not null) + { + url.AddParameter("around", around?.ToString(CultureInfo.InvariantCulture)); + } + + if (before is not null) + { + url.AddParameter("before", before?.ToString(CultureInfo.InvariantCulture)); + } + + if (after is not null) + { + url.AddParameter("after", after?.ToString(CultureInfo.InvariantCulture)); + } + + if (limit > 0) + { + url.AddParameter("limit", limit.ToString(CultureInfo.InvariantCulture)); + } + + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}"; + + RestRequest request = new() + { + Route = route, + Url = url.Build(), + Method = HttpMethod.Get + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + JArray msgsRaw = JArray.Parse(res.Response!); + List msgs = []; + foreach (JToken xj in msgsRaw) + { + msgs.Add(PrepareMessage(xj)); + } + + return new ReadOnlyCollection(new List(msgs)); + } + + internal async ValueTask GetChannelMessageAsync + ( + ulong channelId, + ulong messageId + ) + { + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/:message_id"; + string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/{messageId}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Get + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + DiscordMessage ret = PrepareMessage(JObject.Parse(res.Response!)); + + return ret; + } + + internal async ValueTask EditMessageAsync + ( + ulong channelId, + ulong messageId, + Optional content = default, + Optional> embeds = default, + Optional> mentions = default, + IReadOnlyList? components = null, + IReadOnlyList? files = null, + DiscordMessageFlags? flags = null, + IEnumerable? attachments = null + ) + { + if (embeds.HasValue && embeds.Value != null) + { + foreach (DiscordEmbed embed in embeds.Value) + { + if (embed.Timestamp != null) + { + embed.Timestamp = embed.Timestamp.Value.ToUniversalTime(); + } + } + } + + RestChannelMessageEditPayload pld = new() + { + HasContent = content.HasValue, + Content = content.HasValue ? (string)content : null, + HasEmbed = embeds.HasValue && (embeds.Value?.Any() ?? false), + Embeds = embeds.HasValue && (embeds.Value?.Any() ?? false) ? embeds.Value : null, + Components = components, + Flags = flags, + Attachments = attachments, + Mentions = mentions.HasValue + ? new DiscordMentions + ( + mentions.Value ?? Mentions.None, + mentions.Value?.OfType().Any() ?? false + ) + : null + }; + + string payload = DiscordJson.SerializeObject(pld); + + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/:message_id"; + string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/{messageId}"; + + RestResponse res; + + if (files is not null) + { + MultipartRestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Patch, + Values = new Dictionary() + { + ["payload_json"] = payload + }, + Files = (IReadOnlyList)files + }; + + res = await this.rest.ExecuteRequestAsync(request); + } + else + { + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Patch, + Payload = payload + }; + + res = await this.rest.ExecuteRequestAsync(request); + } + + DiscordMessage ret = PrepareMessage(JObject.Parse(res.Response!)); + + if (files is not null) + { + foreach (DiscordMessageFile file in files.Where(x => x.ResetPositionTo.HasValue)) + { + file.Stream.Position = file.ResetPositionTo!.Value; + } + } + + return ret; + } + + internal async ValueTask DeleteMessageAsync + ( + ulong channelId, + ulong messageId, + string? reason + ) + { + Dictionary headers = []; + if (!string.IsNullOrWhiteSpace(reason)) + { + headers[REASON_HEADER_NAME] = reason; + } + + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/:message_id"; + string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/{messageId}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Delete, + Headers = headers + }; + + await this.rest.ExecuteRequestAsync(request); + } + + internal async ValueTask DeleteMessagesAsync + ( + ulong channelId, + IEnumerable messageIds, + string reason + ) + { + RestChannelMessageBulkDeletePayload pld = new() + { + Messages = messageIds + }; + + Dictionary headers = []; + if (!string.IsNullOrWhiteSpace(reason)) + { + headers[REASON_HEADER_NAME] = reason; + } + + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/{Endpoints.BULK_DELETE}"; + string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/{Endpoints.BULK_DELETE}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Post, + Payload = DiscordJson.SerializeObject(pld), + Headers = headers + }; + + await this.rest.ExecuteRequestAsync(request); + } + + internal async ValueTask> GetChannelInvitesAsync + ( + ulong channelId + ) + { + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.INVITES}"; + string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.INVITES}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Get + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + IEnumerable invitesRaw = JsonConvert.DeserializeObject>(res.Response!)! + .Select + ( + xi => + { + xi.Discord = this.discord!; + return xi; + } + ); + + return new ReadOnlyCollection(new List(invitesRaw)); + } + + internal async ValueTask CreateChannelInviteAsync + ( + ulong channelId, + int maxAge, + int maxUses, + bool temporary, + bool unique, + string reason, + DiscordInviteTargetType? targetType = null, + ulong? targetUserId = null, + ulong? targetApplicationId = null + ) + { + RestChannelInviteCreatePayload pld = new() + { + MaxAge = maxAge, + MaxUses = maxUses, + Temporary = temporary, + Unique = unique, + TargetType = targetType, + TargetUserId = targetUserId, + TargetApplicationId = targetApplicationId + }; + + Dictionary headers = []; + if (!string.IsNullOrWhiteSpace(reason)) + { + headers[REASON_HEADER_NAME] = reason; + } + + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.INVITES}"; + string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.INVITES}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Post, + Payload = DiscordJson.SerializeObject(pld), + Headers = headers + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + DiscordInvite ret = JsonConvert.DeserializeObject(res.Response!)!; + ret.Discord = this.discord!; + + return ret; + } + + internal async ValueTask DeleteChannelPermissionAsync + ( + ulong channelId, + ulong overwriteId, + string reason + ) + { + Dictionary headers = []; + if (!string.IsNullOrWhiteSpace(reason)) + { + headers[REASON_HEADER_NAME] = reason; + } + + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.PERMISSIONS}/:overwrite_id"; + string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.PERMISSIONS}/{overwriteId}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Delete, + Headers = headers + }; + + await this.rest.ExecuteRequestAsync(request); + } + + internal async ValueTask EditChannelPermissionsAsync + ( + ulong channelId, + ulong overwriteId, + DiscordPermissions allow, + DiscordPermissions deny, + string type, + string? reason = null + ) + { + RestChannelPermissionEditPayload pld = new() + { + Type = type switch + { + "role" => 0, + "member" => 1, + _ => throw new InvalidOperationException("Unrecognized permission overwrite target type.") + }, + Allow = allow & DiscordPermissions.All, + Deny = deny & DiscordPermissions.All + }; + + Dictionary headers = []; + if (!string.IsNullOrWhiteSpace(reason)) + { + headers[REASON_HEADER_NAME] = reason; + } + + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.PERMISSIONS}/:overwrite_id"; + string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.PERMISSIONS}/{overwriteId}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Put, + Payload = DiscordJson.SerializeObject(pld), + Headers = headers + }; + + await this.rest.ExecuteRequestAsync(request); + } + + internal async ValueTask TriggerTypingAsync + ( + ulong channelId + ) + { + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.TYPING}"; + string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.TYPING}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Post + }; + await this.rest.ExecuteRequestAsync(request); + } + + internal async ValueTask> GetPinnedMessagesAsync + ( + ulong channelId + ) + { + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.PINS}"; + string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.PINS}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Get + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + JArray msgsRaw = JArray.Parse(res.Response!); + List msgs = []; + foreach (JToken xj in msgsRaw) + { + msgs.Add(PrepareMessage(xj)); + } + + return new ReadOnlyCollection(new List(msgs)); + } + + internal async ValueTask PinMessageAsync + ( + ulong channelId, + ulong messageId + ) + { + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.PINS}/:message_id"; + string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.PINS}/{messageId}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Put + }; + + await this.rest.ExecuteRequestAsync(request); + } + + internal async ValueTask UnpinMessageAsync + ( + ulong channelId, + ulong messageId + ) + { + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.PINS}/:message_id"; + string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.PINS}/{messageId}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Delete + }; + await this.rest.ExecuteRequestAsync(request); + } + + internal async ValueTask AddGroupDmRecipientAsync + ( + ulong channelId, + ulong userId, + string accessToken, + string nickname + ) + { + RestChannelGroupDmRecipientAddPayload pld = new() + { + AccessToken = accessToken, + Nickname = nickname + }; + + string route = $"{Endpoints.USERS}/{Endpoints.ME}/{Endpoints.CHANNELS}/{channelId}/{Endpoints.RECIPIENTS}/:user_id"; + string url = $"{Endpoints.USERS}/{Endpoints.ME}/{Endpoints.CHANNELS}/{channelId}/{Endpoints.RECIPIENTS}/{userId}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Put, + Payload = DiscordJson.SerializeObject(pld) + }; + + await this.rest.ExecuteRequestAsync(request); + } + + internal async ValueTask RemoveGroupDmRecipientAsync + ( + ulong channelId, + ulong userId + ) + { + string route = $"{Endpoints.USERS}/{Endpoints.ME}/{Endpoints.CHANNELS}/{channelId}/{Endpoints.RECIPIENTS}/:user_id"; + string url = $"{Endpoints.USERS}/{Endpoints.ME}/{Endpoints.CHANNELS}/{channelId}/{Endpoints.RECIPIENTS}/{userId}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Delete + }; + await this.rest.ExecuteRequestAsync(request); + } + + internal async ValueTask CreateGroupDmAsync + ( + IEnumerable accessTokens, + IDictionary nicks + ) + { + RestUserGroupDmCreatePayload pld = new() + { + AccessTokens = accessTokens, + Nicknames = nicks + }; + + string route = $"{Endpoints.USERS}/{Endpoints.ME}/{Endpoints.CHANNELS}"; + string url = $"{Endpoints.USERS}/{Endpoints.ME}/{Endpoints.CHANNELS}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Post, + Payload = DiscordJson.SerializeObject(pld) + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + DiscordDmChannel ret = JsonConvert.DeserializeObject(res.Response!)!; + ret.Discord = this.discord!; + + return ret; + } + + internal async ValueTask CreateDmAsync + ( + ulong recipientId + ) + { + RestUserDmCreatePayload pld = new() + { + Recipient = recipientId + }; + + string route = $"{Endpoints.USERS}{Endpoints.ME}{Endpoints.CHANNELS}"; + string url = $"{Endpoints.USERS}/{Endpoints.ME}/{Endpoints.CHANNELS}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Post, + Payload = DiscordJson.SerializeObject(pld) + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + DiscordDmChannel ret = JsonConvert.DeserializeObject(res.Response!)!; + ret.Discord = this.discord!; + + if (this.discord is DiscordClient dc) + { + _ = dc.privateChannels.TryAdd(ret.Id, ret); + } + + return ret; + } + + internal async ValueTask FollowChannelAsync + ( + ulong channelId, + ulong webhookChannelId + ) + { + FollowedChannelAddPayload pld = new() + { + WebhookChannelId = webhookChannelId + }; + + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.FOLLOWERS}"; + string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.FOLLOWERS}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Post, + Payload = DiscordJson.SerializeObject(pld) + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + return JsonConvert.DeserializeObject(res.Response!)!; + } + + internal async ValueTask CrosspostMessageAsync + ( + ulong channelId, + ulong messageId + ) + { + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/:message_id/{Endpoints.CROSSPOST}"; + string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/{messageId}/{Endpoints.CROSSPOST}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Post + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + return JsonConvert.DeserializeObject(res.Response!)!; + } + + internal async ValueTask CreateStageInstanceAsync + ( + ulong channelId, + string topic, + DiscordStagePrivacyLevel? privacyLevel = null, + string? reason = null + ) + { + Dictionary headers = []; + + if (!string.IsNullOrWhiteSpace(reason)) + { + headers[REASON_HEADER_NAME] = reason; + } + + RestCreateStageInstancePayload pld = new() + { + ChannelId = channelId, + Topic = topic, + PrivacyLevel = privacyLevel + }; + + string route = $"{Endpoints.STAGE_INSTANCES}"; + string url = $"{Endpoints.STAGE_INSTANCES}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Post, + Payload = DiscordJson.SerializeObject(pld), + Headers = headers + }; + + RestResponse response = await this.rest.ExecuteRequestAsync(request); + + DiscordStageInstance stage = JsonConvert.DeserializeObject(response.Response!)!; + stage.Discord = this.discord!; + + return stage; + } + + internal async ValueTask GetStageInstanceAsync + ( + ulong channelId + ) + { + string route = $"{Endpoints.STAGE_INSTANCES}/{channelId}"; + string url = $"{Endpoints.STAGE_INSTANCES}/{channelId}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Get + }; + + RestResponse response = await this.rest.ExecuteRequestAsync(request); + + DiscordStageInstance stage = JsonConvert.DeserializeObject(response.Response!)!; + stage.Discord = this.discord!; + + return stage; + } + + internal async ValueTask ModifyStageInstanceAsync + ( + ulong channelId, + Optional topic = default, + Optional privacyLevel = default, + string? reason = null + ) + { + Dictionary headers = []; + + if (!string.IsNullOrWhiteSpace(reason)) + { + headers[REASON_HEADER_NAME] = reason; + } + + RestModifyStageInstancePayload pld = new() + { + Topic = topic, + PrivacyLevel = privacyLevel + }; + + string route = $"{Endpoints.STAGE_INSTANCES}/{channelId}"; + string url = $"{Endpoints.STAGE_INSTANCES}/{channelId}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Patch, + Payload = DiscordJson.SerializeObject(pld), + Headers = headers + }; + + RestResponse response = await this.rest.ExecuteRequestAsync(request); + DiscordStageInstance stage = JsonConvert.DeserializeObject(response.Response!)!; + stage.Discord = this.discord!; + + return stage; + } + + internal async ValueTask BecomeStageInstanceSpeakerAsync + ( + ulong guildId, + ulong id, + ulong? userId = null, + DateTime? timestamp = null, + bool? suppress = null + ) + { + Dictionary headers = []; + + RestBecomeStageSpeakerInstancePayload pld = new() + { + Suppress = suppress, + ChannelId = id, + RequestToSpeakTimestamp = timestamp + }; + + string user = userId?.ToString() ?? "@me"; + string route = $"/guilds/{guildId}/{Endpoints.VOICE_STATES}/{(userId is null ? "@me" : ":user_id")}"; + string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.VOICE_STATES}/{user}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Patch, + Payload = DiscordJson.SerializeObject(pld), + Headers = headers + }; + + await this.rest.ExecuteRequestAsync(request); + } + + internal async ValueTask DeleteStageInstanceAsync + ( + ulong channelId, + string? reason = null + ) + { + Dictionary headers = []; + if (!string.IsNullOrWhiteSpace(reason)) + { + headers[REASON_HEADER_NAME] = reason; + } + + string route = $"{Endpoints.STAGE_INSTANCES}/{channelId}"; + string url = $"{Endpoints.STAGE_INSTANCES}/{channelId}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Delete, + Headers = headers + }; + + await this.rest.ExecuteRequestAsync(request); + } + + #endregion + + #region Threads + + internal async ValueTask CreateThreadFromMessageAsync + ( + ulong channelId, + ulong messageId, + string name, + DiscordAutoArchiveDuration archiveAfter, + string? reason = null + ) + { + Dictionary headers = []; + if (!string.IsNullOrWhiteSpace(reason)) + { + headers[REASON_HEADER_NAME] = reason; + } + + RestThreadCreatePayload payload = new() + { + Name = name, + ArchiveAfter = archiveAfter + }; + + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/:message_id/{Endpoints.THREADS}"; + string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/{messageId}/{Endpoints.THREADS}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Post, + Payload = DiscordJson.SerializeObject(payload), + Headers = headers + }; + + RestResponse response = await this.rest.ExecuteRequestAsync(request); + + DiscordThreadChannel thread = JsonConvert.DeserializeObject(response.Response!)!; + thread.Discord = this.discord!; + + return thread; + } + + internal async ValueTask CreateThreadAsync + ( + ulong channelId, + string name, + DiscordAutoArchiveDuration archiveAfter, + DiscordChannelType type, + string? reason = null + ) + { + Dictionary headers = []; + if (!string.IsNullOrWhiteSpace(reason)) + { + headers[REASON_HEADER_NAME] = reason; + } + + RestThreadCreatePayload payload = new() + { + Name = name, + ArchiveAfter = archiveAfter, + Type = type + }; + + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.THREADS}"; + string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.THREADS}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Post, + Payload = DiscordJson.SerializeObject(payload), + Headers = headers + }; + + RestResponse response = await this.rest.ExecuteRequestAsync(request); + + DiscordThreadChannel thread = JsonConvert.DeserializeObject(response.Response!)!; + thread.Discord = this.discord!; + + return thread; + } + + internal async ValueTask JoinThreadAsync + ( + ulong channelId + ) + { + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.THREAD_MEMBERS}/{Endpoints.ME}"; + string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.THREAD_MEMBERS}/{Endpoints.ME}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Put + }; + + await this.rest.ExecuteRequestAsync(request); + } + + internal async ValueTask LeaveThreadAsync + ( + ulong channelId + ) + { + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.THREAD_MEMBERS}/{Endpoints.ME}"; + string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.THREAD_MEMBERS}/{Endpoints.ME}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Delete + }; + + await this.rest.ExecuteRequestAsync(request); + } + + internal async ValueTask GetThreadMemberAsync + ( + ulong channelId, + ulong userId + ) + { + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.THREAD_MEMBERS}/:user_id"; + string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.THREAD_MEMBERS}/{userId}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Get + }; + + RestResponse response = await this.rest.ExecuteRequestAsync(request); + + DiscordThreadChannelMember ret = JsonConvert.DeserializeObject(response.Response!)!; + + ret.Discord = this.discord!; + + return ret; + } + + internal async ValueTask AddThreadMemberAsync + ( + ulong channelId, + ulong userId + ) + { + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.THREAD_MEMBERS}/:user_id"; + string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.THREAD_MEMBERS}/{userId}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Put + }; + + await this.rest.ExecuteRequestAsync(request); + } + + internal async ValueTask RemoveThreadMemberAsync + ( + ulong channelId, + ulong userId + ) + { + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.THREAD_MEMBERS}/:user_id"; + string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.THREAD_MEMBERS}/{userId}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Delete + }; + + await this.rest.ExecuteRequestAsync(request); + } + + internal async ValueTask> ListThreadMembersAsync + ( + ulong channelId + ) + { + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.THREAD_MEMBERS}"; + string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.THREAD_MEMBERS}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Get + }; + + RestResponse response = await this.rest.ExecuteRequestAsync(request); + + List threadMembers = JsonConvert.DeserializeObject>(response.Response!)!; + + foreach (DiscordThreadChannelMember member in threadMembers) + { + member.Discord = this.discord!; + } + + return new ReadOnlyCollection(threadMembers); + } + + internal async ValueTask ListActiveThreadsAsync + ( + ulong guildId + ) + { + string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.THREADS}/{Endpoints.ACTIVE}"; + string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.THREADS}/{Endpoints.ACTIVE}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Get + }; + + RestResponse response = await this.rest.ExecuteRequestAsync(request); + + ThreadQueryResult result = JsonConvert.DeserializeObject(response.Response!)!; + result.HasMore = false; + + foreach (DiscordThreadChannel thread in result.Threads) + { + thread.Discord = this.discord!; + } + + foreach (DiscordThreadChannelMember member in result.Members) + { + member.Discord = this.discord!; + member.guild_id = guildId; + DiscordThreadChannel? thread = result.Threads.SingleOrDefault(x => x.Id == member.ThreadId); + if (thread is not null) + { + thread.CurrentMember = member; + } + } + + return result; + } + + internal async ValueTask ListPublicArchivedThreadsAsync + ( + ulong guildId, + ulong channelId, + string before, + int limit + ) + { + QueryUriBuilder queryParams = new($"{Endpoints.CHANNELS}/{channelId}/{Endpoints.THREADS}/{Endpoints.ARCHIVED}/{Endpoints.PUBLIC}"); + if (before != null) + { + queryParams.AddParameter("before", before?.ToString(CultureInfo.InvariantCulture)); + } + + if (limit > 0) + { + queryParams.AddParameter("limit", limit.ToString(CultureInfo.InvariantCulture)); + } + + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.THREADS}/{Endpoints.ARCHIVED}/{Endpoints.PUBLIC}"; + + RestRequest request = new() + { + Route = route, + Url = queryParams.Build(), + Method = HttpMethod.Get, + + }; + + RestResponse response = await this.rest.ExecuteRequestAsync(request); + + ThreadQueryResult result = JsonConvert.DeserializeObject(response.Response!)!; + + foreach (DiscordThreadChannel thread in result.Threads) + { + thread.Discord = this.discord!; + } + + foreach (DiscordThreadChannelMember member in result.Members) + { + member.Discord = this.discord!; + member.guild_id = guildId; + DiscordThreadChannel? thread = result.Threads.SingleOrDefault(x => x.Id == member.ThreadId); + if (thread is not null) + { + thread.CurrentMember = member; + } + } + + return result; + } + + internal async ValueTask ListPrivateArchivedThreadsAsync + ( + ulong guildId, + ulong channelId, + int limit, + string? before = null + ) + { + QueryUriBuilder queryParams = new($"{Endpoints.CHANNELS}/{channelId}/{Endpoints.THREADS}/{Endpoints.ARCHIVED}/{Endpoints.PRIVATE}"); + if (before is not null) + { + queryParams.AddParameter("before", before?.ToString(CultureInfo.InvariantCulture)); + } + + if (limit > 0) + { + queryParams.AddParameter("limit", limit.ToString(CultureInfo.InvariantCulture)); + } + + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.THREADS}/{Endpoints.ARCHIVED}/{Endpoints.PRIVATE}"; + + RestRequest request = new() + { + Route = route, + Url = queryParams.Build(), + Method = HttpMethod.Get + }; + + RestResponse response = await this.rest.ExecuteRequestAsync(request); + + ThreadQueryResult result = JsonConvert.DeserializeObject(response.Response!)!; + + foreach (DiscordThreadChannel thread in result.Threads) + { + thread.Discord = this.discord!; + } + + foreach (DiscordThreadChannelMember member in result.Members) + { + member.Discord = this.discord!; + member.guild_id = guildId; + DiscordThreadChannel? thread = result.Threads.SingleOrDefault(x => x.Id == member.ThreadId); + if (thread is not null) + { + thread.CurrentMember = member; + } + } + + return result; + } + + internal async ValueTask ListJoinedPrivateArchivedThreadsAsync + ( + ulong guildId, + ulong channelId, + int limit, + ulong? before = null + ) + { + QueryUriBuilder queryParams = new($"{Endpoints.CHANNELS}/{channelId}/{Endpoints.THREADS}/{Endpoints.ARCHIVED}/{Endpoints.PRIVATE}/{Endpoints.ME}"); + if (before is not null) + { + queryParams.AddParameter("before", before?.ToString(CultureInfo.InvariantCulture)); + } + + if (limit > 0) + { + queryParams.AddParameter("limit", limit.ToString(CultureInfo.InvariantCulture)); + } + + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.USERS}/{Endpoints.ME}/{Endpoints.THREADS}/{Endpoints.ARCHIVED}/{Endpoints.PUBLIC}"; + + RestRequest request = new() + { + Route = route, + Url = queryParams.Build(), + Method = HttpMethod.Get + }; + + RestResponse response = await this.rest.ExecuteRequestAsync(request); + + ThreadQueryResult result = JsonConvert.DeserializeObject(response.Response!)!; + + foreach (DiscordThreadChannel thread in result.Threads) + { + thread.Discord = this.discord!; + } + + foreach (DiscordThreadChannelMember member in result.Members) + { + member.Discord = this.discord!; + member.guild_id = guildId; + DiscordThreadChannel? thread = result.Threads.SingleOrDefault(x => x.Id == member.ThreadId); + if (thread is not null) + { + thread.CurrentMember = member; + } + } + + return result; + } + + #endregion + + #region Member + internal ValueTask GetCurrentUserAsync() + => GetUserAsync("@me"); + + internal ValueTask GetUserAsync(ulong userId) + => GetUserAsync(userId.ToString(CultureInfo.InvariantCulture)); + + internal async ValueTask GetUserAsync(string userId) + { + string route = $"{Endpoints.USERS}/:user_id"; + string url = $"{Endpoints.USERS}/{userId}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Get + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + TransportUser userRaw = JsonConvert.DeserializeObject(res.Response!)!; + DiscordUser user = new(userRaw) + { + Discord = this.discord! + }; + + return user; + } + + internal async ValueTask GetGuildMemberAsync + ( + ulong guildId, + ulong userId + ) + { + string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.MEMBERS}/:user_id"; + string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.MEMBERS}/{userId}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Get + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + TransportMember tm = JsonConvert.DeserializeObject(res.Response!)!; + + DiscordUser usr = new(tm.User) + { + Discord = this.discord! + }; + _ = this.discord!.UpdateUserCache(usr); + + return new DiscordMember(tm) + { + Discord = this.discord, + guild_id = guildId + }; + } + + internal async ValueTask RemoveGuildMemberAsync + ( + ulong guildId, + ulong userId, + string? reason = null + ) + { + string url = new($"{Endpoints.GUILDS}/{guildId}/{Endpoints.MEMBERS}/{userId}"); + string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.MEMBERS}/:user_id"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Delete, + Headers = string.IsNullOrWhiteSpace(reason) + ? null + : new Dictionary + { + [REASON_HEADER_NAME] = reason + } + }; + + await this.rest.ExecuteRequestAsync(request); + } + + internal async ValueTask ModifyCurrentUserAsync + ( + string username, + Optional base64Avatar = default, + Optional base64Banner = default + ) + { + RestUserUpdateCurrentPayload pld = new() + { + Username = username, + AvatarBase64 = base64Avatar.HasValue ? base64Avatar.Value : null, + AvatarSet = base64Avatar.HasValue, + BannerBase64 = base64Banner.HasValue ? base64Banner.Value : null, + BannerSet = base64Banner.HasValue + }; + + string route = $"{Endpoints.USERS}/{Endpoints.ME}"; + string url = $"{Endpoints.USERS}/{Endpoints.ME}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Patch, + Payload = DiscordJson.SerializeObject(pld) + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + TransportUser userRaw = JsonConvert.DeserializeObject(res.Response!)!; + + return userRaw; + } + + internal async ValueTask> GetCurrentUserGuildsAsync + ( + int limit = 100, + ulong? before = null, + ulong? after = null + ) + { + string route = $"{Endpoints.USERS}/{Endpoints.ME}/{Endpoints.GUILDS}"; + QueryUriBuilder url = new($"{Endpoints.USERS}/{Endpoints.ME}/{Endpoints.GUILDS}"); + url.AddParameter($"limit", limit.ToString(CultureInfo.InvariantCulture)); + + if (before != null) + { + url.AddParameter("before", before.Value.ToString(CultureInfo.InvariantCulture)); + } + + if (after != null) + { + url.AddParameter("after", after.Value.ToString(CultureInfo.InvariantCulture)); + } + + RestRequest request = new() + { + Route = route, + Url = url.Build(), + Method = HttpMethod.Get + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + if (this.discord is DiscordClient) + { + IEnumerable guildsRaw = JsonConvert.DeserializeObject>(res.Response!)!; + IEnumerable guilds = guildsRaw.Select + ( + xug => (this.discord as DiscordClient)?.guilds[xug.Id] + ) + .Where(static guild => guild is not null)!; + return new ReadOnlyCollection(new List(guilds)); + } + else + { + List guildsRaw = [.. JsonConvert.DeserializeObject>(res.Response!)!]; + foreach (DiscordGuild guild in guildsRaw) + { + guild.Discord = this.discord!; + + } + return new ReadOnlyCollection(guildsRaw); + } + } + + internal async ValueTask GetCurrentUserGuildMemberAsync + ( + ulong guildId + ) + { + string route = $"{Endpoints.USERS}/{Endpoints.ME}/{Endpoints.GUILDS}/{guildId}/member"; + + RestRequest request = new() + { + Route = route, + Url = route, + Method = HttpMethod.Get + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + TransportMember tm = JsonConvert.DeserializeObject(res.Response!)!; + + DiscordUser usr = new(tm.User) + { + Discord = this.discord! + }; + _ = this.discord!.UpdateUserCache(usr); + + return new DiscordMember(tm) + { + Discord = this.discord, + guild_id = guildId + }; + } + + internal async ValueTask ModifyGuildMemberAsync + ( + ulong guildId, + ulong userId, + Optional nick = default, + Optional> roleIds = default, + Optional mute = default, + Optional deaf = default, + Optional voiceChannelId = default, + Optional communicationDisabledUntil = default, + Optional memberFlags = default, + string? reason = null + ) + { + Dictionary headers = []; + if (!string.IsNullOrWhiteSpace(reason)) + { + headers[REASON_HEADER_NAME] = reason; + } + + RestGuildMemberModifyPayload pld = new() + { + Nickname = nick, + RoleIds = roleIds, + Deafen = deaf, + Mute = mute, + VoiceChannelId = voiceChannelId, + CommunicationDisabledUntil = communicationDisabledUntil + }; + + string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.MEMBERS}/:user_id"; + string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.MEMBERS}/{userId}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Patch, + Payload = DiscordJson.SerializeObject(pld), + Headers = headers + }; + + await this.rest.ExecuteRequestAsync(request); + } + + internal async ValueTask ModifyCurrentMemberAsync + ( + ulong guildId, + string nick, + string? reason = null + ) + { + Dictionary headers = []; + if (!string.IsNullOrWhiteSpace(reason)) + { + headers[REASON_HEADER_NAME] = reason; + } + + RestGuildMemberModifyPayload pld = new() + { + Nickname = nick + }; + + string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.MEMBERS}/{Endpoints.ME}"; + string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.MEMBERS}/{Endpoints.ME}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Patch, + Payload = DiscordJson.SerializeObject(pld), + Headers = headers + }; + + await this.rest.ExecuteRequestAsync(request); + } + #endregion + + #region Roles + internal async ValueTask GetGuildRoleAsync + ( + ulong guildId, + ulong roleId + ) + { + string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.ROLES}/:role_id"; + string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.ROLES}/{roleId}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Get + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + DiscordRole role = JsonConvert.DeserializeObject(res.Response!)!; + role.Discord = this.discord!; + role.guild_id = guildId; + + return role; + } + + internal async ValueTask> GetGuildRolesAsync + ( + ulong guildId + ) + { + string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.ROLES}"; + string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.ROLES}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Get + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + IEnumerable rolesRaw = JsonConvert.DeserializeObject>(res.Response!)! + .Select + ( + xr => + { + xr.Discord = this.discord!; + xr.guild_id = guildId; + return xr; + } + ); + + return new ReadOnlyCollection(new List(rolesRaw)); + } + + internal async ValueTask GetGuildAsync + ( + ulong guildId, + bool? withCounts + ) + { + QueryUriBuilder urlparams = new($"{Endpoints.GUILDS}/{guildId}"); + if (withCounts.HasValue) + { + urlparams.AddParameter("with_counts", withCounts?.ToString()); + } + + string route = $"{Endpoints.GUILDS}/{guildId}"; + + RestRequest request = new() + { + Route = route, + Url = urlparams.Build(), + Method = HttpMethod.Get + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + JObject json = JObject.Parse(res.Response!); + JArray rawMembers = (JArray)json["members"]!; + DiscordGuild guildRest = json.ToDiscordObject(); + foreach (DiscordRole role in guildRest.roles.Values) + { + role.guild_id = guildRest.Id; + } + + if (this.discord is DiscordClient discordClient) + { + await discordClient.OnGuildUpdateEventAsync(guildRest, rawMembers); + return discordClient.guilds[guildRest.Id]; + } + else + { + guildRest.Discord = this.discord!; + return guildRest; + } + } + + internal async ValueTask ModifyGuildRoleAsync + ( + ulong guildId, + ulong roleId, + string? name = null, + DiscordPermissions? permissions = null, + int? color = null, + bool? hoist = null, + bool? mentionable = null, + Stream? icon = null, + string? emoji = null, + string? reason = null + ) + { + string? image = null; + + if (icon != null) + { + using InlineMediaTool it = new(icon); + image = it.GetBase64(); + } + + RestGuildRolePayload pld = new() + { + Name = name, + Permissions = permissions & DiscordPermissions.All, + Color = color, + Hoist = hoist, + Mentionable = mentionable, + Emoji = emoji, + Icon = image + }; + + Dictionary headers = []; + if (!string.IsNullOrWhiteSpace(reason)) + { + headers[REASON_HEADER_NAME] = reason; + } + + string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.ROLES}/:role_id"; + string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.ROLES}/{roleId}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Patch, + Payload = DiscordJson.SerializeObject(pld), + Headers = headers + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + DiscordRole ret = JsonConvert.DeserializeObject(res.Response!)!; + ret.Discord = this.discord!; + ret.guild_id = guildId; + + return ret; + } + + internal async ValueTask DeleteRoleAsync + ( + ulong guildId, + ulong roleId, + string? reason = null + ) + { + Dictionary headers = []; + if (!string.IsNullOrWhiteSpace(reason)) + { + headers[REASON_HEADER_NAME] = reason; + } + + string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.ROLES}/:role_id"; + string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.ROLES}/{roleId}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Delete, + Headers = headers + }; + + await this.rest.ExecuteRequestAsync(request); + } + + internal async ValueTask CreateGuildRoleAsync + ( + ulong guildId, + string name, + DiscordPermissions? permissions = null, + int? color = null, + bool? hoist = null, + bool? mentionable = null, + Stream? icon = null, + string? emoji = null, + string? reason = null + ) + { + string? image = null; + + if (icon != null) + { + using InlineMediaTool it = new(icon); + image = it.GetBase64(); + } + + RestGuildRolePayload pld = new() + { + Name = name, + Permissions = permissions & DiscordPermissions.All, + Color = color, + Hoist = hoist, + Mentionable = mentionable, + Emoji = emoji, + Icon = image + }; + + Dictionary headers = []; + if (!string.IsNullOrWhiteSpace(reason)) + { + headers[REASON_HEADER_NAME] = reason; + } + + string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.ROLES}"; + string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.ROLES}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Post, + Payload = DiscordJson.SerializeObject(pld), + Headers = headers + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + DiscordRole ret = JsonConvert.DeserializeObject(res.Response!)!; + ret.Discord = this.discord!; + ret.guild_id = guildId; + + return ret; + } + #endregion + + #region Prune + internal async ValueTask GetGuildPruneCountAsync + ( + ulong guildId, + int days, + IEnumerable? includeRoles = null + ) + { + if (days is < 0 or > 30) + { + throw new ArgumentException("Prune inactivity days must be a number between 0 and 30.", nameof(days)); + } + + QueryUriBuilder urlparams = new($"{Endpoints.GUILDS}/{guildId}/{Endpoints.PRUNE}"); + urlparams.AddParameter("days", days.ToString(CultureInfo.InvariantCulture)); + + StringBuilder sb = new(); + + if (includeRoles is not null) + { + ulong[] roleArray = includeRoles.ToArray(); + int roleArrayCount = roleArray.Length; + + for (int i = 0; i < roleArrayCount; i++) + { + sb.Append($"&include_roles={roleArray[i]}"); + } + } + + string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.PRUNE}"; + + RestRequest request = new() + { + Route = route, + Url = urlparams.Build(), + Method = HttpMethod.Get + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + RestGuildPruneResultPayload pruned = JsonConvert.DeserializeObject(res.Response!)!; + + return pruned.Pruned!.Value; + } + + internal async ValueTask BeginGuildPruneAsync + ( + ulong guildId, + int days, + bool computePruneCount, + IEnumerable? includeRoles = null, + string? reason = null + ) + { + if (days is < 0 or > 30) + { + throw new ArgumentException("Prune inactivity days must be a number between 0 and 30.", nameof(days)); + } + + QueryUriBuilder urlparams = new($"{Endpoints.GUILDS}/{guildId}/{Endpoints.PRUNE}"); + urlparams.AddParameter("days", days.ToString(CultureInfo.InvariantCulture)); + urlparams.AddParameter("compute_prune_count", computePruneCount.ToString()); + + StringBuilder sb = new(); + + if (includeRoles is not null) + { + foreach (ulong id in includeRoles) + { + sb.Append($"&include_roles={id}"); + } + } + + Dictionary headers = []; + if (string.IsNullOrWhiteSpace(reason)) + { + headers[REASON_HEADER_NAME] = reason!; + } + + string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.PRUNE}"; + + RestRequest request = new() + { + Route = route, + Url = urlparams.Build() + sb.ToString(), + Method = HttpMethod.Post, + Headers = headers + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + RestGuildPruneResultPayload pruned = JsonConvert.DeserializeObject(res.Response!)!; + + return pruned.Pruned; + } + #endregion + + #region GuildVarious + internal async ValueTask GetTemplateAsync + ( + string code + ) + { + string route = $"{Endpoints.GUILDS}/{Endpoints.TEMPLATES}/:code"; + string url = $"{Endpoints.GUILDS}/{Endpoints.TEMPLATES}/{code}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Get + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + DiscordGuildTemplate templatesRaw = JsonConvert.DeserializeObject(res.Response!)!; + + return templatesRaw; + } + + internal async ValueTask> GetGuildIntegrationsAsync + ( + ulong guildId + ) + { + string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.INTEGRATIONS}"; + string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.INTEGRATIONS}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Get + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + IEnumerable integrationsRaw = + JsonConvert.DeserializeObject>(res.Response!)! + .Select + ( + xi => + { + xi.Discord = this.discord!; + return xi; + } + ); + + return new ReadOnlyCollection(new List(integrationsRaw)); + } + + internal async ValueTask GetGuildPreviewAsync + ( + ulong guildId + ) + { + string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.PREVIEW}"; + string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.PREVIEW}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Get + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + DiscordGuildPreview ret = JsonConvert.DeserializeObject(res.Response!)!; + ret.Discord = this.discord!; + + return ret; + } + + internal async ValueTask CreateGuildIntegrationAsync + ( + ulong guildId, + string type, + ulong id + ) + { + RestGuildIntegrationAttachPayload pld = new() + { + Type = type, + Id = id + }; + + string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.INTEGRATIONS}"; + string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.INTEGRATIONS}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Post, + Payload = DiscordJson.SerializeObject(pld) + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + DiscordIntegration ret = JsonConvert.DeserializeObject(res.Response!)!; + ret.Discord = this.discord!; + + return ret; + } + + internal async ValueTask ModifyGuildIntegrationAsync + ( + ulong guildId, + ulong integrationId, + int expireBehaviour, + int expireGracePeriod, + bool enableEmoticons + ) + { + RestGuildIntegrationModifyPayload pld = new() + { + ExpireBehavior = expireBehaviour, + ExpireGracePeriod = expireGracePeriod, + EnableEmoticons = enableEmoticons + }; + + string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.INTEGRATIONS}/:integration_id"; + string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.INTEGRATIONS}/{integrationId}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Patch, + Payload = DiscordJson.SerializeObject(pld) + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + DiscordIntegration ret = JsonConvert.DeserializeObject(res.Response!)!; + ret.Discord = this.discord!; + + return ret; + } + + internal async ValueTask DeleteGuildIntegrationAsync + ( + ulong guildId, + ulong integrationId, + string? reason = null + ) + { + Dictionary headers = []; + if (string.IsNullOrWhiteSpace(reason)) + { + headers[REASON_HEADER_NAME] = reason!; + } + + string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.INTEGRATIONS}/:integration_id"; + string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.INTEGRATIONS}/{integrationId}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Delete, + Headers = headers + }; + + await this.rest.ExecuteRequestAsync(request); + } + + internal async ValueTask SyncGuildIntegrationAsync + ( + ulong guildId, + ulong integrationId + ) + { + string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.INTEGRATIONS}/:integration_id/{Endpoints.SYNC}"; + string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.INTEGRATIONS}/{integrationId}/{Endpoints.SYNC}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Post + }; + + await this.rest.ExecuteRequestAsync(request); + } + + internal async ValueTask> GetGuildVoiceRegionsAsync + ( + ulong guildId + ) + { + string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.REGIONS}"; + string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.REGIONS}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Get + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + IEnumerable regionsRaw = JsonConvert.DeserializeObject>(res.Response!)!; + + return new ReadOnlyCollection(new List(regionsRaw)); + } + + internal async ValueTask> GetGuildInvitesAsync + ( + ulong guildId + ) + { + string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.INVITES}"; + string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.INVITES}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Get + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + IEnumerable invitesRaw = + JsonConvert.DeserializeObject>(res.Response!)! + .Select + ( + xi => + { + xi.Discord = this.discord!; + return xi; + } + ); + + return new ReadOnlyCollection(new List(invitesRaw)); + } + #endregion + + #region Invite + internal async ValueTask GetInviteAsync + ( + string inviteCode, + bool? withCounts = null, + bool? withExpiration = null + ) + { + Dictionary urlparams = []; + if (withCounts.HasValue) + { + urlparams["with_counts"] = withCounts?.ToString()!; + urlparams["with_expiration"] = withExpiration?.ToString()!; + } + + string route = $"{Endpoints.INVITES}/:invite_code"; + string url = $"{Endpoints.INVITES}/{inviteCode}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Get + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + DiscordInvite ret = JsonConvert.DeserializeObject(res.Response!)!; + ret.Discord = this.discord!; + + return ret; + } + + internal async ValueTask DeleteInviteAsync + ( + string inviteCode, + string? reason = null + ) + { + Dictionary headers = []; + if (!string.IsNullOrWhiteSpace(reason)) + { + headers[REASON_HEADER_NAME] = reason; + } + + string route = $"{Endpoints.INVITES}/:invite_code"; + string url = $"{Endpoints.INVITES}/{inviteCode}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Delete, + Headers = headers + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + DiscordInvite ret = JsonConvert.DeserializeObject(res.Response!)!; + ret.Discord = this.discord!; + + return ret; + } + #endregion + + #region Connections + internal async ValueTask> GetUsersConnectionsAsync() + { + string route = $"{Endpoints.USERS}/{Endpoints.ME}/{Endpoints.CONNECTIONS}"; + string url = $"{Endpoints.USERS}/{Endpoints.ME}/{Endpoints.CONNECTIONS}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Get + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + IEnumerable connectionsRaw = + JsonConvert.DeserializeObject>(res.Response!)! + .Select + ( + xc => + { + xc.Discord = this.discord!; + return xc; + } + ); + + return new ReadOnlyCollection(new List(connectionsRaw)); + } + #endregion + + #region Voice + internal async ValueTask> ListVoiceRegionsAsync() + { + string route = $"{Endpoints.VOICE}/{Endpoints.REGIONS}"; + string url = $"{Endpoints.VOICE}/{Endpoints.REGIONS}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Get + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + IEnumerable regions = + JsonConvert.DeserializeObject>(res.Response!)!; + + return new ReadOnlyCollection(new List(regions)); + } + #endregion + + #region Webhooks + internal async ValueTask CreateWebhookAsync + ( + ulong channelId, + string name, + Optional base64Avatar = default, + string? reason = null + ) + { + RestWebhookPayload pld = new() + { + Name = name, + AvatarBase64 = base64Avatar.HasValue ? base64Avatar.Value : null, + AvatarSet = base64Avatar.HasValue + }; + + Dictionary headers = []; + if (!string.IsNullOrWhiteSpace(reason)) + { + headers[REASON_HEADER_NAME] = reason; + } + + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.WEBHOOKS}"; + string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.WEBHOOKS}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Post, + Payload = DiscordJson.SerializeObject(pld), + Headers = headers + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + DiscordWebhook ret = JsonConvert.DeserializeObject(res.Response!)!; + ret.Discord = this.discord!; + ret.ApiClient = this; + + return ret; + } + + internal async ValueTask> GetChannelWebhooksAsync + ( + ulong channelId + ) + { + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.WEBHOOKS}"; + string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.WEBHOOKS}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Get + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + IEnumerable webhooksRaw = + JsonConvert + .DeserializeObject>(res.Response!)! + .Select + ( + xw => + { + xw.Discord = this.discord!; + xw.ApiClient = this; + return xw; + } + ); + + return new ReadOnlyCollection(new List(webhooksRaw)); + } + + internal async ValueTask> GetGuildWebhooksAsync + ( + ulong guildId + ) + { + string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.WEBHOOKS}"; + string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.WEBHOOKS}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Get + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + IEnumerable webhooksRaw = + JsonConvert + .DeserializeObject>(res.Response!)! + .Select + ( + xw => + { + xw.Discord = this.discord!; + xw.ApiClient = this; + return xw; + } + ); + + return new ReadOnlyCollection(new List(webhooksRaw)); + } + + internal async ValueTask GetWebhookAsync + ( + ulong webhookId + ) + { + string route = $"{Endpoints.WEBHOOKS}/{webhookId}"; + string url = $"{Endpoints.WEBHOOKS}/{webhookId}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Get + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + DiscordWebhook ret = JsonConvert.DeserializeObject(res.Response!)!; + ret.Discord = this.discord!; + ret.ApiClient = this; + + return ret; + } + + // Auth header not required + internal async ValueTask GetWebhookWithTokenAsync + ( + ulong webhookId, + string webhookToken + ) + { + string route = $"{Endpoints.WEBHOOKS}/{webhookId}/:webhook_token"; + string url = $"{Endpoints.WEBHOOKS}/{webhookId}/{webhookToken}"; + + RestRequest request = new() + { + Route = route, + Url = url, + IsExemptFromGlobalLimit = true, + Method = HttpMethod.Get + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + DiscordWebhook ret = JsonConvert.DeserializeObject(res.Response!)!; + ret.Token = webhookToken; + ret.Id = webhookId; + ret.Discord = this.discord!; + ret.ApiClient = this; + + return ret; + } + + internal async ValueTask GetWebhookMessageAsync + ( + ulong webhookId, + string webhookToken, + ulong messageId + ) + { + string route = $"{Endpoints.WEBHOOKS}/{webhookId}/:webhook_token/{Endpoints.MESSAGES}/:message_id"; + string url = $"{Endpoints.WEBHOOKS}/{webhookId}/{webhookToken}/{Endpoints.MESSAGES}/{messageId}"; + + RestRequest request = new() + { + Route = route, + Url = url, + IsExemptFromGlobalLimit = true, + Method = HttpMethod.Get + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + DiscordMessage ret = JsonConvert.DeserializeObject(res.Response!)!; + ret.Discord = this.discord!; + return ret; + } + + internal async ValueTask ModifyWebhookAsync + ( + ulong webhookId, + ulong channelId, + string? name = null, + Optional base64Avatar = default, + string? reason = null + ) + { + RestWebhookPayload pld = new() + { + Name = name, + AvatarBase64 = base64Avatar.HasValue ? base64Avatar.Value : null, + AvatarSet = base64Avatar.HasValue, + ChannelId = channelId + }; + + Dictionary headers = []; + if (!string.IsNullOrWhiteSpace(reason)) + { + headers[REASON_HEADER_NAME] = reason; + } + + string route = $"{Endpoints.WEBHOOKS}/{webhookId}"; + string url = $"{Endpoints.WEBHOOKS}/{webhookId}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Patch, + Payload = DiscordJson.SerializeObject(pld), + Headers = headers + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + DiscordWebhook ret = JsonConvert.DeserializeObject(res.Response!)!; + ret.Discord = this.discord!; + ret.ApiClient = this; + + return ret; + } + + internal async ValueTask ModifyWebhookAsync + ( + ulong webhookId, + string webhookToken, + string? name = null, + string? base64Avatar = null, + string? reason = null + ) + { + RestWebhookPayload pld = new() + { + Name = name, + AvatarBase64 = base64Avatar + }; + + Dictionary headers = []; + if (!string.IsNullOrWhiteSpace(reason)) + { + headers[REASON_HEADER_NAME] = reason; + } + + string route = $"{Endpoints.WEBHOOKS}/{webhookId}/:webhook_token"; + string url = $"{Endpoints.WEBHOOKS}/{webhookId}/{webhookToken}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Patch, + Payload = DiscordJson.SerializeObject(pld), + IsExemptFromGlobalLimit = true, + Headers = headers + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + DiscordWebhook ret = JsonConvert.DeserializeObject(res.Response!)!; + ret.Discord = this.discord!; + ret.ApiClient = this; + + return ret; + } + + internal async ValueTask DeleteWebhookAsync + ( + ulong webhookId, + string? reason = null + ) + { + Dictionary headers = []; + if (!string.IsNullOrWhiteSpace(reason)) + { + headers[REASON_HEADER_NAME] = reason; + } + + string route = $"{Endpoints.WEBHOOKS}/{webhookId}"; + string url = $"{Endpoints.WEBHOOKS}/{webhookId}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Delete, + Headers = headers + }; + + await this.rest.ExecuteRequestAsync(request); + } + + internal async ValueTask DeleteWebhookAsync + ( + ulong webhookId, + string webhookToken, + string? reason = null + ) + { + Dictionary headers = []; + if (!string.IsNullOrWhiteSpace(reason)) + { + headers[REASON_HEADER_NAME] = reason; + } + + string route = $"{Endpoints.WEBHOOKS}/{webhookId}/:webhook_token"; + string url = $"{Endpoints.WEBHOOKS}/{webhookId}/{webhookToken}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Delete, + IsExemptFromGlobalLimit = true, + Headers = headers + }; + + await this.rest.ExecuteRequestAsync(request); + } + + internal async ValueTask ExecuteWebhookAsync + ( + ulong webhookId, + string webhookToken, + DiscordWebhookBuilder builder + ) + { + builder.Validate(); + + if (builder.Embeds != null) + { + foreach (DiscordEmbed embed in builder.Embeds) + { + if (embed.Timestamp != null) + { + embed.Timestamp = embed.Timestamp.Value.ToUniversalTime(); + } + } + } + + Dictionary values = []; + RestWebhookExecutePayload pld = new() + { + Content = builder.Content, + Username = builder.Username.HasValue ? builder.Username.Value : null, + AvatarUrl = builder.AvatarUrl.HasValue ? builder.AvatarUrl.Value : null, + IsTTS = builder.IsTTS, + Embeds = builder.Embeds, + Flags = builder.Flags, + Components = builder.Components, + Poll = builder.Poll?.BuildInternal(), + }; + + if (builder.Mentions != null) + { + pld.Mentions = new DiscordMentions(builder.Mentions, builder.Mentions.Any()); + } + + if (!string.IsNullOrEmpty(builder.Content) || builder.Embeds?.Count > 0 || builder.IsTTS == true || builder.Mentions != null) + { + values["payload_json"] = DiscordJson.SerializeObject(pld); + } + + string route = $"{Endpoints.WEBHOOKS}/{webhookId}/:webhook_token"; + QueryUriBuilder url = new($"{Endpoints.WEBHOOKS}/{webhookId}/{webhookToken}"); + url.AddParameter("wait", "true"); + url.AddParameter("with_components", "true"); + + if (builder.ThreadId.HasValue) + { + url.AddParameter("thread_id", builder.ThreadId.Value.ToString()); + } + + MultipartRestRequest request = new() + { + Route = route, + Url = url.Build(), + Method = HttpMethod.Post, + Values = values, + Files = builder.Files, + IsExemptFromGlobalLimit = true + }; + + RestResponse res; + try + { + res = await this.rest.ExecuteRequestAsync(request); + } + finally + { + builder.ResetFileStreamPositions(); + } + DiscordMessage ret = JsonConvert.DeserializeObject(res.Response!)!; + + ret.Discord = this.discord!; + return ret; + } + + internal async ValueTask ExecuteWebhookSlackAsync + ( + ulong webhookId, + string webhookToken, + string jsonPayload + ) + { + string route = $"{Endpoints.WEBHOOKS}/{webhookId}/:webhook_token/{Endpoints.SLACK}"; + QueryUriBuilder url = new($"{Endpoints.WEBHOOKS}/{webhookId}/{webhookToken}/{Endpoints.SLACK}"); + + RestRequest request = new() + { + Route = route, + Url = url.Build(), + Method = HttpMethod.Post, + Payload = jsonPayload, + IsExemptFromGlobalLimit = true + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + DiscordMessage ret = JsonConvert.DeserializeObject(res.Response!)!; + ret.Discord = this.discord!; + return ret; + } + + internal async ValueTask ExecuteWebhookGithubAsync + ( + ulong webhookId, + string webhookToken, + string jsonPayload + ) + { + string route = $"{Endpoints.WEBHOOKS}/{webhookId}/:webhook_token{Endpoints.GITHUB}"; + QueryUriBuilder url = new($"{Endpoints.WEBHOOKS}/{webhookId}/{webhookToken}{Endpoints.GITHUB}"); + url.AddParameter("wait", "true"); + + RestRequest request = new() + { + Route = route, + Url = url.Build(), + Method = HttpMethod.Post, + Payload = jsonPayload, + IsExemptFromGlobalLimit = true + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + DiscordMessage ret = JsonConvert.DeserializeObject(res.Response!)!; + ret.Discord = this.discord!; + return ret; + } + + internal async ValueTask EditWebhookMessageAsync + ( + ulong webhookId, + string webhookToken, + ulong messageId, + DiscordWebhookBuilder builder, + IEnumerable? attachments = null + ) + { + builder.Validate(true); + + DiscordMentions? mentions = builder.Mentions != null ? new DiscordMentions(builder.Mentions, builder.Mentions.Any()) : null; + + RestWebhookMessageEditPayload pld = new() + { + Content = builder.Content, + Embeds = builder.Embeds, + Mentions = mentions, + Flags = builder.Flags, + Components = builder.Components, + Attachments = attachments + }; + + string route = $"{Endpoints.WEBHOOKS}/{webhookId}/:webhook_token/{Endpoints.MESSAGES}/:message_id"; + string url = $"{Endpoints.WEBHOOKS}/{webhookId}/{webhookToken}/{Endpoints.MESSAGES}/{messageId}"; + + Dictionary values = new() + { + ["payload_json"] = DiscordJson.SerializeObject(pld) + }; + + MultipartRestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Patch, + Values = values, + Files = builder.Files, + IsExemptFromGlobalLimit = true + }; + + RestResponse res; + try + { + res = await this.rest.ExecuteRequestAsync(request); + } + finally + { + builder.ResetFileStreamPositions(); + } + + return PrepareMessage(JObject.Parse(res.Response!)); + } + + internal async ValueTask DeleteWebhookMessageAsync + ( + ulong webhookId, + string webhookToken, + ulong messageId + ) + { + string route = $"{Endpoints.WEBHOOKS}/{webhookId}/:webhook_token/{Endpoints.MESSAGES}/:message_id"; + string url = $"{Endpoints.WEBHOOKS}/{webhookId}/{webhookToken}/{Endpoints.MESSAGES}/{messageId}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Delete, + IsExemptFromGlobalLimit = true + }; + + await this.rest.ExecuteRequestAsync(request); + } + #endregion + + #region Reactions + internal async ValueTask CreateReactionAsync + ( + ulong channelId, + ulong messageId, + string emoji + ) + { + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/:message_id/{Endpoints.REACTIONS}/:emoji/{Endpoints.ME}"; + string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/{messageId}/{Endpoints.REACTIONS}/{emoji}/{Endpoints.ME}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Put + }; + + await this.rest.ExecuteRequestAsync(request); + } + + internal async ValueTask DeleteOwnReactionAsync + ( + ulong channelId, + ulong messageId, + string emoji + ) + { + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/:message_id/{Endpoints.REACTIONS}/:emoji/{Endpoints.ME}"; + string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/{messageId}/{Endpoints.REACTIONS}/{emoji}/{Endpoints.ME}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Delete + }; + + await this.rest.ExecuteRequestAsync(request); + } + + internal async ValueTask DeleteUserReactionAsync + ( + ulong channelId, + ulong messageId, + ulong userId, + string emoji, + string? reason + ) + { + Dictionary headers = []; + if (!string.IsNullOrWhiteSpace(reason)) + { + headers[REASON_HEADER_NAME] = reason; + } + + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/:message_id/{Endpoints.REACTIONS}/:emoji/:user_id"; + string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/{messageId}/{Endpoints.REACTIONS}/{emoji}/{userId}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Delete, + Headers = headers + }; + + await this.rest.ExecuteRequestAsync(request); + } + + internal async ValueTask> GetReactionsAsync + ( + ulong channelId, + ulong messageId, + string emoji, + ulong? afterId = null, + int limit = 25 + ) + { + QueryUriBuilder urlparams = new($"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/{messageId}/{Endpoints.REACTIONS}/{emoji}"); + if (afterId.HasValue) + { + urlparams.AddParameter("after", afterId.Value.ToString(CultureInfo.InvariantCulture)); + } + + urlparams.AddParameter("limit", limit.ToString(CultureInfo.InvariantCulture)); + + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/:message_id/{Endpoints.REACTIONS}/:emoji"; + + RestRequest request = new() + { + Route = route, + Url = urlparams.Build(), + Method = HttpMethod.Get + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + IEnumerable usersRaw = JsonConvert.DeserializeObject>(res.Response!)!; + List users = []; + foreach (TransportUser xr in usersRaw) + { + DiscordUser usr = new(xr) + { + Discord = this.discord! + }; + usr = this.discord!.UpdateUserCache(usr); + + users.Add(usr); + } + + return new ReadOnlyCollection(new List(users)); + } + + internal async ValueTask DeleteAllReactionsAsync + ( + ulong channelId, + ulong messageId, + string? reason = null + ) + { + Dictionary headers = []; + if (!string.IsNullOrWhiteSpace(reason)) + { + headers[REASON_HEADER_NAME] = reason; + } + + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/:message_id/{Endpoints.REACTIONS}"; + string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/{messageId}/{Endpoints.REACTIONS}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Delete, + Headers = headers + }; + + await this.rest.ExecuteRequestAsync(request); + } + + internal async ValueTask DeleteReactionsEmojiAsync + ( + ulong channelId, + ulong messageId, + string emoji + ) + { + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/:message_id/{Endpoints.REACTIONS}/:emoji"; + string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.MESSAGES}/{messageId}/{Endpoints.REACTIONS}/{emoji}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Delete + }; + + await this.rest.ExecuteRequestAsync(request); + } + #endregion + + #region Polls + + internal async ValueTask> GetPollAnswerVotersAsync + ( + ulong channelId, + ulong messageId, + int answerId, + ulong? after, + int? limit + ) + { + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.POLLS}/:message_id/{Endpoints.ANSWERS}/:answer_id"; + QueryUriBuilder url = new($"{Endpoints.CHANNELS}/{channelId}/{Endpoints.POLLS}/{messageId}/{Endpoints.ANSWERS}/{answerId}"); + + if (limit > 0) + { + url.AddParameter("limit", limit.Value.ToString(CultureInfo.InvariantCulture)); + } + + if (after > 0) + { + url.AddParameter("after", after.Value.ToString(CultureInfo.InvariantCulture)); + } + + RestRequest request = new() + { + Route = route, + Url = url.Build(), + Method = HttpMethod.Get + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + JToken jto = JToken.Parse(res.Response!); + + return (jto as JArray ?? jto["users"] as JArray)! + .Select(j => j.ToDiscordObject()) + .ToList(); + } + + internal async ValueTask EndPollAsync + ( + ulong channelId, + ulong messageId + ) + { + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.POLLS}/:message_id/{Endpoints.EXPIRE}"; + string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.POLLS}/{messageId}/{Endpoints.EXPIRE}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Post + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + DiscordMessage ret = PrepareMessage(JObject.Parse(res.Response!)); + + return ret; + } + + #endregion + + #region Emoji + internal async ValueTask> GetGuildEmojisAsync + ( + ulong guildId + ) + { + string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.EMOJIS}"; + string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.EMOJIS}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Get + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + IEnumerable emojisRaw = JsonConvert.DeserializeObject>(res.Response!)!; + + this.discord!.Guilds.TryGetValue(guildId, out DiscordGuild? guild); + Dictionary users = []; + List emojis = []; + foreach (JObject rawEmoji in emojisRaw) + { + DiscordGuildEmoji discordGuildEmoji = rawEmoji.ToDiscordObject(); + + if (guild is not null) + { + discordGuildEmoji.Guild = guild; + } + + TransportUser? rawUser = rawEmoji["user"]?.ToDiscordObject(); + if (rawUser != null) + { + if (!users.ContainsKey(rawUser.Id)) + { + DiscordUser user = guild is not null && guild.Members.TryGetValue(rawUser.Id, out DiscordMember? member) ? member : new DiscordUser(rawUser); + users[user.Id] = user; + } + + discordGuildEmoji.User = users[rawUser.Id]; + } + + emojis.Add(discordGuildEmoji); + } + + return new ReadOnlyCollection(emojis); + } + + internal async ValueTask GetGuildEmojiAsync + ( + ulong guildId, + ulong emojiId + ) + { + string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.EMOJIS}/:emoji_id"; + string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.EMOJIS}/{emojiId}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Get + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + this.discord!.Guilds.TryGetValue(guildId, out DiscordGuild? guild); + + JObject emojiRaw = JObject.Parse(res.Response!); + DiscordGuildEmoji emoji = emojiRaw.ToDiscordObject(); + + if (guild is not null) + { + emoji.Guild = guild; + } + + TransportUser? rawUser = emojiRaw["user"]?.ToDiscordObject(); + if (rawUser != null) + { + emoji.User = guild is not null && guild.Members.TryGetValue(rawUser.Id, out DiscordMember? member) ? member : new DiscordUser(rawUser); + } + + return emoji; + } + + internal async ValueTask CreateGuildEmojiAsync + ( + ulong guildId, + string name, + string imageb64, + IEnumerable? roles = null, + string? reason = null + ) + { + RestGuildEmojiCreatePayload pld = new() + { + Name = name, + ImageB64 = imageb64, + Roles = roles?.ToArray() + }; + + Dictionary headers = []; + if (!string.IsNullOrWhiteSpace(reason)) + { + headers[REASON_HEADER_NAME] = reason; + } + + string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.EMOJIS}"; + string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.EMOJIS}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Post, + Payload = DiscordJson.SerializeObject(pld), + Headers = headers + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + this.discord!.Guilds.TryGetValue(guildId, out DiscordGuild? guild); + + JObject emojiRaw = JObject.Parse(res.Response!); + DiscordGuildEmoji emoji = emojiRaw.ToDiscordObject(); + + if (guild is not null) + { + emoji.Guild = guild; + } + + TransportUser? rawUser = emojiRaw["user"]?.ToDiscordObject(); + emoji.User = rawUser != null + ? guild is not null && guild.Members.TryGetValue(rawUser.Id, out DiscordMember? member) ? member : new DiscordUser(rawUser) + : this.discord.CurrentUser; + + return emoji; + } + + internal async ValueTask ModifyGuildEmojiAsync + ( + ulong guildId, + ulong emojiId, + string? name = null, + IEnumerable? roles = null, + string? reason = null + ) + { + RestGuildEmojiModifyPayload pld = new() + { + Name = name, + Roles = roles?.ToArray() + }; + + Dictionary headers = []; + if (!string.IsNullOrWhiteSpace(reason)) + { + headers[REASON_HEADER_NAME] = reason; + } + + string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.EMOJIS}/:emoji_id"; + string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.EMOJIS}/{emojiId}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Patch, + Payload = DiscordJson.SerializeObject(pld), + Headers = headers + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + this.discord!.Guilds.TryGetValue(guildId, out DiscordGuild? guild); + + JObject emojiRaw = JObject.Parse(res.Response!); + DiscordGuildEmoji emoji = emojiRaw.ToDiscordObject(); + + if (guild is not null) + { + emoji.Guild = guild; + } + + TransportUser? rawUser = emojiRaw["user"]?.ToDiscordObject(); + if (rawUser != null) + { + emoji.User = guild is not null && guild.Members.TryGetValue(rawUser.Id, out DiscordMember? member) ? member : new DiscordUser(rawUser); + } + + return emoji; + } + + internal async ValueTask DeleteGuildEmojiAsync + ( + ulong guildId, + ulong emojiId, + string? reason = null + ) + { + Dictionary headers = []; + if (!string.IsNullOrWhiteSpace(reason)) + { + headers[REASON_HEADER_NAME] = reason; + } + + string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.EMOJIS}/:emoji_id"; + string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.EMOJIS}/{emojiId}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Delete, + Headers = headers + }; + + await this.rest.ExecuteRequestAsync(request); + } + #endregion + + #region Application Commands + internal async ValueTask> GetGlobalApplicationCommandsAsync + ( + ulong applicationId, + bool withLocalizations = false + ) + { + string route = $"{Endpoints.APPLICATIONS}/:application_id/{Endpoints.COMMANDS}"; + QueryUriBuilder builder = new($"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.COMMANDS}"); + + if (withLocalizations) + { + builder.AddParameter("with_localizations", "true"); + } + + RestRequest request = new() + { + Route = route, + Url = builder.Build(), + Method = HttpMethod.Get + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + IEnumerable ret = JsonConvert.DeserializeObject>(res.Response!)!; + foreach (DiscordApplicationCommand app in ret) + { + app.Discord = this.discord!; + } + + return ret.ToList(); + } + + internal async ValueTask> BulkOverwriteGlobalApplicationCommandsAsync + ( + ulong applicationId, + IEnumerable commands + ) + { + List pld = []; + foreach (DiscordApplicationCommand command in commands) + { + pld.Add(new RestApplicationCommandCreatePayload + { + Type = command.Type, + Name = command.Name, + Description = command.Description, + Options = command.Options, + DefaultPermission = command.DefaultPermission, + NameLocalizations = command.NameLocalizations, + DescriptionLocalizations = command.DescriptionLocalizations, + AllowDMUsage = command.AllowDMUsage, + DefaultMemberPermissions = command.DefaultMemberPermissions, + NSFW = command.NSFW, + AllowedContexts = command.Contexts, + InstallTypes = command.IntegrationTypes, + }); + } + + string route = $"{Endpoints.APPLICATIONS}/:application_id/{Endpoints.COMMANDS}"; + string url = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.COMMANDS}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Put, + Payload = DiscordJson.SerializeObject(pld) + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + IEnumerable ret = JsonConvert.DeserializeObject>(res.Response!)!; + foreach (DiscordApplicationCommand app in ret) + { + app.Discord = this.discord!; + } + + return ret.ToList(); + } + + internal async ValueTask CreateGlobalApplicationCommandAsync + ( + ulong applicationId, + DiscordApplicationCommand command + ) + { + RestApplicationCommandCreatePayload pld = new() + { + Type = command.Type, + Name = command.Name, + Description = command.Description, + Options = command.Options, + DefaultPermission = command.DefaultPermission, + NameLocalizations = command.NameLocalizations, + DescriptionLocalizations = command.DescriptionLocalizations, + AllowDMUsage = command.AllowDMUsage, + DefaultMemberPermissions = command.DefaultMemberPermissions, + NSFW = command.NSFW, + AllowedContexts = command.Contexts, + InstallTypes = command.IntegrationTypes, + }; + + string route = $"{Endpoints.APPLICATIONS}/:application_id/{Endpoints.COMMANDS}"; + string url = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.COMMANDS}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Post, + Payload = DiscordJson.SerializeObject(pld) + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + DiscordApplicationCommand ret = JsonConvert.DeserializeObject(res.Response!)!; + ret.Discord = this.discord!; + + return ret; + } + + internal async ValueTask GetGlobalApplicationCommandAsync + ( + ulong applicationId, + ulong commandId + ) + { + string route = $"{Endpoints.APPLICATIONS}/:application_id/{Endpoints.COMMANDS}/:command_id"; + string url = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.COMMANDS}/{commandId}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Get + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + DiscordApplicationCommand ret = JsonConvert.DeserializeObject(res.Response!)!; + ret.Discord = this.discord!; + + return ret; + } + + internal async ValueTask EditGlobalApplicationCommandAsync + ( + ulong applicationId, + ulong commandId, + Optional name = default, + Optional description = default, + Optional> options = default, + Optional defaultPermission = default, + Optional nsfw = default, + IReadOnlyDictionary? nameLocalizations = null, + IReadOnlyDictionary? descriptionLocalizations = null, + Optional allowDmUsage = default, + Optional defaultMemberPermissions = default, + Optional> allowedContexts = default, + Optional> installTypes = default + ) + { + RestApplicationCommandEditPayload pld = new() + { + Name = name, + Description = description, + Options = options, + DefaultPermission = defaultPermission, + NameLocalizations = nameLocalizations, + DescriptionLocalizations = descriptionLocalizations, + AllowDMUsage = allowDmUsage, + DefaultMemberPermissions = defaultMemberPermissions, + NSFW = nsfw, + AllowedContexts = allowedContexts, + InstallTypes = installTypes, + }; + + string route = $"{Endpoints.APPLICATIONS}/:application_id/{Endpoints.COMMANDS}/:command_id"; + string url = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.COMMANDS}/{commandId}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Patch, + Payload = DiscordJson.SerializeObject(pld) + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + DiscordApplicationCommand ret = JsonConvert.DeserializeObject(res.Response!)!; + ret.Discord = this.discord!; + + return ret; + } + + internal async ValueTask DeleteGlobalApplicationCommandAsync + ( + ulong applicationId, + ulong commandId + ) + { + string route = $"{Endpoints.APPLICATIONS}/:application_id/{Endpoints.COMMANDS}/:command_id"; + string url = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.COMMANDS}/{commandId}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Delete + }; + + await this.rest.ExecuteRequestAsync(request); + } + + internal async ValueTask> GetGuildApplicationCommandsAsync + ( + ulong applicationId, + ulong guildId, + bool withLocalizations = false + ) + { + string route = $"{Endpoints.APPLICATIONS}/:application_id/{Endpoints.GUILDS}/:guild_id/{Endpoints.COMMANDS}"; + QueryUriBuilder builder = new($"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.GUILDS}/{guildId}/{Endpoints.COMMANDS}"); + + if (withLocalizations) + { + builder.AddParameter("with_localizations", "true"); + } + + RestRequest request = new() + { + Route = route, + Url = builder.Build(), + Method = HttpMethod.Get + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + IEnumerable ret = JsonConvert.DeserializeObject>(res.Response!)!; + foreach (DiscordApplicationCommand app in ret) + { + app.Discord = this.discord!; + } + + return ret.ToList(); + } + + internal async ValueTask> BulkOverwriteGuildApplicationCommandsAsync + ( + ulong applicationId, + ulong guildId, + IEnumerable commands + ) + { + List pld = []; + foreach (DiscordApplicationCommand command in commands) + { + pld.Add(new RestApplicationCommandCreatePayload + { + Type = command.Type, + Name = command.Name, + Description = command.Description, + Options = command.Options, + DefaultPermission = command.DefaultPermission, + NameLocalizations = command.NameLocalizations, + DescriptionLocalizations = command.DescriptionLocalizations, + AllowDMUsage = command.AllowDMUsage, + DefaultMemberPermissions = command.DefaultMemberPermissions, + NSFW = command.NSFW + }); + } + + string route = $"{Endpoints.APPLICATIONS}/:application_id/{Endpoints.GUILDS}/:guild_id/{Endpoints.COMMANDS}"; + string url = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.GUILDS}/{guildId}/{Endpoints.COMMANDS}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Put, + Payload = DiscordJson.SerializeObject(pld) + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + IEnumerable ret = JsonConvert.DeserializeObject>(res.Response!)!; + foreach (DiscordApplicationCommand app in ret) + { + app.Discord = this.discord!; + } + + return ret.ToList(); + } + + internal async ValueTask CreateGuildApplicationCommandAsync + ( + ulong applicationId, + ulong guildId, + DiscordApplicationCommand command + ) + { + RestApplicationCommandCreatePayload pld = new() + { + Type = command.Type, + Name = command.Name, + Description = command.Description, + Options = command.Options, + DefaultPermission = command.DefaultPermission, + NameLocalizations = command.NameLocalizations, + DescriptionLocalizations = command.DescriptionLocalizations, + AllowDMUsage = command.AllowDMUsage, + DefaultMemberPermissions = command.DefaultMemberPermissions, + NSFW = command.NSFW + }; + + string route = $"{Endpoints.APPLICATIONS}/:application_id/{Endpoints.GUILDS}/:guild_id/{Endpoints.COMMANDS}"; + string url = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.GUILDS}/{guildId}/{Endpoints.COMMANDS}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Post, + Payload = DiscordJson.SerializeObject(pld) + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + DiscordApplicationCommand ret = JsonConvert.DeserializeObject(res.Response!)!; + ret.Discord = this.discord!; + + return ret; + } + + internal async ValueTask GetGuildApplicationCommandAsync + ( + ulong applicationId, + ulong guildId, + ulong commandId + ) + { + string route = $"{Endpoints.APPLICATIONS}/:application_id/{Endpoints.GUILDS}/:guild_id/{Endpoints.COMMANDS}/:command_id"; + string url = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.GUILDS}/{guildId}/{Endpoints.COMMANDS}/{commandId}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Get + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + DiscordApplicationCommand ret = JsonConvert.DeserializeObject(res.Response!)!; + ret.Discord = this.discord!; + + return ret; + } + + internal async ValueTask EditGuildApplicationCommandAsync + ( + ulong applicationId, + ulong guildId, + ulong commandId, + Optional name = default, + Optional description = default, + Optional> options = default, + Optional defaultPermission = default, + Optional nsfw = default, + IReadOnlyDictionary? nameLocalizations = null, + IReadOnlyDictionary? descriptionLocalizations = null, + Optional allowDmUsage = default, + Optional defaultMemberPermissions = default, + Optional> allowedContexts = default, + Optional> installTypes = default + ) + { + RestApplicationCommandEditPayload pld = new() + { + Name = name, + Description = description, + Options = options, + DefaultPermission = defaultPermission, + NameLocalizations = nameLocalizations, + DescriptionLocalizations = descriptionLocalizations, + AllowDMUsage = allowDmUsage, + DefaultMemberPermissions = defaultMemberPermissions, + NSFW = nsfw, + AllowedContexts = allowedContexts, + InstallTypes = installTypes + }; + + string route = $"{Endpoints.APPLICATIONS}/:application_id/{Endpoints.GUILDS}/:guild_id/{Endpoints.COMMANDS}/:command_id"; + string url = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.GUILDS}/{guildId}/{Endpoints.COMMANDS}/{commandId}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Patch, + Payload = DiscordJson.SerializeObject(pld) + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + DiscordApplicationCommand ret = JsonConvert.DeserializeObject(res.Response!)!; + ret.Discord = this.discord!; + + return ret; + } + + internal async ValueTask DeleteGuildApplicationCommandAsync + ( + ulong applicationId, + ulong guildId, + ulong commandId + ) + { + string route = $"{Endpoints.APPLICATIONS}/:application_id/{Endpoints.GUILDS}/:guild_id/{Endpoints.COMMANDS}/:command_id"; + string url = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.GUILDS}/{guildId}/{Endpoints.COMMANDS}/{commandId}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Delete + }; + + await this.rest.ExecuteRequestAsync(request); + } + + internal async ValueTask CreateInteractionResponseAsync + ( + ulong interactionId, + string interactionToken, + DiscordInteractionResponseType type, + DiscordInteractionResponseBuilder? builder + ) + { + if (builder?.Embeds != null) + { + foreach (DiscordEmbed embed in builder.Embeds) + { + if (embed.Timestamp is not null) + { + embed.Timestamp = embed.Timestamp.Value.ToUniversalTime(); + } + } + } + + DiscordInteractionResponsePayload payload = new() + { + Type = type, + Data = builder is not null + ? new DiscordInteractionApplicationCommandCallbackData + { + Content = builder.Content, + Title = builder.Title, + CustomId = builder.CustomId, + Embeds = builder.Embeds, + IsTTS = builder.IsTTS, + Mentions = new DiscordMentions(builder.Mentions ?? Mentions.All, builder.Mentions?.Any() ?? false), + Flags = builder.Flags, + Components = builder.Components, + Choices = builder.Choices, + Poll = builder.Poll?.BuildInternal(), + } + : null + }; + + Dictionary values = []; + + if (builder != null) + { + if (!string.IsNullOrEmpty(builder.Content) || builder.Embeds?.Count > 0 || builder.IsTTS == true || builder.Mentions != null) + { + values["payload_json"] = DiscordJson.SerializeObject(payload); + } + } + + string route = $"{Endpoints.INTERACTIONS}/{interactionId}/:interaction_token/{Endpoints.CALLBACK}"; + string url = $"{Endpoints.INTERACTIONS}/{interactionId}/{interactionToken}/{Endpoints.CALLBACK}"; + + if (builder is not null && builder.Files.Count != 0) + { + MultipartRestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Post, + Values = values, + Files = builder.Files, + IsExemptFromAllLimits = true + }; + + try + { + await this.rest.ExecuteRequestAsync(request); + } + finally + { + builder.ResetFileStreamPositions(); + } + } + else + { + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Post, + Payload = DiscordJson.SerializeObject(payload), + IsExemptFromGlobalLimit = true + }; + + await this.rest.ExecuteRequestAsync(request); + } + } + + internal async ValueTask GetOriginalInteractionResponseAsync + ( + ulong applicationId, + string interactionToken + ) + { + string route = $"{Endpoints.WEBHOOKS}/:application_id/{interactionToken}/{Endpoints.MESSAGES}/{Endpoints.ORIGINAL}"; + string url = $"{Endpoints.WEBHOOKS}/{applicationId}/{interactionToken}/{Endpoints.MESSAGES}/{Endpoints.ORIGINAL}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Get, + IsExemptFromGlobalLimit = true + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + DiscordMessage ret = JsonConvert.DeserializeObject(res.Response!)!; + + ret.Channel = (this.discord as DiscordClient).InternalGetCachedChannel(ret.ChannelId); + ret.Discord = this.discord!; + + return ret; + } + + internal async ValueTask EditOriginalInteractionResponseAsync + ( + ulong applicationId, + string interactionToken, + DiscordWebhookBuilder builder, + IEnumerable attachments + ) + { + { + builder.Validate(true); + + DiscordMentions? mentions = builder.Mentions != null ? new DiscordMentions(builder.Mentions, builder.Mentions.Any()) : null; + + RestWebhookMessageEditPayload pld = new() + { + Content = builder.Content, + Embeds = builder.Embeds, + Mentions = mentions, + Flags = builder.Flags, + Components = builder.Components, + Attachments = attachments + }; + + string route = $"{Endpoints.WEBHOOKS}/:application_id/{interactionToken}/{Endpoints.MESSAGES}/@original"; + string url = $"{Endpoints.WEBHOOKS}/{applicationId}/{interactionToken}/{Endpoints.MESSAGES}/@original"; + + Dictionary values = new() + { + ["payload_json"] = DiscordJson.SerializeObject(pld) + }; + + MultipartRestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Patch, + Values = values, + Files = builder.Files, + IsExemptFromAllLimits = true + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + DiscordMessage ret = JsonConvert.DeserializeObject(res.Response!)!; + ret.Discord = this.discord!; + + foreach (DiscordMessageFile file in builder.Files.Where(x => x.ResetPositionTo.HasValue)) + { + file.Stream.Position = file.ResetPositionTo!.Value; + } + + return ret; + } + } + + internal async ValueTask DeleteOriginalInteractionResponseAsync + ( + ulong applicationId, + string interactionToken + ) + { + string route = $"{Endpoints.WEBHOOKS}/:application_id/{interactionToken}/{Endpoints.MESSAGES}/@original"; + string url = $"{Endpoints.WEBHOOKS}/{applicationId}/{interactionToken}/{Endpoints.MESSAGES}/@original"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Delete, + IsExemptFromAllLimits = true + }; + + await this.rest.ExecuteRequestAsync(request); + } + + internal async ValueTask CreateFollowupMessageAsync + ( + ulong applicationId, + string interactionToken, + DiscordFollowupMessageBuilder builder + ) + { + builder.Validate(); + + if (builder.Embeds != null) + { + foreach (DiscordEmbed embed in builder.Embeds) + { + if (embed.Timestamp != null) + { + embed.Timestamp = embed.Timestamp.Value.ToUniversalTime(); + } + } + } + + Dictionary values = []; + RestFollowupMessageCreatePayload pld = new() + { + Content = builder.Content, + IsTTS = builder.IsTTS, + Embeds = builder.Embeds, + Flags = builder.Flags, + Components = builder.Components + }; + + if (builder.Mentions != null) + { + pld.Mentions = new DiscordMentions(builder.Mentions, builder.Mentions.Any()); + } + + if (!string.IsNullOrEmpty(builder.Content) || builder.Embeds?.Count > 0 || builder.IsTTS == true || builder.Mentions != null) + { + values["payload_json"] = DiscordJson.SerializeObject(pld); + } + + string route = $"{Endpoints.WEBHOOKS}/:application_id/{interactionToken}"; + string url = $"{Endpoints.WEBHOOKS}/{applicationId}/{interactionToken}"; + + MultipartRestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Post, + Values = values, + Files = builder.Files, + IsExemptFromAllLimits = true + }; + + RestResponse res; + try + { + res = await this.rest.ExecuteRequestAsync(request); + } + finally + { + builder.ResetFileStreamPositions(); + } + DiscordMessage ret = JsonConvert.DeserializeObject(res.Response!)!; + + ret.Discord = this.discord!; + return ret; + } + + internal ValueTask GetFollowupMessageAsync + ( + ulong applicationId, + string interactionToken, + ulong messageId + ) + => GetWebhookMessageAsync(applicationId, interactionToken, messageId); + + internal ValueTask EditFollowupMessageAsync + ( + ulong applicationId, + string interactionToken, + ulong messageId, + DiscordWebhookBuilder builder, + IEnumerable attachments + ) + => EditWebhookMessageAsync(applicationId, interactionToken, messageId, builder, attachments); + + internal ValueTask DeleteFollowupMessageAsync(ulong applicationId, string interactionToken, ulong messageId) + => DeleteWebhookMessageAsync(applicationId, interactionToken, messageId); + + internal async ValueTask> GetGuildApplicationCommandPermissionsAsync + ( + ulong applicationId, + ulong guildId + ) + { + string route = $"{Endpoints.APPLICATIONS}/:application_id/{Endpoints.GUILDS}/:guild_id/{Endpoints.COMMANDS}/{Endpoints.PERMISSIONS}"; + string url = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.GUILDS}/{guildId}/{Endpoints.COMMANDS}/{Endpoints.PERMISSIONS}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Get + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + IEnumerable ret = JsonConvert.DeserializeObject>(res.Response!)!; + + foreach (DiscordGuildApplicationCommandPermissions perm in ret) + { + perm.Discord = this.discord!; + } + + return ret.ToList(); + } + + internal async ValueTask GetApplicationCommandPermissionsAsync + ( + ulong applicationId, + ulong guildId, + ulong commandId + ) + { + string route = $"{Endpoints.APPLICATIONS}/:application_id/{Endpoints.GUILDS}/:guild_id/{Endpoints.COMMANDS}/:command_id/{Endpoints.PERMISSIONS}"; + string url = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.GUILDS}/{guildId}/{Endpoints.COMMANDS}/{commandId}/{Endpoints.PERMISSIONS}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Get + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + DiscordGuildApplicationCommandPermissions ret = JsonConvert.DeserializeObject(res.Response!)!; + + ret.Discord = this.discord!; + return ret; + } + + internal async ValueTask EditApplicationCommandPermissionsAsync + ( + ulong applicationId, + ulong guildId, + ulong commandId, + IEnumerable permissions + ) + { + + RestEditApplicationCommandPermissionsPayload pld = new() + { + Permissions = permissions + }; + + string route = $"{Endpoints.APPLICATIONS}/:application_id/{Endpoints.GUILDS}/:guild_id/{Endpoints.COMMANDS}/:command_id/{Endpoints.PERMISSIONS}"; + string url = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.GUILDS}/{guildId}/{Endpoints.COMMANDS}/{commandId}/{Endpoints.PERMISSIONS}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Put, + Payload = DiscordJson.SerializeObject(pld) + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + DiscordGuildApplicationCommandPermissions ret = + JsonConvert.DeserializeObject(res.Response!)!; + + ret.Discord = this.discord!; + return ret; + } + + internal async ValueTask> BatchEditApplicationCommandPermissionsAsync + ( + ulong applicationId, + ulong guildId, + IEnumerable permissions + ) + { + string route = $"{Endpoints.APPLICATIONS}/:application_id/{Endpoints.GUILDS}/:guild_id/{Endpoints.COMMANDS}/{Endpoints.PERMISSIONS}"; + string url = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.GUILDS}/{guildId}/{Endpoints.COMMANDS}/{Endpoints.PERMISSIONS}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Put, + Payload = DiscordJson.SerializeObject(permissions) + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + IEnumerable ret = + JsonConvert.DeserializeObject>(res.Response!)!; + + foreach (DiscordGuildApplicationCommandPermissions perm in ret) + { + perm.Discord = this.discord!; + } + + return ret.ToList(); + } + #endregion + + #region Misc + internal ValueTask GetCurrentApplicationInfoAsync() + => GetApplicationInfoAsync("@me"); + + internal ValueTask GetApplicationInfoAsync + ( + ulong applicationId + ) + => GetApplicationInfoAsync(applicationId.ToString(CultureInfo.InvariantCulture)); + + private async ValueTask GetApplicationInfoAsync + ( + string applicationId + ) + { + string route = $"{Endpoints.OAUTH2}/{Endpoints.APPLICATIONS}/:application_id"; + string url = $"{Endpoints.OAUTH2}/{Endpoints.APPLICATIONS}/{applicationId}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Get + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + return JsonConvert.DeserializeObject(res.Response!)!; + } + + internal async ValueTask> GetApplicationAssetsAsync + ( + DiscordApplication application + ) + { + string route = $"{Endpoints.OAUTH2}/{Endpoints.APPLICATIONS}/:application_id/{Endpoints.ASSETS}"; + string url = $"{Endpoints.OAUTH2}/{Endpoints.APPLICATIONS}/{application.Id}/{Endpoints.ASSETS}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Get + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + IEnumerable assets = JsonConvert.DeserializeObject>(res.Response!)!; + foreach (DiscordApplicationAsset asset in assets) + { + asset.Discord = application.Discord; + asset.Application = application; + } + + return new ReadOnlyCollection(new List(assets)); + } + + internal async ValueTask GetGatewayInfoAsync() + { + Dictionary headers = []; + string route = $"{Endpoints.GATEWAY}/{Endpoints.BOT}"; + string url = route; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Get, + Headers = headers + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + + GatewayInfo info = JObject.Parse(res.Response!).ToDiscordObject(); + info.SessionBucket.ResetAfter = DateTimeOffset.UtcNow + TimeSpan.FromMilliseconds(info.SessionBucket.ResetAfterInternal); + return info; + } + #endregion + + internal async ValueTask CreateApplicationEmojiAsync(ulong applicationId, string name, string image) + { + string route = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.EMOJIS}"; + string url = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.EMOJIS}"; + + RestApplicationEmojiCreatePayload pld = new() + { + Name = name, + ImageB64 = image + }; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Post, + Payload = DiscordJson.SerializeObject(pld) + }; + + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + DiscordEmoji emoji = JsonConvert.DeserializeObject(res.Response!)!; + emoji.Discord = this.discord!; + + return emoji; + } + + internal async ValueTask ModifyApplicationEmojiAsync(ulong applicationId, ulong emojiId, string name) + { + string route = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.EMOJIS}/{emojiId}"; + string url = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.EMOJIS}/{emojiId}"; + + RestApplicationEmojiModifyPayload pld = new() + { + Name = name, + }; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Patch, + Payload = DiscordJson.SerializeObject(pld) + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + DiscordEmoji emoji = JsonConvert.DeserializeObject(res.Response!)!; + + emoji.Discord = this.discord!; + + return emoji; + } + + internal async ValueTask DeleteApplicationEmojiAsync(ulong applicationId, ulong emojiId) + { + string route = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.EMOJIS}/{emojiId}"; + string url = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.EMOJIS}/{emojiId}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Delete + }; + + await this.rest.ExecuteRequestAsync(request); + } + + internal async ValueTask GetApplicationEmojiAsync(ulong applicationId, ulong emojiId) + { + string route = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.EMOJIS}/{emojiId}"; + string url = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.EMOJIS}/{emojiId}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Get + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + DiscordEmoji emoji = JsonConvert.DeserializeObject(res.Response!)!; + emoji.Discord = this.discord!; + + return emoji; + } + + internal async ValueTask> GetApplicationEmojisAsync(ulong applicationId) + { + string route = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.EMOJIS}"; + string url = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.EMOJIS}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Get + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + IEnumerable emojis = JObject.Parse(res.Response!)["items"]!.ToDiscordObject(); + + foreach (DiscordEmoji emoji in emojis) + { + emoji.Discord = this.discord!; + emoji.User!.Discord = this.discord!; + } + + return emojis.ToList(); + } + + internal async ValueTask CreateForumPostAsync + ( + ulong channelId, + string name, + DiscordMessageBuilder message, + DiscordAutoArchiveDuration? autoArchiveDuration = null, + int? rateLimitPerUser = null, + IEnumerable? appliedTags = null + ) + { + string route = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.THREADS}"; + string url = $"{Endpoints.CHANNELS}/{channelId}/{Endpoints.THREADS}"; + + RestForumPostCreatePayload pld = new() + { + Name = name, + ArchiveAfter = autoArchiveDuration, + RateLimitPerUser = rateLimitPerUser, + Message = new RestChannelMessageCreatePayload + { + Content = message.Content, + HasContent = !string.IsNullOrWhiteSpace(message.Content), + Embeds = message.Embeds, + HasEmbed = message.Embeds.Count > 0, + Mentions = new DiscordMentions(message.Mentions, message.Mentions.Any()), + Components = message.Components, + StickersIds = message.Stickers?.Select(s => s.Id) ?? Array.Empty(), + }, + AppliedTags = appliedTags + }; + + JObject ret; + RestResponse res; + if (message.Files.Count is 0) + { + RestRequest req = new() + { + Route = route, + Url = url, + Method = HttpMethod.Post, + Payload = DiscordJson.SerializeObject(pld) + }; + + res = await this.rest.ExecuteRequestAsync(req); + ret = JObject.Parse(res.Response!); + } + else + { + Dictionary values = new() + { + ["payload_json"] = DiscordJson.SerializeObject(pld) + }; + + MultipartRestRequest req = new() + { + Route = route, + Url = url, + Method = HttpMethod.Post, + Values = values, + Files = message.Files + }; + + res = await this.rest.ExecuteRequestAsync(req); + ret = JObject.Parse(res.Response!); + } + + JToken? msgToken = ret["message"]; + ret.Remove("message"); + + DiscordMessage msg = PrepareMessage(msgToken!); + // We know the return type; deserialize directly. + DiscordThreadChannel chn = ret.ToDiscordObject(); + chn.Discord = this.discord!; + + return new DiscordForumPostStarter(chn, msg); + } + + /// + /// Internal method to create an auto-moderation rule in a guild. + /// + /// The id of the guild where the rule will be created. + /// The rule name. + /// The Discord event that will trigger the rule. + /// The rule trigger. + /// The trigger metadata. + /// The actions that will run when a rule is triggered. + /// Whenever the rule is enabled or not. + /// The exempted roles that will not trigger the rule. + /// The exempted channels that will not trigger the rule. + /// The reason for audits logs. + /// The created rule. + internal async ValueTask CreateGuildAutoModerationRuleAsync + ( + ulong guildId, + string name, + DiscordRuleEventType eventType, + DiscordRuleTriggerType triggerType, + DiscordRuleTriggerMetadata triggerMetadata, + IReadOnlyList actions, + Optional enabled = default, + Optional> exemptRoles = default, + Optional> exemptChannels = default, + string? reason = null + ) + { + string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.AUTO_MODERATION}/{Endpoints.RULES}"; + string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.AUTO_MODERATION}/{Endpoints.RULES}"; + + Dictionary headers = []; + if (!string.IsNullOrWhiteSpace(reason)) + { + headers[REASON_HEADER_NAME] = reason; + } + + string payload = DiscordJson.SerializeObject(new + { + guild_id = guildId, + name, + event_type = eventType, + trigger_type = triggerType, + trigger_metadata = triggerMetadata, + actions, + enabled, + exempt_roles = exemptRoles.Value.Select(x => x.Id).ToArray(), + exempt_channels = exemptChannels.Value.Select(x => x.Id).ToArray() + }); + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Post, + Headers = headers, + Payload = payload + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + DiscordAutoModerationRule rule = JsonConvert.DeserializeObject(res.Response!)!; + + return rule; + } + + /// + /// Internal method to get an auto-moderation rule in a guild. + /// + /// The guild id where the rule is in. + /// The rule id. + /// The rule found. + internal async ValueTask GetGuildAutoModerationRuleAsync + ( + ulong guildId, + ulong ruleId + ) + { + string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.AUTO_MODERATION}/{Endpoints.RULES}/:rule_id"; + string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.AUTO_MODERATION}/{Endpoints.RULES}/{ruleId}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Get + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + DiscordAutoModerationRule rule = JsonConvert.DeserializeObject(res.Response!)!; + + return rule; + } + + /// + /// Internal method to get all auto-moderation rules in a guild. + /// + /// The guild id where rules are in. + /// The rules found. + internal async ValueTask> GetGuildAutoModerationRulesAsync + ( + ulong guildId + ) + { + string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.AUTO_MODERATION}/{Endpoints.RULES}"; + string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.AUTO_MODERATION}/{Endpoints.RULES}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Get + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + IReadOnlyList rules = JsonConvert.DeserializeObject>(res.Response!)!; + + return rules; + } + + /// + /// Internal method to modify an auto-moderation rule in a guild. + /// + /// The id of the guild where the rule will be modified. + /// The id of the rule that will be modified. + /// The rule name. + /// The Discord event that will trigger the rule. + /// The trigger metadata. + /// The actions that will run when a rule is triggered. + /// Whenever the rule is enabled or not. + /// The exempted roles that will not trigger the rule. + /// The exempted channels that will not trigger the rule. + /// The reason for audits logs. + /// The modified rule. + internal async ValueTask ModifyGuildAutoModerationRuleAsync + ( + ulong guildId, + ulong ruleId, + Optional name, + Optional eventType, + Optional triggerMetadata, + Optional> actions, + Optional enabled, + Optional> exemptRoles, + Optional> exemptChannels, + string? reason = null + ) + { + string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.AUTO_MODERATION}/{Endpoints.RULES}/:rule_id"; + string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.AUTO_MODERATION}/{Endpoints.RULES}/{ruleId}"; + + Dictionary headers = []; + if (!string.IsNullOrWhiteSpace(reason)) + { + headers[REASON_HEADER_NAME] = reason; + } + + string payload = DiscordJson.SerializeObject(new + { + name, + event_type = eventType, + trigger_metadata = triggerMetadata, + actions, + enabled, + exempt_roles = exemptRoles.Value.Select(x => x.Id).ToArray(), + exempt_channels = exemptChannels.Value.Select(x => x.Id).ToArray() + }); + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Patch, + Headers = headers, + Payload = payload + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + DiscordAutoModerationRule rule = JsonConvert.DeserializeObject(res.Response!)!; + + return rule; + } + + /// + /// Internal method to delete an auto-moderation rule in a guild. + /// + /// The id of the guild where the rule is in. + /// The rule id that will be deleted. + /// The reason for audits logs. + internal async ValueTask DeleteGuildAutoModerationRuleAsync + ( + ulong guildId, + ulong ruleId, + string? reason = null + ) + { + string route = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.AUTO_MODERATION}/{Endpoints.RULES}/:rule_id"; + string url = $"{Endpoints.GUILDS}/{guildId}/{Endpoints.AUTO_MODERATION}/{Endpoints.RULES}/{ruleId}"; + + Dictionary headers = []; + if (!string.IsNullOrWhiteSpace(reason)) + { + headers[REASON_HEADER_NAME] = reason; + } + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Delete, + Headers = headers + }; + + await this.rest.ExecuteRequestAsync(request); + } + + /// + /// Internal method to get all SKUs belonging to a specific application + /// + /// Id of the application of which SKUs should be returned + /// Returns a list of SKUs + internal async ValueTask> ListStockKeepingUnitsAsync(ulong applicationId) + { + string route = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.SKUS}"; + string url = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.SKUS}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Get + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + IReadOnlyList stockKeepingUnits = JsonConvert.DeserializeObject>(res.Response!)!; + + return stockKeepingUnits; + } + + /// + /// Returns all entitlements for a given app. + /// + /// Application ID to look up entitlements for + /// User ID to look up entitlements for + /// Optional list of SKU IDs to check entitlements for + /// Retrieve entitlements before this entitlement ID + /// Retrieve entitlements after this entitlement ID + /// Guild ID to look up entitlements for + /// Whether or not ended entitlements should be omitted + /// Number of entitlements to return, 1-100, default 100 + /// Returns the list of entitlments. Sorted by id descending (depending on discord) + internal async ValueTask> ListEntitlementsAsync + ( + ulong applicationId, + ulong? userId = null, + IEnumerable? skuIds = null, + ulong? before = null, + ulong? after = null, + ulong? guildId = null, + bool? excludeEnded = null, + int? limit = 100 + ) + { + string route = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.ENTITLEMENTS}"; + string url = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.ENTITLEMENTS}"; + + QueryUriBuilder builder = new(url); + + if (userId is not null) + { + builder.AddParameter("user_id", userId.ToString()); + } + + if (skuIds is not null) + { + builder.AddParameter("sku_ids", string.Join(",", skuIds.Select(x => x.ToString()))); + } + + if (before is not null) + { + builder.AddParameter("before", before.ToString()); + } + + if (after is not null) + { + builder.AddParameter("after", after.ToString()); + } + + if (limit is not null) + { + ArgumentOutOfRangeException.ThrowIfGreaterThan(limit.Value, 100); + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(limit.Value); + + builder.AddParameter("limit", limit.ToString()); + } + + if (guildId is not null) + { + builder.AddParameter("guild_id", guildId.ToString()); + } + + if (excludeEnded is not null) + { + builder.AddParameter("exclude_ended", excludeEnded.ToString()); + } + + RestRequest request = new() + { + Route = route, + Url = builder.ToString(), + Method = HttpMethod.Get + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + IReadOnlyList entitlements = JsonConvert.DeserializeObject>(res.Response!)!; + + return entitlements; + } + + /// + /// For One-Time Purchase consumable SKUs, marks a given entitlement for the user as consumed. + /// + /// The id of the application the entitlement belongs to + /// The id of the entitlement which will be marked as consumed + internal async ValueTask ConsumeEntitlementAsync(ulong applicationId, ulong entitlementId) + { + string route = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.ENTITLEMENTS}/:entitlementId/{Endpoints.CONSUME}"; + string url = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.ENTITLEMENTS}/{entitlementId}/{Endpoints.CONSUME}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Post + }; + + await this.rest.ExecuteRequestAsync(request); + } + + /// + /// Create a test entitlement which can be granted to a user or a guild + /// + /// The id of the application the SKU belongs to + /// The id of the SKU the entitlement belongs to + /// The id of the entity which should recieve the entitlement + /// The type of the entity which should recieve the entitlement + /// Returns a partial entitlment + internal async ValueTask CreateTestEntitlementAsync + ( + ulong applicationId, + ulong skuId, + ulong ownerId, + DiscordTestEntitlementOwnerType ownerType + ) + { + string route = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.ENTITLEMENTS}"; + string url = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.ENTITLEMENTS}"; + + string payload = DiscordJson.SerializeObject( + new RestCreateTestEntitlementPayload() { SkuId = skuId, OwnerId = ownerId, OwnerType = ownerType }); + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Post, + Payload = payload + }; + + RestResponse res = await this.rest.ExecuteRequestAsync(request); + DiscordEntitlement entitlement = JsonConvert.DeserializeObject(res.Response!)!; + + return entitlement; + } + + /// + /// Deletes a test entitlement + /// + /// The id of the application the entitlement belongs to + /// The id of the test entitlement which should be removed + internal async ValueTask DeleteTestEntitlementAsync + ( + ulong applicationId, + ulong entitlementId + ) + { + string route = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.ENTITLEMENTS}/:entitlementId"; + string url = $"{Endpoints.APPLICATIONS}/{applicationId}/{Endpoints.ENTITLEMENTS}/{entitlementId}"; + + RestRequest request = new() + { + Route = route, + Url = url, + Method = HttpMethod.Delete + }; + + await this.rest.ExecuteRequestAsync(request); + } +} diff --git a/DSharpPlus/Net/Rest/Endpoints.cs b/DSharpPlus/Net/Rest/Endpoints.cs index 91fb6e00a8..8ae397a01f 100644 --- a/DSharpPlus/Net/Rest/Endpoints.cs +++ b/DSharpPlus/Net/Rest/Endpoints.cs @@ -1,80 +1,80 @@ -namespace DSharpPlus.Net; - - -internal static class Endpoints -{ - public const string API_VERSION = "10"; - public const string BASE_URI = "https://discord.com/api/v" + API_VERSION; - - public const string ACK = "ack"; - public const string ACTIVE = "active "; - public const string APPLICATIONS = "applications"; - public const string ARCHIVED = "archived"; - public const string ASSETS = "assets"; - public const string AUDIT_LOGS = "audit-logs"; - public const string AUTH = "auth"; - public const string AUTO_MODERATION = "auto-moderation"; - public const string AVATARS = "avatars"; - public const string BANS = "bans"; - public const string BOT = "bot"; - public const string BULK_BAN = "bulk-ban"; - public const string BULK_DELETE = "bulk-delete"; - public const string CALLBACK = "callback"; - public const string CHANNELS = "channels"; - public const string COMMANDS = "commands"; - public const string CONNECTIONS = "connections"; - public const string CONSUME = "consume"; - public const string CROSSPOST = "crosspost"; - public const string EMOJIS = "emojis"; - public const string ENTITLEMENTS = "entitlements"; - public const string EVENTS = "scheduled-events"; - public const string FOLLOWERS = "followers"; - public const string GATEWAY = "gateway"; - public const string GITHUB = "github"; - public const string GUILDS = "guilds"; - public const string ICONS = "icons"; - public const string INTEGRATIONS = "integrations"; - public const string INTERACTIONS = "interactions"; - public const string INVITES = "invites"; - public const string LOGIN = "login"; - public const string ME = "@me"; - public const string MEMBERS = "members"; - public const string MEMBER_VERIFICATION = "member-verification"; - public const string MESSAGES = "messages"; - public const string OAUTH2 = "oauth2"; - public const string ORIGINAL = "@original"; - public const string PERMISSIONS = "permissions"; - public const string PINS = "pins"; - public const string PREVIEW = "preview"; - public const string PRIVATE = "private"; - public const string PRUNE = "prune"; - public const string PUBLIC = "public"; - public const string REACTIONS = "reactions"; - public const string POLLS = "polls"; - public const string EXPIRE = "expire"; - public const string ANSWERS = "answers"; - public const string RECIPIENTS = "recipients"; - public const string REGIONS = "regions"; - public const string ROLES = "roles"; - public const string RULES = "rules"; - public const string SEARCH = "search"; - public const string SKUS = "skus"; - public const string SLACK = "slack"; - public const string STAGE_INSTANCES = "stage-instances"; - public const string STICKERPACKS = "sticker-packs"; - public const string STICKERS = "stickers"; - public const string SYNC = "sync"; - public const string TEMPLATES = "templates"; - public const string THREADS = "threads"; - public const string THREAD_MEMBERS = "thread-members"; - public const string TYPING = "typing"; - public const string USERS = "users"; - public const string VANITY_URL = "vanity-url"; - public const string VOICE = "voice"; - public const string VOICE_STATES = "voice-states"; - public const string WEBHOOKS = "webhooks"; - public const string WELCOME_SCREEN = "welcome-screen"; - public const string WIDGET = "widget"; - public const string WIDGET_JSON = "widget.json"; - public const string WIDGET_PNG = "widget.png"; -} +namespace DSharpPlus.Net; + + +internal static class Endpoints +{ + public const string API_VERSION = "10"; + public const string BASE_URI = "https://discord.com/api/v" + API_VERSION; + + public const string ACK = "ack"; + public const string ACTIVE = "active "; + public const string APPLICATIONS = "applications"; + public const string ARCHIVED = "archived"; + public const string ASSETS = "assets"; + public const string AUDIT_LOGS = "audit-logs"; + public const string AUTH = "auth"; + public const string AUTO_MODERATION = "auto-moderation"; + public const string AVATARS = "avatars"; + public const string BANS = "bans"; + public const string BOT = "bot"; + public const string BULK_BAN = "bulk-ban"; + public const string BULK_DELETE = "bulk-delete"; + public const string CALLBACK = "callback"; + public const string CHANNELS = "channels"; + public const string COMMANDS = "commands"; + public const string CONNECTIONS = "connections"; + public const string CONSUME = "consume"; + public const string CROSSPOST = "crosspost"; + public const string EMOJIS = "emojis"; + public const string ENTITLEMENTS = "entitlements"; + public const string EVENTS = "scheduled-events"; + public const string FOLLOWERS = "followers"; + public const string GATEWAY = "gateway"; + public const string GITHUB = "github"; + public const string GUILDS = "guilds"; + public const string ICONS = "icons"; + public const string INTEGRATIONS = "integrations"; + public const string INTERACTIONS = "interactions"; + public const string INVITES = "invites"; + public const string LOGIN = "login"; + public const string ME = "@me"; + public const string MEMBERS = "members"; + public const string MEMBER_VERIFICATION = "member-verification"; + public const string MESSAGES = "messages"; + public const string OAUTH2 = "oauth2"; + public const string ORIGINAL = "@original"; + public const string PERMISSIONS = "permissions"; + public const string PINS = "pins"; + public const string PREVIEW = "preview"; + public const string PRIVATE = "private"; + public const string PRUNE = "prune"; + public const string PUBLIC = "public"; + public const string REACTIONS = "reactions"; + public const string POLLS = "polls"; + public const string EXPIRE = "expire"; + public const string ANSWERS = "answers"; + public const string RECIPIENTS = "recipients"; + public const string REGIONS = "regions"; + public const string ROLES = "roles"; + public const string RULES = "rules"; + public const string SEARCH = "search"; + public const string SKUS = "skus"; + public const string SLACK = "slack"; + public const string STAGE_INSTANCES = "stage-instances"; + public const string STICKERPACKS = "sticker-packs"; + public const string STICKERS = "stickers"; + public const string SYNC = "sync"; + public const string TEMPLATES = "templates"; + public const string THREADS = "threads"; + public const string THREAD_MEMBERS = "thread-members"; + public const string TYPING = "typing"; + public const string USERS = "users"; + public const string VANITY_URL = "vanity-url"; + public const string VOICE = "voice"; + public const string VOICE_STATES = "voice-states"; + public const string WEBHOOKS = "webhooks"; + public const string WELCOME_SCREEN = "welcome-screen"; + public const string WIDGET = "widget"; + public const string WIDGET_JSON = "widget.json"; + public const string WIDGET_PNG = "widget.png"; +} diff --git a/DSharpPlus/Net/Rest/IRestRequest.cs b/DSharpPlus/Net/Rest/IRestRequest.cs index c38bf1b2a3..b11f4c556e 100644 --- a/DSharpPlus/Net/Rest/IRestRequest.cs +++ b/DSharpPlus/Net/Rest/IRestRequest.cs @@ -1,35 +1,35 @@ -using System.Net.Http; - -namespace DSharpPlus.Net; - -/// -/// Serves as a generic constraint for the rest client. -/// -internal interface IRestRequest -{ - /// - /// Builds the current rest request object into a request message. - /// - public HttpRequestMessage Build(); - - /// - /// The URL this request is made to. This is distinct from the in that the route - /// cannot contain query parameters or secondary IDs necessary for the request. - /// - public string Url { get; init; } - - /// - /// The ratelimiting route this request is made to. - /// - public string Route { get; init; } - - /// - /// Specifies whether this request is exempt from the global limit. Generally applies to webhook requests. - /// - public bool IsExemptFromGlobalLimit { get; init; } - - /// - /// Specifies whether this request is exempt from all ratelimits. - /// - public bool IsExemptFromAllLimits { get; init; } -} +using System.Net.Http; + +namespace DSharpPlus.Net; + +/// +/// Serves as a generic constraint for the rest client. +/// +internal interface IRestRequest +{ + /// + /// Builds the current rest request object into a request message. + /// + public HttpRequestMessage Build(); + + /// + /// The URL this request is made to. This is distinct from the in that the route + /// cannot contain query parameters or secondary IDs necessary for the request. + /// + public string Url { get; init; } + + /// + /// The ratelimiting route this request is made to. + /// + public string Route { get; init; } + + /// + /// Specifies whether this request is exempt from the global limit. Generally applies to webhook requests. + /// + public bool IsExemptFromGlobalLimit { get; init; } + + /// + /// Specifies whether this request is exempt from all ratelimits. + /// + public bool IsExemptFromAllLimits { get; init; } +} diff --git a/DSharpPlus/Net/Rest/IpEndpoint.cs b/DSharpPlus/Net/Rest/IpEndpoint.cs index 76c9e2feb4..d30a027ef9 100644 --- a/DSharpPlus/Net/Rest/IpEndpoint.cs +++ b/DSharpPlus/Net/Rest/IpEndpoint.cs @@ -1,30 +1,30 @@ -using System.Net; - -namespace DSharpPlus.Net; - -/// -/// Represents a network connection IP endpoint. -/// -public struct IpEndpoint -{ - /// - /// Gets or sets the hostname associated with this endpoint. - /// - public IPAddress Address { get; set; } - - /// - /// Gets or sets the port associated with this endpoint. - /// - public int Port { get; set; } - - /// - /// Creates a new IP endpoint structure. - /// - /// IP address to connect to. - /// Port to use for connection. - public IpEndpoint(IPAddress address, int port) - { - this.Address = address; - this.Port = port; - } -} +using System.Net; + +namespace DSharpPlus.Net; + +/// +/// Represents a network connection IP endpoint. +/// +public struct IpEndpoint +{ + /// + /// Gets or sets the hostname associated with this endpoint. + /// + public IPAddress Address { get; set; } + + /// + /// Gets or sets the port associated with this endpoint. + /// + public int Port { get; set; } + + /// + /// Creates a new IP endpoint structure. + /// + /// IP address to connect to. + /// Port to use for connection. + public IpEndpoint(IPAddress address, int port) + { + this.Address = address; + this.Port = port; + } +} diff --git a/DSharpPlus/Net/Rest/MultipartRestRequest.cs b/DSharpPlus/Net/Rest/MultipartRestRequest.cs index eaded92414..97beb4dba8 100644 --- a/DSharpPlus/Net/Rest/MultipartRestRequest.cs +++ b/DSharpPlus/Net/Rest/MultipartRestRequest.cs @@ -1,154 +1,154 @@ -// This file is part of the DSharpPlus project. -// -// Copyright (c) 2015 Mike Santiago -// Copyright (c) 2016-2025 DSharpPlus Contributors -// -// 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.Net.Http; -using System.Net.Http.Headers; -using CommunityToolkit.HighPerformance.Buffers; -using DSharpPlus.Entities; - -namespace DSharpPlus.Net; - -/// -/// Represents a multipart HTTP request. -/// -internal readonly record struct MultipartRestRequest : IRestRequest -{ - /// - public string Url { get; init; } - - /// - /// The method for this request. - /// - public HttpMethod Method { get; init; } - - /// - public string Route { get; init; } - - /// - public bool IsExemptFromGlobalLimit { get; init; } - - /// - /// The headers for this request. - /// - public IReadOnlyDictionary? Headers { get; init; } - - /// - /// Gets the dictionary of values attached to this request. - /// - public IReadOnlyDictionary Values { get; init; } - - /// - /// Gets the dictionary of files attached to this request. - /// - public IReadOnlyList Files { get; init; } - - /// - public bool IsExemptFromAllLimits { get; init; } - - public HttpRequestMessage Build() - { - HttpRequestMessage request = new() - { - Method = this.Method, - RequestUri = new($"{Endpoints.BASE_URI}/{this.Url}") - }; - - if (this.Headers is not null) - { - foreach (KeyValuePair header in this.Headers) - { - request.Headers.Add(header.Key, Uri.EscapeDataString(header.Value)); - } - } - - request.Headers.Add("Connection", "keep-alive"); - request.Headers.Add("Keep-Alive", "600"); - - string boundary = "---------------------------" + DateTimeOffset.UtcNow.Ticks.ToString("x"); - - MultipartFormDataContent content = new(boundary); - - if (this.Values is not null) - { - foreach (KeyValuePair element in this.Values) - { - content.Add(new StringContent(element.Value), element.Key); - } - } - - if (this.Files is not null) - { - for (int i = 0; i < this.Files.Count; i++) - { - DiscordMessageFile current = this.Files[i]; - - ArrayPoolBufferWriter writer; - - try - { - writer = new ArrayPoolBufferWriter(checked((int)current.Stream.Length)); - } - catch (NotSupportedException) - { - writer = new ArrayPoolBufferWriter(4096); - } - - int writtenBytes; - while ((writtenBytes = current.Stream.Read(writer.GetSpan())) > 0) - { - writer.Advance(writtenBytes); - } - - ByteArrayContent file = new(writer.WrittenSpan.ToArray()); - - writer.Dispose(); - - if (current.ContentType is not null) - { - file.Headers.ContentType = MediaTypeHeaderValue.Parse(current.ContentType); - } - - string filename = current.FileType is null - ? current.FileName - : $"{current.FileName}.{current.FileType}"; - - // do we actually need this distinction? it's been made since the beginning of time, - // but it doesn't seem very necessary - if (this.Files.Count > 1) - { - content.Add(file, $"file{i + 1}", filename); - } - else - { - content.Add(file, "file", filename); - } - } - } - - request.Content = content; - - return request; - } -} +// This file is part of the DSharpPlus project. +// +// Copyright (c) 2015 Mike Santiago +// Copyright (c) 2016-2025 DSharpPlus Contributors +// +// 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.Net.Http; +using System.Net.Http.Headers; +using CommunityToolkit.HighPerformance.Buffers; +using DSharpPlus.Entities; + +namespace DSharpPlus.Net; + +/// +/// Represents a multipart HTTP request. +/// +internal readonly record struct MultipartRestRequest : IRestRequest +{ + /// + public string Url { get; init; } + + /// + /// The method for this request. + /// + public HttpMethod Method { get; init; } + + /// + public string Route { get; init; } + + /// + public bool IsExemptFromGlobalLimit { get; init; } + + /// + /// The headers for this request. + /// + public IReadOnlyDictionary? Headers { get; init; } + + /// + /// Gets the dictionary of values attached to this request. + /// + public IReadOnlyDictionary Values { get; init; } + + /// + /// Gets the dictionary of files attached to this request. + /// + public IReadOnlyList Files { get; init; } + + /// + public bool IsExemptFromAllLimits { get; init; } + + public HttpRequestMessage Build() + { + HttpRequestMessage request = new() + { + Method = this.Method, + RequestUri = new($"{Endpoints.BASE_URI}/{this.Url}") + }; + + if (this.Headers is not null) + { + foreach (KeyValuePair header in this.Headers) + { + request.Headers.Add(header.Key, Uri.EscapeDataString(header.Value)); + } + } + + request.Headers.Add("Connection", "keep-alive"); + request.Headers.Add("Keep-Alive", "600"); + + string boundary = "---------------------------" + DateTimeOffset.UtcNow.Ticks.ToString("x"); + + MultipartFormDataContent content = new(boundary); + + if (this.Values is not null) + { + foreach (KeyValuePair element in this.Values) + { + content.Add(new StringContent(element.Value), element.Key); + } + } + + if (this.Files is not null) + { + for (int i = 0; i < this.Files.Count; i++) + { + DiscordMessageFile current = this.Files[i]; + + ArrayPoolBufferWriter writer; + + try + { + writer = new ArrayPoolBufferWriter(checked((int)current.Stream.Length)); + } + catch (NotSupportedException) + { + writer = new ArrayPoolBufferWriter(4096); + } + + int writtenBytes; + while ((writtenBytes = current.Stream.Read(writer.GetSpan())) > 0) + { + writer.Advance(writtenBytes); + } + + ByteArrayContent file = new(writer.WrittenSpan.ToArray()); + + writer.Dispose(); + + if (current.ContentType is not null) + { + file.Headers.ContentType = MediaTypeHeaderValue.Parse(current.ContentType); + } + + string filename = current.FileType is null + ? current.FileName + : $"{current.FileName}.{current.FileType}"; + + // do we actually need this distinction? it's been made since the beginning of time, + // but it doesn't seem very necessary + if (this.Files.Count > 1) + { + content.Add(file, $"file{i + 1}", filename); + } + else + { + content.Add(file, "file", filename); + } + } + } + + request.Content = content; + + return request; + } +} diff --git a/DSharpPlus/Net/Rest/PreemptiveRatelimitException.cs b/DSharpPlus/Net/Rest/PreemptiveRatelimitException.cs index 7f347f440e..7c3c0027b5 100644 --- a/DSharpPlus/Net/Rest/PreemptiveRatelimitException.cs +++ b/DSharpPlus/Net/Rest/PreemptiveRatelimitException.cs @@ -1,18 +1,18 @@ -using System; -using System.Diagnostics.CodeAnalysis; - -namespace DSharpPlus.Net; - -internal class PreemptiveRatelimitException : Exception -{ - public required string Scope { get; set; } - - public required TimeSpan ResetAfter { get; set; } - - [SetsRequiredMembers] - public PreemptiveRatelimitException(string scope, TimeSpan resetAfter) - { - this.Scope = scope; - this.ResetAfter = resetAfter; - } -} +using System; +using System.Diagnostics.CodeAnalysis; + +namespace DSharpPlus.Net; + +internal class PreemptiveRatelimitException : Exception +{ + public required string Scope { get; set; } + + public required TimeSpan ResetAfter { get; set; } + + [SetsRequiredMembers] + public PreemptiveRatelimitException(string scope, TimeSpan resetAfter) + { + this.Scope = scope; + this.ResetAfter = resetAfter; + } +} diff --git a/DSharpPlus/Net/Rest/RateLimitBucket.cs b/DSharpPlus/Net/Rest/RateLimitBucket.cs index b042ccde75..211984e311 100644 --- a/DSharpPlus/Net/Rest/RateLimitBucket.cs +++ b/DSharpPlus/Net/Rest/RateLimitBucket.cs @@ -1,186 +1,186 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Net.Http.Headers; -using System.Threading; - -namespace DSharpPlus.Net; - -/// -/// Represents a rate limit bucket. -/// -internal sealed class RateLimitBucket -{ - /// - /// Gets the number of uses left before pre-emptive rate limit is triggered. - /// - public int Remaining - => this.remaining; - - /// - /// Gets the timestamp at which the rate limit resets. - /// - public DateTime Reset { get; internal set; } - - /// - /// Gets the maximum number of uses within a single bucket. - /// - public int Maximum => this.maximum; - - internal int maximum; - internal int remaining; - internal int reserved = 0; - - public RateLimitBucket - ( - int maximum, - int remaining, - DateTime reset - ) - { - this.maximum = maximum; - this.remaining = remaining; - this.Reset = reset; - } - - public RateLimitBucket() - { - this.maximum = 1; - this.remaining = 1; - this.Reset = DateTime.UtcNow + TimeSpan.FromSeconds(10); - this.reserved = 0; - } - - /// - /// Resets the bucket to the next reset time. - /// - internal void ResetLimit(DateTime nextReset) - { - if (nextReset < this.Reset) - { - throw new ArgumentOutOfRangeException - ( - nameof(nextReset), - "The next ratelimit expiration must follow the present expiration." - ); - } - - Interlocked.Exchange(ref this.remaining, this.Maximum); - this.Reset = nextReset; - } - - public static bool TryExtractRateLimitBucket - ( - HttpResponseHeaders headers, - - out RateLimitCandidateBucket bucket - ) - { - bucket = default; - - try - { - if - ( - !headers.TryGetValues("X-RateLimit-Limit", out IEnumerable? limitRaw) - || !headers.TryGetValues("X-RateLimit-Remaining", out IEnumerable? remainingRaw) - || !headers.TryGetValues("X-RateLimit-Reset-After", out IEnumerable? ratelimitResetRaw) - ) - { - return false; - } - - if - ( - !int.TryParse(limitRaw.SingleOrDefault(), CultureInfo.InvariantCulture, out int limit) - || !int.TryParse(remainingRaw.SingleOrDefault(), CultureInfo.InvariantCulture, out int remaining) - || !double.TryParse(ratelimitResetRaw.SingleOrDefault(), CultureInfo.InvariantCulture, out double ratelimitReset) - ) - { - return false; - } - - DateTime reset = (DateTimeOffset.UtcNow + TimeSpan.FromSeconds(ratelimitReset)).UtcDateTime; - - bucket = new(limit, remaining, reset); - return true; - } - catch - { - return false; - } - } - - internal bool CheckNextRequest() - { - if (this.Reset < DateTime.UtcNow) - { - ResetLimit(DateTime.UtcNow + TimeSpan.FromSeconds(1)); - Interlocked.Increment(ref this.reserved); - return true; - } - - if (this.Remaining - this.reserved <= 0) - { - return false; - } - - Interlocked.Increment(ref this.reserved); - return true; - } - - internal void UpdateBucket(int maximum, int remaining, DateTime reset) - { - if (reset == this.Reset && this.remaining <= remaining) - { - // we're out of sync, just decrement the reservation - we trust the most pessimistic data. - if (this.reserved > 0) - { - Interlocked.Decrement(ref this.reserved); - } - - return; - } - - Interlocked.Exchange(ref this.maximum, maximum); - Interlocked.Exchange(ref this.remaining, remaining); - - if (this.reserved > 0) - { - Interlocked.Decrement(ref this.reserved); - } - - this.Reset = reset; - } - - internal void CancelReservation() - { - if (this.reserved > 0) - { - Interlocked.Decrement(ref this.reserved); - } - } - - internal void CompleteReservation() - { - if (this.Reset < DateTime.UtcNow) - { - ResetLimit(DateTime.UtcNow + TimeSpan.FromSeconds(1)); - - if (this.reserved > 0) - { - Interlocked.Decrement(ref this.reserved); - } - - return; - } - - Interlocked.Decrement(ref this.remaining); - - if (this.reserved > 0) - { - Interlocked.Decrement(ref this.reserved); - } - } -} +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Net.Http.Headers; +using System.Threading; + +namespace DSharpPlus.Net; + +/// +/// Represents a rate limit bucket. +/// +internal sealed class RateLimitBucket +{ + /// + /// Gets the number of uses left before pre-emptive rate limit is triggered. + /// + public int Remaining + => this.remaining; + + /// + /// Gets the timestamp at which the rate limit resets. + /// + public DateTime Reset { get; internal set; } + + /// + /// Gets the maximum number of uses within a single bucket. + /// + public int Maximum => this.maximum; + + internal int maximum; + internal int remaining; + internal int reserved = 0; + + public RateLimitBucket + ( + int maximum, + int remaining, + DateTime reset + ) + { + this.maximum = maximum; + this.remaining = remaining; + this.Reset = reset; + } + + public RateLimitBucket() + { + this.maximum = 1; + this.remaining = 1; + this.Reset = DateTime.UtcNow + TimeSpan.FromSeconds(10); + this.reserved = 0; + } + + /// + /// Resets the bucket to the next reset time. + /// + internal void ResetLimit(DateTime nextReset) + { + if (nextReset < this.Reset) + { + throw new ArgumentOutOfRangeException + ( + nameof(nextReset), + "The next ratelimit expiration must follow the present expiration." + ); + } + + Interlocked.Exchange(ref this.remaining, this.Maximum); + this.Reset = nextReset; + } + + public static bool TryExtractRateLimitBucket + ( + HttpResponseHeaders headers, + + out RateLimitCandidateBucket bucket + ) + { + bucket = default; + + try + { + if + ( + !headers.TryGetValues("X-RateLimit-Limit", out IEnumerable? limitRaw) + || !headers.TryGetValues("X-RateLimit-Remaining", out IEnumerable? remainingRaw) + || !headers.TryGetValues("X-RateLimit-Reset-After", out IEnumerable? ratelimitResetRaw) + ) + { + return false; + } + + if + ( + !int.TryParse(limitRaw.SingleOrDefault(), CultureInfo.InvariantCulture, out int limit) + || !int.TryParse(remainingRaw.SingleOrDefault(), CultureInfo.InvariantCulture, out int remaining) + || !double.TryParse(ratelimitResetRaw.SingleOrDefault(), CultureInfo.InvariantCulture, out double ratelimitReset) + ) + { + return false; + } + + DateTime reset = (DateTimeOffset.UtcNow + TimeSpan.FromSeconds(ratelimitReset)).UtcDateTime; + + bucket = new(limit, remaining, reset); + return true; + } + catch + { + return false; + } + } + + internal bool CheckNextRequest() + { + if (this.Reset < DateTime.UtcNow) + { + ResetLimit(DateTime.UtcNow + TimeSpan.FromSeconds(1)); + Interlocked.Increment(ref this.reserved); + return true; + } + + if (this.Remaining - this.reserved <= 0) + { + return false; + } + + Interlocked.Increment(ref this.reserved); + return true; + } + + internal void UpdateBucket(int maximum, int remaining, DateTime reset) + { + if (reset == this.Reset && this.remaining <= remaining) + { + // we're out of sync, just decrement the reservation - we trust the most pessimistic data. + if (this.reserved > 0) + { + Interlocked.Decrement(ref this.reserved); + } + + return; + } + + Interlocked.Exchange(ref this.maximum, maximum); + Interlocked.Exchange(ref this.remaining, remaining); + + if (this.reserved > 0) + { + Interlocked.Decrement(ref this.reserved); + } + + this.Reset = reset; + } + + internal void CancelReservation() + { + if (this.reserved > 0) + { + Interlocked.Decrement(ref this.reserved); + } + } + + internal void CompleteReservation() + { + if (this.Reset < DateTime.UtcNow) + { + ResetLimit(DateTime.UtcNow + TimeSpan.FromSeconds(1)); + + if (this.reserved > 0) + { + Interlocked.Decrement(ref this.reserved); + } + + return; + } + + Interlocked.Decrement(ref this.remaining); + + if (this.reserved > 0) + { + Interlocked.Decrement(ref this.reserved); + } + } +} diff --git a/DSharpPlus/Net/Rest/RateLimitCandidateBucket.cs b/DSharpPlus/Net/Rest/RateLimitCandidateBucket.cs index 6b49dd55df..9ebc7da043 100644 --- a/DSharpPlus/Net/Rest/RateLimitCandidateBucket.cs +++ b/DSharpPlus/Net/Rest/RateLimitCandidateBucket.cs @@ -1,12 +1,12 @@ -using System; - -namespace DSharpPlus.Net; - -/// -/// A value-type variant of for extraction, in case we don't need the object. -/// -internal readonly record struct RateLimitCandidateBucket(int Maximum, int Remaining, DateTime Reset) -{ - public RateLimitBucket ToFullBucket() - => new(this.Maximum, this.Remaining, this.Reset); -} +using System; + +namespace DSharpPlus.Net; + +/// +/// A value-type variant of for extraction, in case we don't need the object. +/// +internal readonly record struct RateLimitCandidateBucket(int Maximum, int Remaining, DateTime Reset) +{ + public RateLimitBucket ToFullBucket() + => new(this.Maximum, this.Remaining, this.Reset); +} diff --git a/DSharpPlus/Net/Rest/RateLimitOptions.cs b/DSharpPlus/Net/Rest/RateLimitOptions.cs index c9c34d5125..d64c7061f6 100644 --- a/DSharpPlus/Net/Rest/RateLimitOptions.cs +++ b/DSharpPlus/Net/Rest/RateLimitOptions.cs @@ -1,5 +1,5 @@ -using Polly; - -namespace DSharpPlus.Net; - -internal class RateLimitOptions : ResilienceStrategyOptions; +using Polly; + +namespace DSharpPlus.Net; + +internal class RateLimitOptions : ResilienceStrategyOptions; diff --git a/DSharpPlus/Net/Rest/RateLimitStrategy.cs b/DSharpPlus/Net/Rest/RateLimitStrategy.cs index 424b611f8a..a68ef962e9 100644 --- a/DSharpPlus/Net/Rest/RateLimitStrategy.cs +++ b/DSharpPlus/Net/Rest/RateLimitStrategy.cs @@ -1,332 +1,332 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; - -using Microsoft.Extensions.Logging; - -using Polly; - -namespace DSharpPlus.Net; - -internal class RateLimitStrategy : ResilienceStrategy, IDisposable -{ - private readonly RateLimitBucket globalBucket; - private readonly ConcurrentDictionary buckets = []; - private readonly ConcurrentDictionary routeHashes = []; - - private readonly ILogger logger; - private readonly int waitingForHashMilliseconds; - - private readonly Lock bucketCheckingLock = new(); - - private bool cancel = false; - - public RateLimitStrategy(ILogger logger, int waitingForHashMilliseconds = 200, int maximumRestRequestsPerSecond = 15) - { - this.logger = logger; - this.waitingForHashMilliseconds = waitingForHashMilliseconds; - this.globalBucket = new(maximumRestRequestsPerSecond, maximumRestRequestsPerSecond, DateTime.UtcNow.AddSeconds(1)); - _ = CleanAsync(); - } - - protected override async ValueTask> ExecuteCore - ( - Func>> action, - ResilienceContext context, - TState state - ) - { - // fail-fast if we dont have a route to ratelimit to -#pragma warning disable CS8600 - if (!context.Properties.TryGetValue(new("route"), out string route)) - { - return Outcome.FromException( - new InvalidOperationException("No route passed. This should be reported to library developers.")); - } -#pragma warning restore CS8600 - - // get trace id for logging - Ulid traceId = context.Properties.TryGetValue(new("trace-id"), out Ulid tid) ? tid : Ulid.Empty; - - // if we're exempt, execute immediately - if (context.Properties.TryGetValue(new("exempt-from-all-limits"), out bool allExempt) && allExempt) - { - this.logger.LogTrace - ( - LoggerEvents.RatelimitDiag, - "Request ID:{TraceId}: Executing request exempt from all ratelimits to {Route}", - traceId, - route - ); - - return await action(context, state); - } - - // get global limit - bool exemptFromGlobalLimit = false; - - if (context.Properties.TryGetValue(new("exempt-from-global-limit"), out bool exempt)) - { - exemptFromGlobalLimit = exempt; - } - - // check against ratelimits now - DateTime instant = DateTime.UtcNow; - - lock (this.bucketCheckingLock) - { - if (!exemptFromGlobalLimit && !this.globalBucket.CheckNextRequest()) - { - return SynthesizeInternalResponse(route, this.globalBucket.Reset, "global", traceId); - } - } - - if (!this.routeHashes.TryGetValue(route, out string? hash)) - { - if (!this.routeHashes.TryAdd(route, "pending")) - { - // two different async requests entered this at the same time, requeue this one - return SynthesizeInternalResponse - ( - route, - instant + TimeSpan.FromMilliseconds(this.waitingForHashMilliseconds), - "route", - traceId - ); - } - - this.logger.LogTrace - ( - LoggerEvents.RatelimitDiag, - "Request ID:{TraceId}: Route has no known hash: {Route}.", - traceId, - route - ); - - Outcome outcome = await action(context, state); - - if (!exemptFromGlobalLimit) - { - this.globalBucket.CompleteReservation(); - } - - if (outcome.Result is null) - { - this.routeHashes.Remove(route, out _); - return outcome; - } - - UpdateRateLimitBuckets(outcome.Result, "pending", route, traceId); - - // something went awry, just reset and try again next time. this may be because the endpoint didn't return valid headers, - // which is the case for some endpoints, and we don't need to get hung up on this - if (this.routeHashes[route] == "pending") - { - this.routeHashes.Remove(route, out _); - } - - return outcome; - } - else if (hash == "pending") - { - if (!exemptFromGlobalLimit) - { - this.globalBucket.CancelReservation(); - } - - return SynthesizeInternalResponse - ( - route, - instant + TimeSpan.FromMilliseconds(this.waitingForHashMilliseconds), - "route", - traceId - ); - } - else - { - RateLimitBucket bucket = this.buckets.GetOrAdd(hash, _ => new()); - - this.logger.LogTrace - ( - LoggerEvents.RatelimitDiag, - "Request ID:{TraceId}: Checking bucket, current state is [Remaining: {Remaining}, Reserved: {Reserved}]", - traceId, - bucket.remaining, - bucket.reserved - ); - - lock (this.bucketCheckingLock) - { - if (!bucket.CheckNextRequest()) - { - if (!exemptFromGlobalLimit) - { - this.globalBucket.CancelReservation(); - } - - return SynthesizeInternalResponse(route, bucket.Reset, "bucket", traceId); - } - } - - this.logger.LogTrace - ( - LoggerEvents.RatelimitDiag, - "Request ID:{TraceId}: Allowed request, current state is [Remaining: {Remaining}, Reserved: {Reserved}]", - traceId, - bucket.remaining, - bucket.reserved - ); - - Outcome outcome; - - try - { - // make the actual request - outcome = await action(context, state); - - if (outcome.Result is null) - { - if (!exemptFromGlobalLimit) - { - this.globalBucket.CancelReservation(); - } - - return outcome; - } - - if (!exemptFromGlobalLimit) - { - this.globalBucket.CompleteReservation(); - } - } - catch (Exception e) - { - if (!exemptFromGlobalLimit) - { - this.globalBucket.CancelReservation(); - } - - bucket.CancelReservation(); - return Outcome.FromException(e); - } - - if (!exemptFromGlobalLimit) - { - UpdateRateLimitBuckets(outcome.Result, hash, route, traceId); - } - - if (outcome.Result?.StatusCode == HttpStatusCode.TooManyRequests) - { - string resetAfterRaw = outcome.Result.Headers.GetValues("X-RateLimit-Reset-After").Single(); - TimeSpan resetAfter = TimeSpan.FromSeconds(double.Parse(resetAfterRaw)); - - string traceIdString = ""; - if (this.logger.IsEnabled(LogLevel.Trace)) - { - traceIdString = $"Request ID:{traceId}: "; - } - - this.logger.LogWarning - ( - "{TraceId}Hit Discord ratelimit on route {Route}, waiting for {ResetAfter}", - traceIdString, - route, - resetAfter - ); - - return Outcome.FromException(new RetryableRatelimitException(resetAfter)); - } - - return outcome; - } - } - - private Outcome SynthesizeInternalResponse(string route, DateTime retry, string scope, Ulid traceId) - { - string waitingForRoute = scope == "route" ? " for route hash" : ""; - string global = scope == "global" ? " global" : ""; - - string traceIdString = ""; - if (this.logger.IsEnabled(LogLevel.Trace)) - { - traceIdString = $"Request ID:{traceId}: "; - } - - DateTime retryJittered = retry + TimeSpan.FromMilliseconds(Random.Shared.NextInt64(100)); - - this.logger.LogDebug - ( - LoggerEvents.RatelimitPreemptive, - "{TraceId}Pre-emptive{Global} ratelimit for {Route} triggered - waiting{WaitingForRoute} until {Reset:O}.", - traceIdString, - global, - route, - waitingForRoute, - retryJittered - ); - - return Outcome.FromException( - new PreemptiveRatelimitException(scope, retryJittered - DateTime.UtcNow)); - } - - private void UpdateRateLimitBuckets(HttpResponseMessage response, string oldHash, string route, Ulid id) - { - if (response.Headers.TryGetValues("X-RateLimit-Bucket", out IEnumerable? hashHeader)) - { - string newHash = hashHeader?.Single()!; - - if (!RateLimitBucket.TryExtractRateLimitBucket(response.Headers, out RateLimitCandidateBucket extracted)) - { - return; - } - else if (oldHash != newHash) - { - this.logger.LogTrace("Request ID:{ID} - Initial bucket capacity: {max}", id, extracted.Maximum); - this.buckets.AddOrUpdate(newHash, _ => extracted.ToFullBucket(), (_, _) => extracted.ToFullBucket()); - } - else - { - if (this.buckets.TryGetValue(newHash, out RateLimitBucket? oldBucket)) - { - oldBucket.UpdateBucket(extracted.Maximum, extracted.Remaining, extracted.Reset); - } - else - { - this.logger.LogTrace("Request ID:{ID} - Initial bucket capacity: {max}", id, extracted.Maximum); - this.buckets.AddOrUpdate(newHash, _ => extracted.ToFullBucket(), - (_, _) => extracted.ToFullBucket()); - } - } - - this.routeHashes.AddOrUpdate(route, newHash!, (_, _) => newHash!); - } - } - - private async Task CleanAsync() - { - PeriodicTimer timer = new(TimeSpan.FromSeconds(10)); - while (await timer.WaitForNextTickAsync()) - { - foreach (KeyValuePair pair in this.routeHashes) - { - if (this.buckets.TryGetValue(pair.Value, out RateLimitBucket? bucket) && bucket.Reset < DateTime.UtcNow + TimeSpan.FromSeconds(1)) - { - this.buckets.Remove(pair.Value, out _); - this.routeHashes.Remove(pair.Key, out _); - } - } - - if (this.cancel) - { - return; - } - } - } - - public void Dispose() => this.cancel = true; -} +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.Extensions.Logging; + +using Polly; + +namespace DSharpPlus.Net; + +internal class RateLimitStrategy : ResilienceStrategy, IDisposable +{ + private readonly RateLimitBucket globalBucket; + private readonly ConcurrentDictionary buckets = []; + private readonly ConcurrentDictionary routeHashes = []; + + private readonly ILogger logger; + private readonly int waitingForHashMilliseconds; + + private readonly Lock bucketCheckingLock = new(); + + private bool cancel = false; + + public RateLimitStrategy(ILogger logger, int waitingForHashMilliseconds = 200, int maximumRestRequestsPerSecond = 15) + { + this.logger = logger; + this.waitingForHashMilliseconds = waitingForHashMilliseconds; + this.globalBucket = new(maximumRestRequestsPerSecond, maximumRestRequestsPerSecond, DateTime.UtcNow.AddSeconds(1)); + _ = CleanAsync(); + } + + protected override async ValueTask> ExecuteCore + ( + Func>> action, + ResilienceContext context, + TState state + ) + { + // fail-fast if we dont have a route to ratelimit to +#pragma warning disable CS8600 + if (!context.Properties.TryGetValue(new("route"), out string route)) + { + return Outcome.FromException( + new InvalidOperationException("No route passed. This should be reported to library developers.")); + } +#pragma warning restore CS8600 + + // get trace id for logging + Ulid traceId = context.Properties.TryGetValue(new("trace-id"), out Ulid tid) ? tid : Ulid.Empty; + + // if we're exempt, execute immediately + if (context.Properties.TryGetValue(new("exempt-from-all-limits"), out bool allExempt) && allExempt) + { + this.logger.LogTrace + ( + LoggerEvents.RatelimitDiag, + "Request ID:{TraceId}: Executing request exempt from all ratelimits to {Route}", + traceId, + route + ); + + return await action(context, state); + } + + // get global limit + bool exemptFromGlobalLimit = false; + + if (context.Properties.TryGetValue(new("exempt-from-global-limit"), out bool exempt)) + { + exemptFromGlobalLimit = exempt; + } + + // check against ratelimits now + DateTime instant = DateTime.UtcNow; + + lock (this.bucketCheckingLock) + { + if (!exemptFromGlobalLimit && !this.globalBucket.CheckNextRequest()) + { + return SynthesizeInternalResponse(route, this.globalBucket.Reset, "global", traceId); + } + } + + if (!this.routeHashes.TryGetValue(route, out string? hash)) + { + if (!this.routeHashes.TryAdd(route, "pending")) + { + // two different async requests entered this at the same time, requeue this one + return SynthesizeInternalResponse + ( + route, + instant + TimeSpan.FromMilliseconds(this.waitingForHashMilliseconds), + "route", + traceId + ); + } + + this.logger.LogTrace + ( + LoggerEvents.RatelimitDiag, + "Request ID:{TraceId}: Route has no known hash: {Route}.", + traceId, + route + ); + + Outcome outcome = await action(context, state); + + if (!exemptFromGlobalLimit) + { + this.globalBucket.CompleteReservation(); + } + + if (outcome.Result is null) + { + this.routeHashes.Remove(route, out _); + return outcome; + } + + UpdateRateLimitBuckets(outcome.Result, "pending", route, traceId); + + // something went awry, just reset and try again next time. this may be because the endpoint didn't return valid headers, + // which is the case for some endpoints, and we don't need to get hung up on this + if (this.routeHashes[route] == "pending") + { + this.routeHashes.Remove(route, out _); + } + + return outcome; + } + else if (hash == "pending") + { + if (!exemptFromGlobalLimit) + { + this.globalBucket.CancelReservation(); + } + + return SynthesizeInternalResponse + ( + route, + instant + TimeSpan.FromMilliseconds(this.waitingForHashMilliseconds), + "route", + traceId + ); + } + else + { + RateLimitBucket bucket = this.buckets.GetOrAdd(hash, _ => new()); + + this.logger.LogTrace + ( + LoggerEvents.RatelimitDiag, + "Request ID:{TraceId}: Checking bucket, current state is [Remaining: {Remaining}, Reserved: {Reserved}]", + traceId, + bucket.remaining, + bucket.reserved + ); + + lock (this.bucketCheckingLock) + { + if (!bucket.CheckNextRequest()) + { + if (!exemptFromGlobalLimit) + { + this.globalBucket.CancelReservation(); + } + + return SynthesizeInternalResponse(route, bucket.Reset, "bucket", traceId); + } + } + + this.logger.LogTrace + ( + LoggerEvents.RatelimitDiag, + "Request ID:{TraceId}: Allowed request, current state is [Remaining: {Remaining}, Reserved: {Reserved}]", + traceId, + bucket.remaining, + bucket.reserved + ); + + Outcome outcome; + + try + { + // make the actual request + outcome = await action(context, state); + + if (outcome.Result is null) + { + if (!exemptFromGlobalLimit) + { + this.globalBucket.CancelReservation(); + } + + return outcome; + } + + if (!exemptFromGlobalLimit) + { + this.globalBucket.CompleteReservation(); + } + } + catch (Exception e) + { + if (!exemptFromGlobalLimit) + { + this.globalBucket.CancelReservation(); + } + + bucket.CancelReservation(); + return Outcome.FromException(e); + } + + if (!exemptFromGlobalLimit) + { + UpdateRateLimitBuckets(outcome.Result, hash, route, traceId); + } + + if (outcome.Result?.StatusCode == HttpStatusCode.TooManyRequests) + { + string resetAfterRaw = outcome.Result.Headers.GetValues("X-RateLimit-Reset-After").Single(); + TimeSpan resetAfter = TimeSpan.FromSeconds(double.Parse(resetAfterRaw)); + + string traceIdString = ""; + if (this.logger.IsEnabled(LogLevel.Trace)) + { + traceIdString = $"Request ID:{traceId}: "; + } + + this.logger.LogWarning + ( + "{TraceId}Hit Discord ratelimit on route {Route}, waiting for {ResetAfter}", + traceIdString, + route, + resetAfter + ); + + return Outcome.FromException(new RetryableRatelimitException(resetAfter)); + } + + return outcome; + } + } + + private Outcome SynthesizeInternalResponse(string route, DateTime retry, string scope, Ulid traceId) + { + string waitingForRoute = scope == "route" ? " for route hash" : ""; + string global = scope == "global" ? " global" : ""; + + string traceIdString = ""; + if (this.logger.IsEnabled(LogLevel.Trace)) + { + traceIdString = $"Request ID:{traceId}: "; + } + + DateTime retryJittered = retry + TimeSpan.FromMilliseconds(Random.Shared.NextInt64(100)); + + this.logger.LogDebug + ( + LoggerEvents.RatelimitPreemptive, + "{TraceId}Pre-emptive{Global} ratelimit for {Route} triggered - waiting{WaitingForRoute} until {Reset:O}.", + traceIdString, + global, + route, + waitingForRoute, + retryJittered + ); + + return Outcome.FromException( + new PreemptiveRatelimitException(scope, retryJittered - DateTime.UtcNow)); + } + + private void UpdateRateLimitBuckets(HttpResponseMessage response, string oldHash, string route, Ulid id) + { + if (response.Headers.TryGetValues("X-RateLimit-Bucket", out IEnumerable? hashHeader)) + { + string newHash = hashHeader?.Single()!; + + if (!RateLimitBucket.TryExtractRateLimitBucket(response.Headers, out RateLimitCandidateBucket extracted)) + { + return; + } + else if (oldHash != newHash) + { + this.logger.LogTrace("Request ID:{ID} - Initial bucket capacity: {max}", id, extracted.Maximum); + this.buckets.AddOrUpdate(newHash, _ => extracted.ToFullBucket(), (_, _) => extracted.ToFullBucket()); + } + else + { + if (this.buckets.TryGetValue(newHash, out RateLimitBucket? oldBucket)) + { + oldBucket.UpdateBucket(extracted.Maximum, extracted.Remaining, extracted.Reset); + } + else + { + this.logger.LogTrace("Request ID:{ID} - Initial bucket capacity: {max}", id, extracted.Maximum); + this.buckets.AddOrUpdate(newHash, _ => extracted.ToFullBucket(), + (_, _) => extracted.ToFullBucket()); + } + } + + this.routeHashes.AddOrUpdate(route, newHash!, (_, _) => newHash!); + } + } + + private async Task CleanAsync() + { + PeriodicTimer timer = new(TimeSpan.FromSeconds(10)); + while (await timer.WaitForNextTickAsync()) + { + foreach (KeyValuePair pair in this.routeHashes) + { + if (this.buckets.TryGetValue(pair.Value, out RateLimitBucket? bucket) && bucket.Reset < DateTime.UtcNow + TimeSpan.FromSeconds(1)) + { + this.buckets.Remove(pair.Value, out _); + this.routeHashes.Remove(pair.Key, out _); + } + } + + if (this.cancel) + { + return; + } + } + } + + public void Dispose() => this.cancel = true; +} diff --git a/DSharpPlus/Net/Rest/RequestStreamWrapper.cs b/DSharpPlus/Net/Rest/RequestStreamWrapper.cs index 3c71a47a75..b9212bd223 100644 --- a/DSharpPlus/Net/Rest/RequestStreamWrapper.cs +++ b/DSharpPlus/Net/Rest/RequestStreamWrapper.cs @@ -1,77 +1,77 @@ -using System; -using System.IO; - -namespace DSharpPlus.Net; - -// this class is a clusterfuck to prevent the RestClient from disposing streams we dont want to dispose -// only god, aaron and i know what a psychosis it was to fix this issue (#1677) -public class RequestStreamWrapper : Stream, IDisposable -{ - public Stream UnderlyingStream { get; init; } - - private void CheckDisposed() => ObjectDisposedException.ThrowIf(this.UnderlyingStream is null, this); - - //basically these two methods are the whole purpose of this class - protected override void Dispose(bool disposing) { /* NOT TODAY MY FRIEND */ } - protected new void Dispose() => Dispose(true); - void IDisposable.Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - public RequestStreamWrapper(Stream stream) - { - ArgumentNullException.ThrowIfNull(stream); - this.UnderlyingStream = stream; - } - - /// - public override bool CanRead => this.UnderlyingStream.CanRead; - - /// - public override bool CanSeek => this.UnderlyingStream.CanSeek; - - /// - public override bool CanWrite => this.UnderlyingStream.CanWrite; - - /// - public override void Flush() => this.UnderlyingStream.Flush(); - - /// - public override long Length - { - get - { - CheckDisposed(); - return this.UnderlyingStream.Length; - } - } - - /// - public override long Position - { - get => this.UnderlyingStream.Position; - set => this.UnderlyingStream.Position = value; - } - - /// - public override int Read(byte[] buffer, int offset, int count) - { - CheckDisposed(); - return this.UnderlyingStream.Read(buffer, offset, count); - } - - /// - public override long Seek(long offset, SeekOrigin origin) - { - CheckDisposed(); - return this.UnderlyingStream.Seek(offset, origin); - } - - /// - public override void SetLength(long value) => this.UnderlyingStream.SetLength(value); - - /// - public override void Write(byte[] buffer, int offset, int count) => this.UnderlyingStream.Write(buffer, offset, count); -} +using System; +using System.IO; + +namespace DSharpPlus.Net; + +// this class is a clusterfuck to prevent the RestClient from disposing streams we dont want to dispose +// only god, aaron and i know what a psychosis it was to fix this issue (#1677) +public class RequestStreamWrapper : Stream, IDisposable +{ + public Stream UnderlyingStream { get; init; } + + private void CheckDisposed() => ObjectDisposedException.ThrowIf(this.UnderlyingStream is null, this); + + //basically these two methods are the whole purpose of this class + protected override void Dispose(bool disposing) { /* NOT TODAY MY FRIEND */ } + protected new void Dispose() => Dispose(true); + void IDisposable.Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + public RequestStreamWrapper(Stream stream) + { + ArgumentNullException.ThrowIfNull(stream); + this.UnderlyingStream = stream; + } + + /// + public override bool CanRead => this.UnderlyingStream.CanRead; + + /// + public override bool CanSeek => this.UnderlyingStream.CanSeek; + + /// + public override bool CanWrite => this.UnderlyingStream.CanWrite; + + /// + public override void Flush() => this.UnderlyingStream.Flush(); + + /// + public override long Length + { + get + { + CheckDisposed(); + return this.UnderlyingStream.Length; + } + } + + /// + public override long Position + { + get => this.UnderlyingStream.Position; + set => this.UnderlyingStream.Position = value; + } + + /// + public override int Read(byte[] buffer, int offset, int count) + { + CheckDisposed(); + return this.UnderlyingStream.Read(buffer, offset, count); + } + + /// + public override long Seek(long offset, SeekOrigin origin) + { + CheckDisposed(); + return this.UnderlyingStream.Seek(offset, origin); + } + + /// + public override void SetLength(long value) => this.UnderlyingStream.SetLength(value); + + /// + public override void Write(byte[] buffer, int offset, int count) => this.UnderlyingStream.Write(buffer, offset, count); +} diff --git a/DSharpPlus/Net/Rest/RestClient.cs b/DSharpPlus/Net/Rest/RestClient.cs index 5704d18d2c..f6241f19ec 100644 --- a/DSharpPlus/Net/Rest/RestClient.cs +++ b/DSharpPlus/Net/Rest/RestClient.cs @@ -1,283 +1,283 @@ -using System; -using System.Net; -using System.Net.Http; -using System.Text.RegularExpressions; -using System.Threading; -using System.Threading.Tasks; -using DSharpPlus.Exceptions; -using DSharpPlus.Logging; -using DSharpPlus.Metrics; - -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -using Polly; - -namespace DSharpPlus.Net; - -/// -/// Represents a client used to make REST requests. -/// -public sealed partial class RestClient : IDisposable -{ - [GeneratedRegex(":([a-z_]+)")] - private static partial Regex GenerateRouteArgumentRegex(); - - private static readonly Regex routeArgumentRegex = GenerateRouteArgumentRegex(); - private readonly HttpClient httpClient; - private readonly ILogger logger; - private readonly AsyncManualResetEvent globalRateLimitEvent; - private readonly ResiliencePipeline pipeline; - private readonly RateLimitStrategy rateLimitStrategy; - private readonly RequestMetricsContainer metrics = new(); - - private volatile bool disposed; - - public RestClient - ( - ILogger logger, - HttpClient client, - IOptions options, - IOptions tokenContainer - ) - : this - ( - client, - options.Value.Timeout, - logger, - options.Value.MaximumRatelimitRetries, - (int)options.Value.RatelimitRetryDelayFallback.TotalMilliseconds, - (int)options.Value.InitialRequestTimeout.TotalMilliseconds, - options.Value.MaximumConcurrentRestRequests - ) - { - string token = tokenContainer.Value.GetToken(); - - this.httpClient.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", $"Bot {token}"); - this.httpClient.BaseAddress = new(Endpoints.BASE_URI); - } - - // This is for meta-clients, such as the webhook client - internal RestClient - ( - HttpClient client, - TimeSpan timeout, - ILogger logger, - int maxRetries = int.MaxValue, - int retryDelayFallback = 2500, - int waitingForHashMilliseconds = 200, - int maximumRequestsPerSecond = 15 - ) - { - this.logger = logger; - this.httpClient = client; - - this.httpClient.BaseAddress = new Uri(Utilities.GetApiBaseUri()); - this.httpClient.Timeout = timeout; - this.httpClient.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", Utilities.GetUserAgent()); - this.httpClient.BaseAddress = new(Endpoints.BASE_URI); - - this.globalRateLimitEvent = new AsyncManualResetEvent(true); - - this.rateLimitStrategy = new(logger, waitingForHashMilliseconds, maximumRequestsPerSecond); - - ResiliencePipelineBuilder builder = new(); - - builder.AddRetry - ( - new() - { - DelayGenerator = result => - { - return ValueTask.FromResult(result.Outcome.Exception switch - { - PreemptiveRatelimitException preemptive => preemptive.ResetAfter, - RetryableRatelimitException real => real.ResetAfter, - _ => TimeSpan.FromMilliseconds(retryDelayFallback) - }); - }, - MaxRetryAttempts = maxRetries - } - ) - .AddStrategy(_ => this.rateLimitStrategy, new RateLimitOptions()); - - this.pipeline = builder.Build(); - } - - internal async ValueTask ExecuteRequestAsync - ( - TRequest request - ) - where TRequest : struct, IRestRequest - { - if (this.disposed) - { - throw new ObjectDisposedException - ( - "DSharpPlus Rest Client", - "The Rest Client was disposed. No further requests are possible." - ); - } - - try - { - await this.globalRateLimitEvent.WaitAsync(); - - Ulid traceId = Ulid.NewUlid(); - - ResilienceContext context = ResilienceContextPool.Shared.Get(); - - context.Properties.Set(new("route"), request.Route); - context.Properties.Set(new("exempt-from-global-limit"), request.IsExemptFromGlobalLimit); - context.Properties.Set(new("trace-id"), traceId); - context.Properties.Set(new("exempt-from-all-limits"), request.IsExemptFromAllLimits); - - using HttpResponseMessage response = await this.pipeline.ExecuteAsync - ( - async (_) => - { - using HttpRequestMessage req = request.Build(); - return await this.httpClient.SendAsync - ( - req, - HttpCompletionOption.ResponseContentRead, - CancellationToken.None - ); - }, - context - ); - - ResilienceContextPool.Shared.Return(context); - - string content = await response.Content.ReadAsStringAsync(); - - // consider logging headers too - if (this.logger.IsEnabled(LogLevel.Trace) && RuntimeFeatures.EnableRestRequestLogging) - { - string anonymized = content; - - if (RuntimeFeatures.AnonymizeTokens) - { - anonymized = AnonymizationUtilities.AnonymizeTokens(anonymized); - } - - if (RuntimeFeatures.AnonymizeContents) - { - anonymized = AnonymizationUtilities.AnonymizeContents(anonymized); - } - - this.logger.LogTrace("Request {TraceId}: {Content}", traceId, anonymized); - } - - switch (response.StatusCode) - { - case HttpStatusCode.BadRequest or HttpStatusCode.MethodNotAllowed: - - this.metrics.RegisterBadRequest(); - throw new BadRequestException(request.Build(), response, content); - - case HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden: - - this.metrics.RegisterForbidden(); - throw new UnauthorizedException(request.Build(), response, content); - - case HttpStatusCode.NotFound: - - this.metrics.RegisterNotFound(); - throw new NotFoundException(request.Build(), response, content); - - case HttpStatusCode.RequestEntityTooLarge: - - this.metrics.RegisterRequestTooLarge(); - throw new RequestSizeException(request.Build(), response, content); - - case HttpStatusCode.TooManyRequests: - - this.metrics.RegisterRatelimitHit(response.Headers); - throw new RateLimitException(request.Build(), response, content); - - case HttpStatusCode.InternalServerError - or HttpStatusCode.BadGateway - or HttpStatusCode.ServiceUnavailable - or HttpStatusCode.GatewayTimeout: - - this.metrics.RegisterServerError(); - throw new ServerErrorException(request.Build(), response, content); - - default: - - this.metrics.RegisterSuccess(); - break; - } - - return new RestResponse() - { - Response = content, - ResponseCode = response.StatusCode - }; - } - catch (Exception ex) - { - if (ex is BadRequestException badRequest) - { - this.logger.LogError - ( - "Request to {url} was rejected by the Discord API:\n" + - " Error Code: {Code}\n" + - " Errors: {Errors}\n" + - " Message: {JsonMessage}\n" + - " Stack trace: {Stacktrace}", - $"{Endpoints.BASE_URI}/{request.Url}", - badRequest.Code, - badRequest.Errors, - badRequest.JsonMessage, - badRequest.StackTrace - ); - } - else - { - this.logger.LogError - ( - LoggerEvents.RestError, - ex, - "Request to {url} triggered an exception", - $"{Endpoints.BASE_URI}/{request.Url}" - ); - } - - throw; - } - } - - /// - /// Gets the request metrics, optionally since the last time they were checked. - /// - /// If set to true, this resets the counter. Lifetime metrics are unaffected. - /// A snapshot of the rest metrics. - public RequestMetricsCollection GetRequestMetrics(bool sinceLastCall = false) - => sinceLastCall ? this.metrics.GetTemporalMetrics() : this.metrics.GetLifetimeMetrics(); - - public void Dispose() - { - if (this.disposed) - { - return; - } - - this.disposed = true; - - this.globalRateLimitEvent.Reset(); - this.rateLimitStrategy.Dispose(); - - try - { - this.httpClient?.Dispose(); - } - catch { } - } -} - -// More useless comments, sorry.. -// Was listening to this, felt like sharing. -// https://www.youtube.com/watch?v=ePX5qgDe9s4 -// ♫♪.ılılıll|̲̅̅●̲̅̅|̲̅̅=̲̅̅|̲̅̅●̲̅̅|llılılı.♫♪ +using System; +using System.Net; +using System.Net.Http; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using DSharpPlus.Exceptions; +using DSharpPlus.Logging; +using DSharpPlus.Metrics; + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +using Polly; + +namespace DSharpPlus.Net; + +/// +/// Represents a client used to make REST requests. +/// +public sealed partial class RestClient : IDisposable +{ + [GeneratedRegex(":([a-z_]+)")] + private static partial Regex GenerateRouteArgumentRegex(); + + private static readonly Regex routeArgumentRegex = GenerateRouteArgumentRegex(); + private readonly HttpClient httpClient; + private readonly ILogger logger; + private readonly AsyncManualResetEvent globalRateLimitEvent; + private readonly ResiliencePipeline pipeline; + private readonly RateLimitStrategy rateLimitStrategy; + private readonly RequestMetricsContainer metrics = new(); + + private volatile bool disposed; + + public RestClient + ( + ILogger logger, + HttpClient client, + IOptions options, + IOptions tokenContainer + ) + : this + ( + client, + options.Value.Timeout, + logger, + options.Value.MaximumRatelimitRetries, + (int)options.Value.RatelimitRetryDelayFallback.TotalMilliseconds, + (int)options.Value.InitialRequestTimeout.TotalMilliseconds, + options.Value.MaximumConcurrentRestRequests + ) + { + string token = tokenContainer.Value.GetToken(); + + this.httpClient.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", $"Bot {token}"); + this.httpClient.BaseAddress = new(Endpoints.BASE_URI); + } + + // This is for meta-clients, such as the webhook client + internal RestClient + ( + HttpClient client, + TimeSpan timeout, + ILogger logger, + int maxRetries = int.MaxValue, + int retryDelayFallback = 2500, + int waitingForHashMilliseconds = 200, + int maximumRequestsPerSecond = 15 + ) + { + this.logger = logger; + this.httpClient = client; + + this.httpClient.BaseAddress = new Uri(Utilities.GetApiBaseUri()); + this.httpClient.Timeout = timeout; + this.httpClient.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", Utilities.GetUserAgent()); + this.httpClient.BaseAddress = new(Endpoints.BASE_URI); + + this.globalRateLimitEvent = new AsyncManualResetEvent(true); + + this.rateLimitStrategy = new(logger, waitingForHashMilliseconds, maximumRequestsPerSecond); + + ResiliencePipelineBuilder builder = new(); + + builder.AddRetry + ( + new() + { + DelayGenerator = result => + { + return ValueTask.FromResult(result.Outcome.Exception switch + { + PreemptiveRatelimitException preemptive => preemptive.ResetAfter, + RetryableRatelimitException real => real.ResetAfter, + _ => TimeSpan.FromMilliseconds(retryDelayFallback) + }); + }, + MaxRetryAttempts = maxRetries + } + ) + .AddStrategy(_ => this.rateLimitStrategy, new RateLimitOptions()); + + this.pipeline = builder.Build(); + } + + internal async ValueTask ExecuteRequestAsync + ( + TRequest request + ) + where TRequest : struct, IRestRequest + { + if (this.disposed) + { + throw new ObjectDisposedException + ( + "DSharpPlus Rest Client", + "The Rest Client was disposed. No further requests are possible." + ); + } + + try + { + await this.globalRateLimitEvent.WaitAsync(); + + Ulid traceId = Ulid.NewUlid(); + + ResilienceContext context = ResilienceContextPool.Shared.Get(); + + context.Properties.Set(new("route"), request.Route); + context.Properties.Set(new("exempt-from-global-limit"), request.IsExemptFromGlobalLimit); + context.Properties.Set(new("trace-id"), traceId); + context.Properties.Set(new("exempt-from-all-limits"), request.IsExemptFromAllLimits); + + using HttpResponseMessage response = await this.pipeline.ExecuteAsync + ( + async (_) => + { + using HttpRequestMessage req = request.Build(); + return await this.httpClient.SendAsync + ( + req, + HttpCompletionOption.ResponseContentRead, + CancellationToken.None + ); + }, + context + ); + + ResilienceContextPool.Shared.Return(context); + + string content = await response.Content.ReadAsStringAsync(); + + // consider logging headers too + if (this.logger.IsEnabled(LogLevel.Trace) && RuntimeFeatures.EnableRestRequestLogging) + { + string anonymized = content; + + if (RuntimeFeatures.AnonymizeTokens) + { + anonymized = AnonymizationUtilities.AnonymizeTokens(anonymized); + } + + if (RuntimeFeatures.AnonymizeContents) + { + anonymized = AnonymizationUtilities.AnonymizeContents(anonymized); + } + + this.logger.LogTrace("Request {TraceId}: {Content}", traceId, anonymized); + } + + switch (response.StatusCode) + { + case HttpStatusCode.BadRequest or HttpStatusCode.MethodNotAllowed: + + this.metrics.RegisterBadRequest(); + throw new BadRequestException(request.Build(), response, content); + + case HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden: + + this.metrics.RegisterForbidden(); + throw new UnauthorizedException(request.Build(), response, content); + + case HttpStatusCode.NotFound: + + this.metrics.RegisterNotFound(); + throw new NotFoundException(request.Build(), response, content); + + case HttpStatusCode.RequestEntityTooLarge: + + this.metrics.RegisterRequestTooLarge(); + throw new RequestSizeException(request.Build(), response, content); + + case HttpStatusCode.TooManyRequests: + + this.metrics.RegisterRatelimitHit(response.Headers); + throw new RateLimitException(request.Build(), response, content); + + case HttpStatusCode.InternalServerError + or HttpStatusCode.BadGateway + or HttpStatusCode.ServiceUnavailable + or HttpStatusCode.GatewayTimeout: + + this.metrics.RegisterServerError(); + throw new ServerErrorException(request.Build(), response, content); + + default: + + this.metrics.RegisterSuccess(); + break; + } + + return new RestResponse() + { + Response = content, + ResponseCode = response.StatusCode + }; + } + catch (Exception ex) + { + if (ex is BadRequestException badRequest) + { + this.logger.LogError + ( + "Request to {url} was rejected by the Discord API:\n" + + " Error Code: {Code}\n" + + " Errors: {Errors}\n" + + " Message: {JsonMessage}\n" + + " Stack trace: {Stacktrace}", + $"{Endpoints.BASE_URI}/{request.Url}", + badRequest.Code, + badRequest.Errors, + badRequest.JsonMessage, + badRequest.StackTrace + ); + } + else + { + this.logger.LogError + ( + LoggerEvents.RestError, + ex, + "Request to {url} triggered an exception", + $"{Endpoints.BASE_URI}/{request.Url}" + ); + } + + throw; + } + } + + /// + /// Gets the request metrics, optionally since the last time they were checked. + /// + /// If set to true, this resets the counter. Lifetime metrics are unaffected. + /// A snapshot of the rest metrics. + public RequestMetricsCollection GetRequestMetrics(bool sinceLastCall = false) + => sinceLastCall ? this.metrics.GetTemporalMetrics() : this.metrics.GetLifetimeMetrics(); + + public void Dispose() + { + if (this.disposed) + { + return; + } + + this.disposed = true; + + this.globalRateLimitEvent.Reset(); + this.rateLimitStrategy.Dispose(); + + try + { + this.httpClient?.Dispose(); + } + catch { } + } +} + +// More useless comments, sorry.. +// Was listening to this, felt like sharing. +// https://www.youtube.com/watch?v=ePX5qgDe9s4 +// ♫♪.ılılıll|̲̅̅●̲̅̅|̲̅̅=̲̅̅|̲̅̅●̲̅̅|llılılı.♫♪ diff --git a/DSharpPlus/Net/Rest/RestRequest.cs b/DSharpPlus/Net/Rest/RestRequest.cs index 24b9f5c3c8..5d60406855 100644 --- a/DSharpPlus/Net/Rest/RestRequest.cs +++ b/DSharpPlus/Net/Rest/RestRequest.cs @@ -1,65 +1,65 @@ -using System; -using System.Collections.Generic; -using System.Net.Http; -using System.Net.Http.Headers; - -namespace DSharpPlus.Net; - -/// -/// Represents a non-multipart HTTP request. -/// -internal readonly record struct RestRequest : IRestRequest -{ - /// - public string Url { get; init; } - - /// - /// The method for this request. - /// - public HttpMethod Method { get; init; } - - /// - public string Route { get; init; } - - /// - public bool IsExemptFromGlobalLimit { get; init; } - - /// - /// The headers for this request. - /// - public IReadOnlyDictionary? Headers { get; init; } - - /// - /// The payload sent with this request. - /// - public string? Payload { get; init; } - - /// - public bool IsExemptFromAllLimits { get; init; } - - /// - public HttpRequestMessage Build() - { - HttpRequestMessage request = new() - { - Method = this.Method, - RequestUri = new($"{Endpoints.BASE_URI}/{this.Url}") - }; - - if (this.Payload is not null) - { - request.Content = new StringContent(this.Payload); - request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); - } - - if (this.Headers is not null) - { - foreach (KeyValuePair header in this.Headers) - { - request.Headers.Add(header.Key, Uri.EscapeDataString(header.Value)); - } - } - - return request; - } -} +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; + +namespace DSharpPlus.Net; + +/// +/// Represents a non-multipart HTTP request. +/// +internal readonly record struct RestRequest : IRestRequest +{ + /// + public string Url { get; init; } + + /// + /// The method for this request. + /// + public HttpMethod Method { get; init; } + + /// + public string Route { get; init; } + + /// + public bool IsExemptFromGlobalLimit { get; init; } + + /// + /// The headers for this request. + /// + public IReadOnlyDictionary? Headers { get; init; } + + /// + /// The payload sent with this request. + /// + public string? Payload { get; init; } + + /// + public bool IsExemptFromAllLimits { get; init; } + + /// + public HttpRequestMessage Build() + { + HttpRequestMessage request = new() + { + Method = this.Method, + RequestUri = new($"{Endpoints.BASE_URI}/{this.Url}") + }; + + if (this.Payload is not null) + { + request.Content = new StringContent(this.Payload); + request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json"); + } + + if (this.Headers is not null) + { + foreach (KeyValuePair header in this.Headers) + { + request.Headers.Add(header.Key, Uri.EscapeDataString(header.Value)); + } + } + + return request; + } +} diff --git a/DSharpPlus/Net/Rest/RestResponse.cs b/DSharpPlus/Net/Rest/RestResponse.cs index 672b7df895..443ed3665e 100644 --- a/DSharpPlus/Net/Rest/RestResponse.cs +++ b/DSharpPlus/Net/Rest/RestResponse.cs @@ -1,19 +1,19 @@ -using System.Net; - -namespace DSharpPlus.Net; - -/// -/// Represents a response sent by the remote HTTP party. -/// -public record struct RestResponse -{ - /// - /// Gets the response code sent by the remote party. - /// - public HttpStatusCode? ResponseCode { get; internal set; } - - /// - /// Gets the contents of the response sent by the remote party. - /// - public string? Response { get; internal set; } -} +using System.Net; + +namespace DSharpPlus.Net; + +/// +/// Represents a response sent by the remote HTTP party. +/// +public record struct RestResponse +{ + /// + /// Gets the response code sent by the remote party. + /// + public HttpStatusCode? ResponseCode { get; internal set; } + + /// + /// Gets the contents of the response sent by the remote party. + /// + public string? Response { get; internal set; } +} diff --git a/DSharpPlus/Net/Rest/SessionBucket.cs b/DSharpPlus/Net/Rest/SessionBucket.cs index ad94544371..e7d340b4f9 100644 --- a/DSharpPlus/Net/Rest/SessionBucket.cs +++ b/DSharpPlus/Net/Rest/SessionBucket.cs @@ -1,41 +1,41 @@ -using System; -using Newtonsoft.Json; - -namespace DSharpPlus.Net; - -/// -/// Represents the bucket limits for identifying to Discord. -/// This is only relevant for clients that are manually sharding. -/// -public class SessionBucket -{ - /// - /// Gets the total amount of sessions per token. - /// - [JsonProperty("total")] - public int Total { get; internal set; } - - /// - /// Gets the remaining amount of sessions for this token. - /// - [JsonProperty("remaining")] - public int Remaining { get; internal set; } - - /// - /// Gets the datetime when the will reset. - /// - [JsonIgnore] - public DateTimeOffset ResetAfter { get; internal set; } - - /// - /// Gets the maximum amount of shards that can boot concurrently. - /// - [JsonProperty("max_concurrency")] - public int MaxConcurrency { get; internal set; } - - [JsonProperty("reset_after")] - internal int ResetAfterInternal { get; set; } - - public override string ToString() - => $"[{this.Remaining}/{this.Total}] {this.ResetAfter}. {this.MaxConcurrency}x concurrency"; -} +using System; +using Newtonsoft.Json; + +namespace DSharpPlus.Net; + +/// +/// Represents the bucket limits for identifying to Discord. +/// This is only relevant for clients that are manually sharding. +/// +public class SessionBucket +{ + /// + /// Gets the total amount of sessions per token. + /// + [JsonProperty("total")] + public int Total { get; internal set; } + + /// + /// Gets the remaining amount of sessions for this token. + /// + [JsonProperty("remaining")] + public int Remaining { get; internal set; } + + /// + /// Gets the datetime when the will reset. + /// + [JsonIgnore] + public DateTimeOffset ResetAfter { get; internal set; } + + /// + /// Gets the maximum amount of shards that can boot concurrently. + /// + [JsonProperty("max_concurrency")] + public int MaxConcurrency { get; internal set; } + + [JsonProperty("reset_after")] + internal int ResetAfterInternal { get; set; } + + public override string ToString() + => $"[{this.Remaining}/{this.Total}] {this.ResetAfter}. {this.MaxConcurrency}x concurrency"; +} diff --git a/DSharpPlus/Net/Serialization/DiscordComponentJsonConverter.cs b/DSharpPlus/Net/Serialization/DiscordComponentJsonConverter.cs index 1d13dcb1b1..6ea7f9de5c 100644 --- a/DSharpPlus/Net/Serialization/DiscordComponentJsonConverter.cs +++ b/DSharpPlus/Net/Serialization/DiscordComponentJsonConverter.cs @@ -1,50 +1,50 @@ -using System; -using DSharpPlus.Entities; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace DSharpPlus.Net.Serialization; - -internal sealed class DiscordComponentJsonConverter : JsonConverter -{ - public override bool CanWrite => false; - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) => throw new NotImplementedException(); - - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - if (reader.TokenType == JsonToken.Null) - { - return null; - } - - JObject job = JObject.Load(reader); - DiscordComponentType? type = job["type"]?.ToDiscordObject() ?? throw new ArgumentException($"Value {reader} does not have a component type specifier"); - DiscordComponent cmp = type switch - { - DiscordComponentType.ActionRow => new DiscordActionRowComponent(), - DiscordComponentType.Button when (int)job["style"] is 5 => new DiscordLinkButtonComponent(), - DiscordComponentType.Button => new DiscordButtonComponent(), - DiscordComponentType.StringSelect => new DiscordSelectComponent(), - DiscordComponentType.FormInput => new DiscordTextInputComponent(), - DiscordComponentType.UserSelect => new DiscordUserSelectComponent(), - DiscordComponentType.RoleSelect => new DiscordRoleSelectComponent(), - DiscordComponentType.MentionableSelect => new DiscordMentionableSelectComponent(), - DiscordComponentType.ChannelSelect => new DiscordChannelSelectComponent(), - DiscordComponentType.Section => new DiscordSectionComponent(), - DiscordComponentType.TextDisplay => new DiscordTextDisplayComponent(), - DiscordComponentType.Thumbnail => new DiscordThumbnailComponent(), - DiscordComponentType.MediaGallery => new DiscordMediaGalleryComponent(), - DiscordComponentType.Separator => new DiscordSeparatorComponent(), - DiscordComponentType.Container => new DiscordContainerComponent(), - _ => new DiscordComponent() { Type = type.Value } - }; - - // Populate the existing component with the values in the JObject. This avoids a recursive JsonConverter loop - using JsonReader jreader = job.CreateReader(); - serializer.Populate(jreader, cmp); - - return cmp; - } - - public override bool CanConvert(Type objectType) => typeof(DiscordComponent).IsAssignableFrom(objectType); -} +using System; +using DSharpPlus.Entities; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace DSharpPlus.Net.Serialization; + +internal sealed class DiscordComponentJsonConverter : JsonConverter +{ + public override bool CanWrite => false; + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) => throw new NotImplementedException(); + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.Null) + { + return null; + } + + JObject job = JObject.Load(reader); + DiscordComponentType? type = job["type"]?.ToDiscordObject() ?? throw new ArgumentException($"Value {reader} does not have a component type specifier"); + DiscordComponent cmp = type switch + { + DiscordComponentType.ActionRow => new DiscordActionRowComponent(), + DiscordComponentType.Button when (int)job["style"] is 5 => new DiscordLinkButtonComponent(), + DiscordComponentType.Button => new DiscordButtonComponent(), + DiscordComponentType.StringSelect => new DiscordSelectComponent(), + DiscordComponentType.FormInput => new DiscordTextInputComponent(), + DiscordComponentType.UserSelect => new DiscordUserSelectComponent(), + DiscordComponentType.RoleSelect => new DiscordRoleSelectComponent(), + DiscordComponentType.MentionableSelect => new DiscordMentionableSelectComponent(), + DiscordComponentType.ChannelSelect => new DiscordChannelSelectComponent(), + DiscordComponentType.Section => new DiscordSectionComponent(), + DiscordComponentType.TextDisplay => new DiscordTextDisplayComponent(), + DiscordComponentType.Thumbnail => new DiscordThumbnailComponent(), + DiscordComponentType.MediaGallery => new DiscordMediaGalleryComponent(), + DiscordComponentType.Separator => new DiscordSeparatorComponent(), + DiscordComponentType.Container => new DiscordContainerComponent(), + _ => new DiscordComponent() { Type = type.Value } + }; + + // Populate the existing component with the values in the JObject. This avoids a recursive JsonConverter loop + using JsonReader jreader = job.CreateReader(); + serializer.Populate(jreader, cmp); + + return cmp; + } + + public override bool CanConvert(Type objectType) => typeof(DiscordComponent).IsAssignableFrom(objectType); +} diff --git a/DSharpPlus/Net/Serialization/DiscordForumChannelJsonConverter.cs b/DSharpPlus/Net/Serialization/DiscordForumChannelJsonConverter.cs index b55c13a51d..09e27bf8f1 100644 --- a/DSharpPlus/Net/Serialization/DiscordForumChannelJsonConverter.cs +++ b/DSharpPlus/Net/Serialization/DiscordForumChannelJsonConverter.cs @@ -1,58 +1,58 @@ -using System; -using DSharpPlus.Entities; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace DSharpPlus.Net.Serialization; - -public class DiscordForumChannelJsonConverter : JsonConverter -{ - public override bool CanWrite => false; - - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) => throw new(); - - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - JObject job = JObject.Load(reader); - bool hasType = job.TryGetValue("type", out JToken typeToken); - - if (!hasType) - { - throw new JsonException("Channel object lacks type - this should be reported to library developers"); - } - - DiscordChannel channel; - DiscordChannelType channelType = typeToken.ToObject(); - - if (channelType is DiscordChannelType.GuildForum) - { - // Type erasure is almost unheard of in C#, but you never know... - DiscordForumChannel chn = new(); - serializer.Populate(job.CreateReader(), chn); - - channel = chn; - } - // May or not be necessary. Better safe than sorry. - else if (channelType is DiscordChannelType.NewsThread or DiscordChannelType.PrivateThread or DiscordChannelType.PublicThread) - { - DiscordThreadChannel chn = new(); - serializer.Populate(job.CreateReader(), chn); - - channel = chn; - } - else if (channelType is DiscordChannelType.Private or DiscordChannelType.Group) - { - channel = new DiscordDmChannel(); - serializer.Populate(job.CreateReader(), channel); - } - else - { - channel = new DiscordChannel(); - serializer.Populate(job.CreateReader(), channel); - } - - return channel; - } - - public override bool CanConvert(Type objectType) => objectType.IsAssignableFrom(typeof(DiscordChannel)); -} +using System; +using DSharpPlus.Entities; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace DSharpPlus.Net.Serialization; + +public class DiscordForumChannelJsonConverter : JsonConverter +{ + public override bool CanWrite => false; + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) => throw new(); + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + JObject job = JObject.Load(reader); + bool hasType = job.TryGetValue("type", out JToken typeToken); + + if (!hasType) + { + throw new JsonException("Channel object lacks type - this should be reported to library developers"); + } + + DiscordChannel channel; + DiscordChannelType channelType = typeToken.ToObject(); + + if (channelType is DiscordChannelType.GuildForum) + { + // Type erasure is almost unheard of in C#, but you never know... + DiscordForumChannel chn = new(); + serializer.Populate(job.CreateReader(), chn); + + channel = chn; + } + // May or not be necessary. Better safe than sorry. + else if (channelType is DiscordChannelType.NewsThread or DiscordChannelType.PrivateThread or DiscordChannelType.PublicThread) + { + DiscordThreadChannel chn = new(); + serializer.Populate(job.CreateReader(), chn); + + channel = chn; + } + else if (channelType is DiscordChannelType.Private or DiscordChannelType.Group) + { + channel = new DiscordDmChannel(); + serializer.Populate(job.CreateReader(), channel); + } + else + { + channel = new DiscordChannel(); + serializer.Populate(job.CreateReader(), channel); + } + + return channel; + } + + public override bool CanConvert(Type objectType) => objectType.IsAssignableFrom(typeof(DiscordChannel)); +} diff --git a/DSharpPlus/Net/Serialization/DiscordJson.cs b/DSharpPlus/Net/Serialization/DiscordJson.cs index b89ea09534..bee000afec 100644 --- a/DSharpPlus/Net/Serialization/DiscordJson.cs +++ b/DSharpPlus/Net/Serialization/DiscordJson.cs @@ -1,53 +1,53 @@ -using System; -using System.Globalization; -using System.IO; -using System.Text; -using DSharpPlus.Entities; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace DSharpPlus.Net.Serialization; - -public static class DiscordJson -{ - private static readonly JsonSerializer serializer = JsonSerializer.CreateDefault(new JsonSerializerSettings - { - ContractResolver = new OptionalJsonContractResolver(), - DateParseHandling = DateParseHandling.None, - Converters = new[] { new ISO8601DateTimeOffsetJsonConverter() } - }); - - /// Serializes the specified object to a JSON string. - /// The object to serialize. - /// A JSON string representation of the object. - public static string SerializeObject(object value) => SerializeObjectInternal(value, null, serializer); - - /// Populates an object with the values from a JSON node. - /// The token to populate the object with. - /// The object to populate. - public static void PopulateObject(JToken value, object target) - { - using JsonReader reader = value.CreateReader(); - serializer.Populate(reader, target); - } - - /// - /// Converts this token into an object, passing any properties through extra s if - /// needed. - /// - /// The token to convert - /// Type to convert to - /// The converted token - public static T ToDiscordObject(this JToken token) => token.ToObject(serializer); - - private static string SerializeObjectInternal(object value, Type type, JsonSerializer jsonSerializer) - { - StringWriter stringWriter = new(new StringBuilder(256), CultureInfo.InvariantCulture); - using (JsonTextWriter jsonTextWriter = new(stringWriter)) - { - jsonTextWriter.Formatting = jsonSerializer.Formatting; - jsonSerializer.Serialize(jsonTextWriter, value, type); - } - return stringWriter.ToString(); - } -} +using System; +using System.Globalization; +using System.IO; +using System.Text; +using DSharpPlus.Entities; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace DSharpPlus.Net.Serialization; + +public static class DiscordJson +{ + private static readonly JsonSerializer serializer = JsonSerializer.CreateDefault(new JsonSerializerSettings + { + ContractResolver = new OptionalJsonContractResolver(), + DateParseHandling = DateParseHandling.None, + Converters = new[] { new ISO8601DateTimeOffsetJsonConverter() } + }); + + /// Serializes the specified object to a JSON string. + /// The object to serialize. + /// A JSON string representation of the object. + public static string SerializeObject(object value) => SerializeObjectInternal(value, null, serializer); + + /// Populates an object with the values from a JSON node. + /// The token to populate the object with. + /// The object to populate. + public static void PopulateObject(JToken value, object target) + { + using JsonReader reader = value.CreateReader(); + serializer.Populate(reader, target); + } + + /// + /// Converts this token into an object, passing any properties through extra s if + /// needed. + /// + /// The token to convert + /// Type to convert to + /// The converted token + public static T ToDiscordObject(this JToken token) => token.ToObject(serializer); + + private static string SerializeObjectInternal(object value, Type type, JsonSerializer jsonSerializer) + { + StringWriter stringWriter = new(new StringBuilder(256), CultureInfo.InvariantCulture); + using (JsonTextWriter jsonTextWriter = new(stringWriter)) + { + jsonTextWriter.Formatting = jsonSerializer.Formatting; + jsonSerializer.Serialize(jsonTextWriter, value, type); + } + return stringWriter.ToString(); + } +} diff --git a/DSharpPlus/Net/Serialization/DiscordPermissionsAsStringJsonConverter.cs b/DSharpPlus/Net/Serialization/DiscordPermissionsAsStringJsonConverter.cs index f0d1a9ff48..8331e1a89d 100644 --- a/DSharpPlus/Net/Serialization/DiscordPermissionsAsStringJsonConverter.cs +++ b/DSharpPlus/Net/Serialization/DiscordPermissionsAsStringJsonConverter.cs @@ -1,38 +1,38 @@ -using System; -using System.Numerics; -using DSharpPlus.Entities; -using Newtonsoft.Json; - -namespace DSharpPlus.Net.Serialization; - -/// -/// Facilitates serializing permissions as string. -/// -internal sealed class DiscordPermissionsAsStringJsonConverter : JsonConverter -{ - public override DiscordPermissions ReadJson - ( - JsonReader reader, - Type objectType, - DiscordPermissions existingValue, - bool hasExistingValue, - JsonSerializer serializer - ) - { - string? value = reader.Value as string; - - return value is not null ? new(BigInteger.Parse(value)) : existingValue; - } - - public override void WriteJson(JsonWriter writer, DiscordPermissions value, JsonSerializer serializer) - { - if (value == DiscordPermissions.None) - { - writer.WriteNull(); - } - else - { - writer.WriteValue(value.ToString()); - } - } -} +using System; +using System.Numerics; +using DSharpPlus.Entities; +using Newtonsoft.Json; + +namespace DSharpPlus.Net.Serialization; + +/// +/// Facilitates serializing permissions as string. +/// +internal sealed class DiscordPermissionsAsStringJsonConverter : JsonConverter +{ + public override DiscordPermissions ReadJson + ( + JsonReader reader, + Type objectType, + DiscordPermissions existingValue, + bool hasExistingValue, + JsonSerializer serializer + ) + { + string? value = reader.Value as string; + + return value is not null ? new(BigInteger.Parse(value)) : existingValue; + } + + public override void WriteJson(JsonWriter writer, DiscordPermissions value, JsonSerializer serializer) + { + if (value == DiscordPermissions.None) + { + writer.WriteNull(); + } + else + { + writer.WriteValue(value.ToString()); + } + } +} diff --git a/DSharpPlus/Net/Serialization/ISO8601DateTimeOffsetJsonConverter.cs b/DSharpPlus/Net/Serialization/ISO8601DateTimeOffsetJsonConverter.cs index 6b118d7664..b9f98767a9 100644 --- a/DSharpPlus/Net/Serialization/ISO8601DateTimeOffsetJsonConverter.cs +++ b/DSharpPlus/Net/Serialization/ISO8601DateTimeOffsetJsonConverter.cs @@ -1,22 +1,22 @@ -using System; -using System.Globalization; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace DSharpPlus.Net.Serialization; - -/// -/// Json converter for handling DateTimeOffset values. -/// -internal sealed class ISO8601DateTimeOffsetJsonConverter : JsonConverter -{ - public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) - => writer.WriteValue(((DateTimeOffset)value!).ToString("O", CultureInfo.InvariantCulture)); - public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) - { - JToken jr = JToken.Load(reader); - - return jr.ToObject(); - } - public override bool CanConvert(Type objectType) => objectType == typeof(DateTimeOffset); -} +using System; +using System.Globalization; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace DSharpPlus.Net.Serialization; + +/// +/// Json converter for handling DateTimeOffset values. +/// +internal sealed class ISO8601DateTimeOffsetJsonConverter : JsonConverter +{ + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) + => writer.WriteValue(((DateTimeOffset)value!).ToString("O", CultureInfo.InvariantCulture)); + public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) + { + JToken jr = JToken.Load(reader); + + return jr.ToObject(); + } + public override bool CanConvert(Type objectType) => objectType == typeof(DateTimeOffset); +} diff --git a/DSharpPlus/Net/Serialization/SnowflakeArrayAsDictionaryJsonConverter.cs b/DSharpPlus/Net/Serialization/SnowflakeArrayAsDictionaryJsonConverter.cs index 3468f8335a..00e74a5dd9 100644 --- a/DSharpPlus/Net/Serialization/SnowflakeArrayAsDictionaryJsonConverter.cs +++ b/DSharpPlus/Net/Serialization/SnowflakeArrayAsDictionaryJsonConverter.cs @@ -1,76 +1,76 @@ -using System; -using System.Collections; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using DSharpPlus.Entities; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace DSharpPlus.Net.Serialization; - -/// -/// Used for a or mapping -/// to any class extending (or, as a special case, -/// ). When serializing, discards the ulong -/// keys and writes only the values. When deserializing, pulls the keys from (or, -/// in the case of , . -/// -internal class SnowflakeArrayAsDictionaryJsonConverter : JsonConverter -{ - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - if (value == null) - { - writer.WriteNull(); - } - else - { - TypeInfo type = value.GetType().GetTypeInfo(); - JToken.FromObject(type.GetDeclaredProperty("Values").GetValue(value)).WriteTo(writer); - } - } - - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - ConstructorInfo? constructor = objectType.GetTypeInfo().DeclaredConstructors - .FirstOrDefault(e => !e.IsStatic && e.GetParameters().Length == 0); - - object dict = constructor.Invoke([]); - - // the default name of an indexer is "Item" - PropertyInfo? properties = objectType.GetTypeInfo().GetDeclaredProperty("Item"); - - IEnumerable? entries = (IEnumerable)serializer.Deserialize(reader, objectType.GenericTypeArguments[1].MakeArrayType()); - foreach (object? entry in entries) - { - properties.SetValue(dict, entry, - [ - (entry as SnowflakeObject)?.Id - ?? (entry as DiscordVoiceState)?.UserId - ?? throw new InvalidOperationException($"Type {entry?.GetType()} is not deserializable") - ]); - } - - return dict; - } - - public override bool CanConvert(Type objectType) - { - Type genericTypedef = objectType.GetGenericTypeDefinition(); - if (genericTypedef != typeof(Dictionary<,>) && genericTypedef != typeof(ConcurrentDictionary<,>)) - { - return false; - } - - if (objectType.GenericTypeArguments[0] != typeof(ulong)) - { - return false; - } - - Type valueParam = objectType.GenericTypeArguments[1]; - return typeof(SnowflakeObject).GetTypeInfo().IsAssignableFrom(valueParam.GetTypeInfo()) || - valueParam == typeof(DiscordVoiceState); - } -} +using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using DSharpPlus.Entities; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace DSharpPlus.Net.Serialization; + +/// +/// Used for a or mapping +/// to any class extending (or, as a special case, +/// ). When serializing, discards the ulong +/// keys and writes only the values. When deserializing, pulls the keys from (or, +/// in the case of , . +/// +internal class SnowflakeArrayAsDictionaryJsonConverter : JsonConverter +{ + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + if (value == null) + { + writer.WriteNull(); + } + else + { + TypeInfo type = value.GetType().GetTypeInfo(); + JToken.FromObject(type.GetDeclaredProperty("Values").GetValue(value)).WriteTo(writer); + } + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + ConstructorInfo? constructor = objectType.GetTypeInfo().DeclaredConstructors + .FirstOrDefault(e => !e.IsStatic && e.GetParameters().Length == 0); + + object dict = constructor.Invoke([]); + + // the default name of an indexer is "Item" + PropertyInfo? properties = objectType.GetTypeInfo().GetDeclaredProperty("Item"); + + IEnumerable? entries = (IEnumerable)serializer.Deserialize(reader, objectType.GenericTypeArguments[1].MakeArrayType()); + foreach (object? entry in entries) + { + properties.SetValue(dict, entry, + [ + (entry as SnowflakeObject)?.Id + ?? (entry as DiscordVoiceState)?.UserId + ?? throw new InvalidOperationException($"Type {entry?.GetType()} is not deserializable") + ]); + } + + return dict; + } + + public override bool CanConvert(Type objectType) + { + Type genericTypedef = objectType.GetGenericTypeDefinition(); + if (genericTypedef != typeof(Dictionary<,>) && genericTypedef != typeof(ConcurrentDictionary<,>)) + { + return false; + } + + if (objectType.GenericTypeArguments[0] != typeof(ulong)) + { + return false; + } + + Type valueParam = objectType.GenericTypeArguments[1]; + return typeof(SnowflakeObject).GetTypeInfo().IsAssignableFrom(valueParam.GetTypeInfo()) || + valueParam == typeof(DiscordVoiceState); + } +} diff --git a/DSharpPlus/Net/Udp/BaseUdpClient.cs b/DSharpPlus/Net/Udp/BaseUdpClient.cs index 09b42fc5d7..75b39cadbc 100644 --- a/DSharpPlus/Net/Udp/BaseUdpClient.cs +++ b/DSharpPlus/Net/Udp/BaseUdpClient.cs @@ -1,40 +1,40 @@ -using System.Threading.Tasks; - -namespace DSharpPlus.Net.Udp; - -/// -/// Creates an instance of a UDP client implementation. -/// -/// Constructed UDP client implementation. -public delegate BaseUdpClient UdpClientFactoryDelegate(); - -/// -/// Represents a base abstraction for all UDP client implementations. -/// -public abstract class BaseUdpClient -{ - /// - /// Configures the UDP client. - /// - /// Endpoint that the client will be communicating with. - public abstract void Setup(ConnectionEndpoint endpoint); - - /// - /// Sends a datagram. - /// - /// Datagram. - /// Length of the datagram. - /// - public abstract Task SendAsync(byte[] data, int dataLength); - - /// - /// Receives a datagram. - /// - /// The received bytes. - public abstract Task ReceiveAsync(); - - /// - /// Closes and disposes the client. - /// - public abstract void Close(); -} +using System.Threading.Tasks; + +namespace DSharpPlus.Net.Udp; + +/// +/// Creates an instance of a UDP client implementation. +/// +/// Constructed UDP client implementation. +public delegate BaseUdpClient UdpClientFactoryDelegate(); + +/// +/// Represents a base abstraction for all UDP client implementations. +/// +public abstract class BaseUdpClient +{ + /// + /// Configures the UDP client. + /// + /// Endpoint that the client will be communicating with. + public abstract void Setup(ConnectionEndpoint endpoint); + + /// + /// Sends a datagram. + /// + /// Datagram. + /// Length of the datagram. + /// + public abstract Task SendAsync(byte[] data, int dataLength); + + /// + /// Receives a datagram. + /// + /// The received bytes. + public abstract Task ReceiveAsync(); + + /// + /// Closes and disposes the client. + /// + public abstract void Close(); +} diff --git a/DSharpPlus/Net/Udp/DspUdpClient.cs b/DSharpPlus/Net/Udp/DspUdpClient.cs index d125c8f09b..71a8962bf5 100644 --- a/DSharpPlus/Net/Udp/DspUdpClient.cs +++ b/DSharpPlus/Net/Udp/DspUdpClient.cs @@ -1,91 +1,91 @@ -using System; -using System.Collections.Concurrent; -using System.Net.Sockets; -using System.Threading; -using System.Threading.Tasks; - -namespace DSharpPlus.Net.Udp; - -/// -/// The default, native-based UDP client implementation. -/// -internal class DspUdpClient : BaseUdpClient -{ - private UdpClient Client { get; set; } - private ConnectionEndpoint EndPoint { get; set; } - private BlockingCollection PacketQueue { get; } - - private CancellationTokenSource TokenSource { get; } - private CancellationToken Token => this.TokenSource.Token; - - /// - /// Creates a new UDP client instance. - /// - public DspUdpClient() - { - this.PacketQueue = []; - 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(); - _ = Task.Run(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); - - /// - /// Receives a datagram. - /// - /// The received bytes. - public override Task ReceiveAsync() => Task.FromResult(this.PacketQueue.Take(this.Token)); - - /// - /// Closes and disposes the client. - /// - public override void Close() - { - this.TokenSource.Cancel(); -#if !NETSTANDARD1_3 - try - { this.Client.Close(); } - catch (Exception) { } -#endif - - // dequeue all the packets - this.PacketQueue.Dispose(); - } - - private async Task ReceiverLoopAsync() - { - while (!this.Token.IsCancellationRequested) - { - try - { - UdpReceiveResult packet = await this.Client.ReceiveAsync(); - this.PacketQueue.Add(packet.Buffer); - } - catch (Exception) { } - } - } - - /// - /// Creates a new instance of . - /// - /// An instance of . - public static BaseUdpClient CreateNew() - => new DspUdpClient(); -} +using System; +using System.Collections.Concurrent; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; + +namespace DSharpPlus.Net.Udp; + +/// +/// The default, native-based UDP client implementation. +/// +internal class DspUdpClient : BaseUdpClient +{ + private UdpClient Client { get; set; } + private ConnectionEndpoint EndPoint { get; set; } + private BlockingCollection PacketQueue { get; } + + private CancellationTokenSource TokenSource { get; } + private CancellationToken Token => this.TokenSource.Token; + + /// + /// Creates a new UDP client instance. + /// + public DspUdpClient() + { + this.PacketQueue = []; + 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(); + _ = Task.Run(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); + + /// + /// Receives a datagram. + /// + /// The received bytes. + public override Task ReceiveAsync() => Task.FromResult(this.PacketQueue.Take(this.Token)); + + /// + /// Closes and disposes the client. + /// + public override void Close() + { + this.TokenSource.Cancel(); +#if !NETSTANDARD1_3 + try + { this.Client.Close(); } + catch (Exception) { } +#endif + + // dequeue all the packets + this.PacketQueue.Dispose(); + } + + private async Task ReceiverLoopAsync() + { + while (!this.Token.IsCancellationRequested) + { + try + { + UdpReceiveResult packet = await this.Client.ReceiveAsync(); + this.PacketQueue.Add(packet.Buffer); + } + catch (Exception) { } + } + } + + /// + /// Creates a new instance of . + /// + /// An instance of . + public static BaseUdpClient CreateNew() + => new DspUdpClient(); +} diff --git a/DSharpPlus/Net/WebSocket/SocketLock.cs b/DSharpPlus/Net/WebSocket/SocketLock.cs index 56e1b89e4a..7479e07c96 100644 --- a/DSharpPlus/Net/WebSocket/SocketLock.cs +++ b/DSharpPlus/Net/WebSocket/SocketLock.cs @@ -1,70 +1,70 @@ -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace DSharpPlus.Net.WebSocket; - -// Licensed from Clyde.NET (etc; I don't know how licenses work) - -internal sealed class SocketLock : IDisposable -{ - public ulong ApplicationId { get; } - - private SemaphoreSlim LockSemaphore { get; } - private CancellationTokenSource TimeoutCancelSource { get; set; } - private CancellationToken TimeoutCancel => this.TimeoutCancelSource.Token; - private Task UnlockTask { get; set; } - private int MaxConcurrency { get; set; } - - public SocketLock(ulong appId, int maxConcurrency) - { - this.ApplicationId = appId; - this.TimeoutCancelSource = null; - this.MaxConcurrency = maxConcurrency; - this.LockSemaphore = new SemaphoreSlim(maxConcurrency); - } - - public async Task LockAsync() - { - await this.LockSemaphore.WaitAsync(); - - this.TimeoutCancelSource = new CancellationTokenSource(); - this.UnlockTask = Task.Delay(TimeSpan.FromSeconds(30), this.TimeoutCancel); - _ = this.UnlockTask.ContinueWith(InternalUnlock, TaskContinuationOptions.NotOnCanceled); - } - - public void UnlockAfter(TimeSpan unlockDelay) - { - 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(); - } - catch { } - this.TimeoutCancelSource = null; - - this.UnlockTask = Task.Delay(unlockDelay, CancellationToken.None); - _ = this.UnlockTask.ContinueWith(InternalUnlock); - } - - public Task WaitAsync() - => this.LockSemaphore.WaitAsync(); - - public void Dispose() - { - try - { - this.TimeoutCancelSource?.Cancel(); - this.TimeoutCancelSource?.Dispose(); - } - catch { } - } - - private void InternalUnlock(Task t) - => this.LockSemaphore.Release(this.MaxConcurrency); -} +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace DSharpPlus.Net.WebSocket; + +// Licensed from Clyde.NET (etc; I don't know how licenses work) + +internal sealed class SocketLock : IDisposable +{ + public ulong ApplicationId { get; } + + private SemaphoreSlim LockSemaphore { get; } + private CancellationTokenSource TimeoutCancelSource { get; set; } + private CancellationToken TimeoutCancel => this.TimeoutCancelSource.Token; + private Task UnlockTask { get; set; } + private int MaxConcurrency { get; set; } + + public SocketLock(ulong appId, int maxConcurrency) + { + this.ApplicationId = appId; + this.TimeoutCancelSource = null; + this.MaxConcurrency = maxConcurrency; + this.LockSemaphore = new SemaphoreSlim(maxConcurrency); + } + + public async Task LockAsync() + { + await this.LockSemaphore.WaitAsync(); + + this.TimeoutCancelSource = new CancellationTokenSource(); + this.UnlockTask = Task.Delay(TimeSpan.FromSeconds(30), this.TimeoutCancel); + _ = this.UnlockTask.ContinueWith(InternalUnlock, TaskContinuationOptions.NotOnCanceled); + } + + public void UnlockAfter(TimeSpan unlockDelay) + { + 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(); + } + catch { } + this.TimeoutCancelSource = null; + + this.UnlockTask = Task.Delay(unlockDelay, CancellationToken.None); + _ = this.UnlockTask.ContinueWith(InternalUnlock); + } + + public Task WaitAsync() + => this.LockSemaphore.WaitAsync(); + + public void Dispose() + { + try + { + this.TimeoutCancelSource?.Cancel(); + this.TimeoutCancelSource?.Dispose(); + } + catch { } + } + + private void InternalUnlock(Task t) + => this.LockSemaphore.Release(this.MaxConcurrency); +} diff --git a/DSharpPlus/Properties/AssemblyProperties.cs b/DSharpPlus/Properties/AssemblyProperties.cs index 5c3db4c306..245db61bcb 100644 --- a/DSharpPlus/Properties/AssemblyProperties.cs +++ b/DSharpPlus/Properties/AssemblyProperties.cs @@ -1,11 +1,11 @@ -using System.Runtime.CompilerServices; - -[assembly: InternalsVisibleTo("DSharpPlus.CommandsNext")] -[assembly: InternalsVisibleTo("DSharpPlus.SlashCommands")] -[assembly: InternalsVisibleTo("DSharpPlus.Interactivity")] -[assembly: InternalsVisibleTo("DSharpPlus.VoiceNext")] -[assembly: InternalsVisibleTo("DSharpPlus.Lavalink")] -[assembly: InternalsVisibleTo("DSharpPlus.Rest")] - -[assembly: InternalsVisibleTo("DSharpPlus.Tests")] -[assembly: InternalsVisibleTo("DSharpPlus.Tools.ShardedEventHandlingGen")] +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("DSharpPlus.CommandsNext")] +[assembly: InternalsVisibleTo("DSharpPlus.SlashCommands")] +[assembly: InternalsVisibleTo("DSharpPlus.Interactivity")] +[assembly: InternalsVisibleTo("DSharpPlus.VoiceNext")] +[assembly: InternalsVisibleTo("DSharpPlus.Lavalink")] +[assembly: InternalsVisibleTo("DSharpPlus.Rest")] + +[assembly: InternalsVisibleTo("DSharpPlus.Tests")] +[assembly: InternalsVisibleTo("DSharpPlus.Tools.ShardedEventHandlingGen")] diff --git a/DSharpPlus/QueryUriBuilder.cs b/DSharpPlus/QueryUriBuilder.cs index f4fd49e5fd..78e4106468 100644 --- a/DSharpPlus/QueryUriBuilder.cs +++ b/DSharpPlus/QueryUriBuilder.cs @@ -1,47 +1,47 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace DSharpPlus; - -internal class QueryUriBuilder -{ - public string SourceUri { get; } - - public IReadOnlyList> QueryParameters => this.queryParams; - private readonly List> queryParams = []; - - public QueryUriBuilder(string uri) - { - ArgumentNullException.ThrowIfNull(uri, nameof(uri)); - - this.SourceUri = uri; - } - - public QueryUriBuilder AddParameter(string key, string? value) - { - if (value is null) - { - return this; - } - - this.queryParams.Add(new KeyValuePair(key, value)); - return this; - } - - public string Build() - { - string query = string.Join - ( - "&", - this.queryParams.Select - ( - e => Uri.EscapeDataString(e.Key) + '=' + Uri.EscapeDataString(e.Value) - ) - ); - - return $"{this.SourceUri}?{query}"; - } - - public override string ToString() => Build().ToString(); -} +using System; +using System.Collections.Generic; +using System.Linq; + +namespace DSharpPlus; + +internal class QueryUriBuilder +{ + public string SourceUri { get; } + + public IReadOnlyList> QueryParameters => this.queryParams; + private readonly List> queryParams = []; + + public QueryUriBuilder(string uri) + { + ArgumentNullException.ThrowIfNull(uri, nameof(uri)); + + this.SourceUri = uri; + } + + public QueryUriBuilder AddParameter(string key, string? value) + { + if (value is null) + { + return this; + } + + this.queryParams.Add(new KeyValuePair(key, value)); + return this; + } + + public string Build() + { + string query = string.Join + ( + "&", + this.queryParams.Select + ( + e => Uri.EscapeDataString(e.Key) + '=' + Uri.EscapeDataString(e.Value) + ) + ); + + return $"{this.SourceUri}?{query}"; + } + + public override string ToString() => Build().ToString(); +} diff --git a/DSharpPlus/ReadOnlyConcurrentDictionary.cs b/DSharpPlus/ReadOnlyConcurrentDictionary.cs index cd242c85c2..11d842da2e 100644 --- a/DSharpPlus/ReadOnlyConcurrentDictionary.cs +++ b/DSharpPlus/ReadOnlyConcurrentDictionary.cs @@ -1,43 +1,43 @@ -using System.Collections; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Runtime.CompilerServices; - -namespace DSharpPlus; - -/// -/// Read-only view of a given . -/// -/// The type of keys in the dictionary. -/// The type of values in the dictionary. -internal readonly struct ReadOnlyConcurrentDictionary : IReadOnlyDictionary - where TKey : notnull -{ - private readonly ConcurrentDictionary underlyingDict; - - /// - /// Creates a new read-only view of the given dictionary. - /// - /// Dictionary to create a view over. - public ReadOnlyConcurrentDictionary(ConcurrentDictionary underlyingDict) => this.underlyingDict = underlyingDict; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public IEnumerator> GetEnumerator() => this.underlyingDict.GetEnumerator(); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)this.underlyingDict).GetEnumerator(); - - public int Count => this.underlyingDict.Count; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool ContainsKey(TKey key) => this.underlyingDict.ContainsKey(key); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool TryGetValue(TKey key, out TValue value) => this.underlyingDict.TryGetValue(key, out value); - - public TValue this[TKey key] => this.underlyingDict[key]; - - public IEnumerable Keys => this.underlyingDict.Keys; - - public IEnumerable Values => this.underlyingDict.Values; -} +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace DSharpPlus; + +/// +/// Read-only view of a given . +/// +/// The type of keys in the dictionary. +/// The type of values in the dictionary. +internal readonly struct ReadOnlyConcurrentDictionary : IReadOnlyDictionary + where TKey : notnull +{ + private readonly ConcurrentDictionary underlyingDict; + + /// + /// Creates a new read-only view of the given dictionary. + /// + /// Dictionary to create a view over. + public ReadOnlyConcurrentDictionary(ConcurrentDictionary underlyingDict) => this.underlyingDict = underlyingDict; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public IEnumerator> GetEnumerator() => this.underlyingDict.GetEnumerator(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)this.underlyingDict).GetEnumerator(); + + public int Count => this.underlyingDict.Count; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool ContainsKey(TKey key) => this.underlyingDict.ContainsKey(key); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryGetValue(TKey key, out TValue value) => this.underlyingDict.TryGetValue(key, out value); + + public TValue this[TKey key] => this.underlyingDict[key]; + + public IEnumerable Keys => this.underlyingDict.Keys; + + public IEnumerable Values => this.underlyingDict.Values; +} diff --git a/DSharpPlus/ReadOnlySet.cs b/DSharpPlus/ReadOnlySet.cs index aeec8a90e5..b950bcba26 100644 --- a/DSharpPlus/ReadOnlySet.cs +++ b/DSharpPlus/ReadOnlySet.cs @@ -1,47 +1,47 @@ -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.CompilerServices; - -namespace DSharpPlus; - -/// -/// Read-only view of a given . -/// -/// Type of the items in the set. -internal readonly struct ReadOnlySet : IReadOnlyList -{ - private readonly ISet underlyingSet; - - /// - /// Creates a new read-only view of the given set. - /// - /// Set to create a view over. - public ReadOnlySet(ISet sourceSet) => this.underlyingSet = sourceSet; - - /// - /// Gets the item at the specified index. - /// - public T this[int index] => this.underlyingSet.ElementAt(index); - - /// - /// Gets the number of items in the underlying set. - /// - public int Count => this.underlyingSet.Count; - - /// - /// Returns an enumerator that iterates through this set view. - /// - /// Enumerator for the underlying set. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public IEnumerator GetEnumerator() - => this.underlyingSet.GetEnumerator(); - - /// - /// Returns an enumerator that iterates through this set view. - /// - /// Enumerator for the underlying set. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - IEnumerator IEnumerable.GetEnumerator() - => (this.underlyingSet as IEnumerable).GetEnumerator(); -} +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; + +namespace DSharpPlus; + +/// +/// Read-only view of a given . +/// +/// Type of the items in the set. +internal readonly struct ReadOnlySet : IReadOnlyList +{ + private readonly ISet underlyingSet; + + /// + /// Creates a new read-only view of the given set. + /// + /// Set to create a view over. + public ReadOnlySet(ISet sourceSet) => this.underlyingSet = sourceSet; + + /// + /// Gets the item at the specified index. + /// + public T this[int index] => this.underlyingSet.ElementAt(index); + + /// + /// Gets the number of items in the underlying set. + /// + public int Count => this.underlyingSet.Count; + + /// + /// Returns an enumerator that iterates through this set view. + /// + /// Enumerator for the underlying set. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public IEnumerator GetEnumerator() + => this.underlyingSet.GetEnumerator(); + + /// + /// Returns an enumerator that iterates through this set view. + /// + /// Enumerator for the underlying set. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + IEnumerator IEnumerable.GetEnumerator() + => (this.underlyingSet as IEnumerable).GetEnumerator(); +} diff --git a/DSharpPlus/RingBuffer.cs b/DSharpPlus/RingBuffer.cs index 839b9ea6f6..a1cffed11c 100644 --- a/DSharpPlus/RingBuffer.cs +++ b/DSharpPlus/RingBuffer.cs @@ -1,230 +1,230 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; - -namespace DSharpPlus; - -/// -/// A circular buffer collection. -/// -/// Type of elements within this ring buffer. -public class RingBuffer : ICollection -{ - /// - /// Gets the current index of the buffer items. - /// - public int CurrentIndex { get; protected set; } - - /// - /// Gets the capacity of this ring buffer. - /// - public int Capacity { get; protected set; } - - /// - /// Gets the number of items in this ring buffer. - /// - public int Count - => this.reached_end ? this.Capacity : this.CurrentIndex; - - /// - /// Gets whether this ring buffer is read-only. - /// - public bool IsReadOnly - => false; - - /// - /// Gets or sets the internal collection of items. - /// - protected T[] InternalBuffer { get; set; } - private bool reached_end = false; - - /// - /// Creates a new ring buffer with specified size. - /// - /// Size of the buffer to create. - /// - public RingBuffer(int size) - { - if (size <= 0) - { - throw new ArgumentOutOfRangeException(nameof(size), "Size must be positive."); - } - - this.CurrentIndex = 0; - this.Capacity = size; - this.InternalBuffer = new T[this.Capacity]; - } - - /// - /// Creates a new ring buffer, filled with specified elements. - /// - /// Elements to fill the buffer with. - /// - /// - public RingBuffer(IEnumerable elements) - : this(elements, 0) - { } - - /// - /// Creates a new ring buffer, filled with specified elements, and starting at specified index. - /// - /// Elements to fill the buffer with. - /// Starting element index. - /// - /// - public RingBuffer(IEnumerable elements, int index) - { - if (elements == null || !elements.Any()) - { - throw new ArgumentException("The collection cannot be null or empty.", nameof(elements)); - } - - this.CurrentIndex = index; - this.InternalBuffer = elements.ToArray(); - this.Capacity = this.InternalBuffer.Length; - - if (this.CurrentIndex >= this.InternalBuffer.Length || this.CurrentIndex < 0) - { - throw new ArgumentOutOfRangeException(nameof(index), "Index must be less than buffer capacity, and greater than zero."); - } - } - - /// - /// Inserts an item into this ring buffer. - /// - /// Item to insert. - public void Add(T item) - { - this.InternalBuffer[this.CurrentIndex++] = item; - - if (this.CurrentIndex == this.Capacity) - { - this.CurrentIndex = 0; - this.reached_end = true; - } - } - - /// - /// Gets first item from the buffer that matches the predicate. - /// - /// Predicate used to find the item. - /// Item that matches the predicate, or default value for the type of the items in this ring buffer, if one is not found. - /// Whether an item that matches the predicate was found or not. - public bool TryGet(Func predicate, out T item) - { - for (int i = this.CurrentIndex; i < this.InternalBuffer.Length; i++) - { - if (this.InternalBuffer[i] != null && predicate(this.InternalBuffer[i])) - { - item = this.InternalBuffer[i]; - return true; - } - } - for (int i = 0; i < this.CurrentIndex; i++) - { - if (this.InternalBuffer[i] != null && predicate(this.InternalBuffer[i])) - { - item = this.InternalBuffer[i]; - return true; - } - } - - item = default; - return false; - } - - /// - /// Clears this ring buffer and resets the current item index. - /// - public void Clear() - { - for (int i = 0; i < this.InternalBuffer.Length; i++) - { - this.InternalBuffer[i] = default; - } - - this.CurrentIndex = 0; - } - - /// - /// Checks whether given item is present in the buffer. This method is not implemented. Use instead. - /// - /// Item to check for. - /// Whether the buffer contains the item. - /// - public bool Contains(T item) => throw new NotImplementedException("This method is not implemented. Use.Contains(predicate) instead."); - - /// - /// Checks whether given item is present in the buffer using given predicate to find it. - /// - /// Predicate used to check for the item. - /// Whether the buffer contains the item. - public bool Contains(Func predicate) => this.InternalBuffer.Any(predicate); - - /// - /// Copies this ring buffer to target array, attempting to maintain the order of items within. - /// - /// Target array. - /// Index starting at which to copy the items to. - public void CopyTo(T[] array, int index) - { - if (array.Length - index < 1) - { - throw new ArgumentException("Target array is too small to contain the elements from this buffer.", nameof(array)); - } - - int ci = 0; - for (int i = this.CurrentIndex; i < this.InternalBuffer.Length; i++) - { - array[ci++] = this.InternalBuffer[i]; - } - - for (int i = 0; i < this.CurrentIndex; i++) - { - array[ci++] = this.InternalBuffer[i]; - } - } - - /// - /// Removes an item from the buffer. This method is not implemented. Use instead. - /// - /// Item to remove. - /// Whether an item was removed or not. - public bool Remove(T item) => throw new NotImplementedException("This method is not implemented. Use.Remove(predicate) instead."); - - /// - /// Removes an item from the buffer using given predicate to find it. - /// - /// Predicate used to find the item. - /// Whether an item was removed or not. - public bool Remove(Func predicate) - { - for (int i = 0; i < this.InternalBuffer.Length; i++) - { - if (this.InternalBuffer[i] != null && predicate(this.InternalBuffer[i])) - { - this.InternalBuffer[i] = default; - return true; - } - } - - return false; - } - - /// - /// Returns an enumerator for this ring buffer. - /// - /// Enumerator for this ring buffer. - public IEnumerator GetEnumerator() => !this.reached_end - ? this.InternalBuffer.AsEnumerable().GetEnumerator() - : this.InternalBuffer.Skip(this.CurrentIndex) - .Concat(this.InternalBuffer.Take(this.CurrentIndex)) - .GetEnumerator(); - - /// - /// Returns an enumerator for this ring buffer. - /// - /// Enumerator for this ring buffer. - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); -} +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace DSharpPlus; + +/// +/// A circular buffer collection. +/// +/// Type of elements within this ring buffer. +public class RingBuffer : ICollection +{ + /// + /// Gets the current index of the buffer items. + /// + public int CurrentIndex { get; protected set; } + + /// + /// Gets the capacity of this ring buffer. + /// + public int Capacity { get; protected set; } + + /// + /// Gets the number of items in this ring buffer. + /// + public int Count + => this.reached_end ? this.Capacity : this.CurrentIndex; + + /// + /// Gets whether this ring buffer is read-only. + /// + public bool IsReadOnly + => false; + + /// + /// Gets or sets the internal collection of items. + /// + protected T[] InternalBuffer { get; set; } + private bool reached_end = false; + + /// + /// Creates a new ring buffer with specified size. + /// + /// Size of the buffer to create. + /// + public RingBuffer(int size) + { + if (size <= 0) + { + throw new ArgumentOutOfRangeException(nameof(size), "Size must be positive."); + } + + this.CurrentIndex = 0; + this.Capacity = size; + this.InternalBuffer = new T[this.Capacity]; + } + + /// + /// Creates a new ring buffer, filled with specified elements. + /// + /// Elements to fill the buffer with. + /// + /// + public RingBuffer(IEnumerable elements) + : this(elements, 0) + { } + + /// + /// Creates a new ring buffer, filled with specified elements, and starting at specified index. + /// + /// Elements to fill the buffer with. + /// Starting element index. + /// + /// + public RingBuffer(IEnumerable elements, int index) + { + if (elements == null || !elements.Any()) + { + throw new ArgumentException("The collection cannot be null or empty.", nameof(elements)); + } + + this.CurrentIndex = index; + this.InternalBuffer = elements.ToArray(); + this.Capacity = this.InternalBuffer.Length; + + if (this.CurrentIndex >= this.InternalBuffer.Length || this.CurrentIndex < 0) + { + throw new ArgumentOutOfRangeException(nameof(index), "Index must be less than buffer capacity, and greater than zero."); + } + } + + /// + /// Inserts an item into this ring buffer. + /// + /// Item to insert. + public void Add(T item) + { + this.InternalBuffer[this.CurrentIndex++] = item; + + if (this.CurrentIndex == this.Capacity) + { + this.CurrentIndex = 0; + this.reached_end = true; + } + } + + /// + /// Gets first item from the buffer that matches the predicate. + /// + /// Predicate used to find the item. + /// Item that matches the predicate, or default value for the type of the items in this ring buffer, if one is not found. + /// Whether an item that matches the predicate was found or not. + public bool TryGet(Func predicate, out T item) + { + for (int i = this.CurrentIndex; i < this.InternalBuffer.Length; i++) + { + if (this.InternalBuffer[i] != null && predicate(this.InternalBuffer[i])) + { + item = this.InternalBuffer[i]; + return true; + } + } + for (int i = 0; i < this.CurrentIndex; i++) + { + if (this.InternalBuffer[i] != null && predicate(this.InternalBuffer[i])) + { + item = this.InternalBuffer[i]; + return true; + } + } + + item = default; + return false; + } + + /// + /// Clears this ring buffer and resets the current item index. + /// + public void Clear() + { + for (int i = 0; i < this.InternalBuffer.Length; i++) + { + this.InternalBuffer[i] = default; + } + + this.CurrentIndex = 0; + } + + /// + /// Checks whether given item is present in the buffer. This method is not implemented. Use instead. + /// + /// Item to check for. + /// Whether the buffer contains the item. + /// + public bool Contains(T item) => throw new NotImplementedException("This method is not implemented. Use.Contains(predicate) instead."); + + /// + /// Checks whether given item is present in the buffer using given predicate to find it. + /// + /// Predicate used to check for the item. + /// Whether the buffer contains the item. + public bool Contains(Func predicate) => this.InternalBuffer.Any(predicate); + + /// + /// Copies this ring buffer to target array, attempting to maintain the order of items within. + /// + /// Target array. + /// Index starting at which to copy the items to. + public void CopyTo(T[] array, int index) + { + if (array.Length - index < 1) + { + throw new ArgumentException("Target array is too small to contain the elements from this buffer.", nameof(array)); + } + + int ci = 0; + for (int i = this.CurrentIndex; i < this.InternalBuffer.Length; i++) + { + array[ci++] = this.InternalBuffer[i]; + } + + for (int i = 0; i < this.CurrentIndex; i++) + { + array[ci++] = this.InternalBuffer[i]; + } + } + + /// + /// Removes an item from the buffer. This method is not implemented. Use instead. + /// + /// Item to remove. + /// Whether an item was removed or not. + public bool Remove(T item) => throw new NotImplementedException("This method is not implemented. Use.Remove(predicate) instead."); + + /// + /// Removes an item from the buffer using given predicate to find it. + /// + /// Predicate used to find the item. + /// Whether an item was removed or not. + public bool Remove(Func predicate) + { + for (int i = 0; i < this.InternalBuffer.Length; i++) + { + if (this.InternalBuffer[i] != null && predicate(this.InternalBuffer[i])) + { + this.InternalBuffer[i] = default; + return true; + } + } + + return false; + } + + /// + /// Returns an enumerator for this ring buffer. + /// + /// Enumerator for this ring buffer. + public IEnumerator GetEnumerator() => !this.reached_end + ? this.InternalBuffer.AsEnumerable().GetEnumerator() + : this.InternalBuffer.Skip(this.CurrentIndex) + .Concat(this.InternalBuffer.Take(this.CurrentIndex)) + .GetEnumerator(); + + /// + /// Returns an enumerator for this ring buffer. + /// + /// Enumerator for this ring buffer. + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} diff --git a/DSharpPlus/TimestampFormat.cs b/DSharpPlus/TimestampFormat.cs index 271f7b2ff1..f7cb47a46a 100644 --- a/DSharpPlus/TimestampFormat.cs +++ b/DSharpPlus/TimestampFormat.cs @@ -1,43 +1,43 @@ -namespace DSharpPlus; - - -/// -/// Denotes the type of formatting to use for timestamps. -/// -public enum TimestampFormat : byte -{ - /// - /// A short date. e.g. 18/06/2021. - /// - ShortDate = (byte)'d', - - /// - /// A long date. e.g. 18 June 2021. - /// - LongDate = (byte)'D', - - /// - /// A short date and time. e.g. 18 June 2021 03:50. - /// - ShortDateTime = (byte)'f', - - /// - /// A long date and time. e.g. Friday 18 June 2021 03:50. - /// - LongDateTime = (byte)'F', - - /// - /// A short time. e.g. 03:50. - /// - ShortTime = (byte)'t', - - /// - /// A long time. e.g. 03:50:15. - /// - LongTime = (byte)'T', - - /// - /// The time relative to the client. e.g. An hour ago. - /// - RelativeTime = (byte)'R' -} +namespace DSharpPlus; + + +/// +/// Denotes the type of formatting to use for timestamps. +/// +public enum TimestampFormat : byte +{ + /// + /// A short date. e.g. 18/06/2021. + /// + ShortDate = (byte)'d', + + /// + /// A long date. e.g. 18 June 2021. + /// + LongDate = (byte)'D', + + /// + /// A short date and time. e.g. 18 June 2021 03:50. + /// + ShortDateTime = (byte)'f', + + /// + /// A long date and time. e.g. Friday 18 June 2021 03:50. + /// + LongDateTime = (byte)'F', + + /// + /// A short time. e.g. 03:50. + /// + ShortTime = (byte)'t', + + /// + /// A long time. e.g. 03:50:15. + /// + LongTime = (byte)'T', + + /// + /// The time relative to the client. e.g. An hour ago. + /// + RelativeTime = (byte)'R' +} diff --git a/DSharpPlus/TokenType.cs b/DSharpPlus/TokenType.cs index 5087a7b210..12d84c2145 100644 --- a/DSharpPlus/TokenType.cs +++ b/DSharpPlus/TokenType.cs @@ -1,18 +1,18 @@ -namespace DSharpPlus; - - -/// -/// Token type -/// -public enum TokenType -{ - /// - /// Bot token type - /// - Bot = 1, - - /// - /// Bearer token type (used for oAuth) - /// - Bearer = 2 -} +namespace DSharpPlus; + + +/// +/// Token type +/// +public enum TokenType +{ + /// + /// Bot token type + /// + Bot = 1, + + /// + /// Bearer token type (used for oAuth) + /// + Bearer = 2 +} diff --git a/DSharpPlus/Utilities.cs b/DSharpPlus/Utilities.cs index 3603bf07ef..214d0207b7 100644 --- a/DSharpPlus/Utilities.cs +++ b/DSharpPlus/Utilities.cs @@ -1,302 +1,302 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using DSharpPlus.Entities; -using DSharpPlus.Net; -using Microsoft.Extensions.Logging; - -namespace DSharpPlus; - -/// -/// Various Discord-related utilities. -/// -public static partial class Utilities -{ - /// - /// Gets the version of the library - /// - private static string VersionHeader { get; set; } - - internal static UTF8Encoding UTF8 { get; } = new UTF8Encoding(false); - - static Utilities() - { - Assembly a = typeof(DiscordClient).GetTypeInfo().Assembly; - - string vs = ""; - AssemblyInformationalVersionAttribute? iv = a.GetCustomAttribute(); - if (iv != null) - { - vs = iv.InformationalVersion; - } - else - { - Version? v = a.GetName().Version; - vs = v.ToString(3); - } - - VersionHeader = $"DiscordBot (https://github.com/DSharpPlus/DSharpPlus, v{vs})"; - } - - internal static string GetApiBaseUri() - => Endpoints.BASE_URI; - - internal static Uri GetApiUriFor(string path) - => new($"{GetApiBaseUri()}{path}"); - - internal static Uri GetApiUriFor(string path, string queryString) - => new($"{GetApiBaseUri()}{path}{queryString}"); - - internal static QueryUriBuilder GetApiUriBuilderFor(string path) - => new($"{GetApiBaseUri()}{path}"); - - internal static string GetUserAgent() - => VersionHeader; - - internal static bool ContainsUserMentions(string message) - { - Regex regex = UserMentionRegex(); - return regex.IsMatch(message); - } - - internal static bool ContainsNicknameMentions(string message) - { - Regex regex = NicknameMentionRegex(); - return regex.IsMatch(message); - } - - internal static bool ContainsChannelMentions(string message) - { - Regex regex = ChannelMentionRegex(); - return regex.IsMatch(message); - } - - internal static bool ContainsRoleMentions(string message) - { - Regex regex = RoleMentionRegex(); - return regex.IsMatch(message); - } - - internal static bool ContainsEmojis(string message) - { - Regex regex = EmojiMentionRegex(); - return regex.IsMatch(message); - } - - internal static IEnumerable GetUserMentions(DiscordMessage message) - { - Regex regex = UserMentionRegex(); - MatchCollection matches = regex.Matches(message.Content); - foreach (Match match in matches.Cast()) - { - yield return ulong.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture); - } - } - - internal static IEnumerable GetRoleMentions(DiscordMessage message) - { - Regex regex = RoleMentionRegex(); - MatchCollection matches = regex.Matches(message.Content); - foreach (Match match in matches.Cast()) - { - yield return ulong.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture); - } - } - - internal static IEnumerable GetChannelMentions(DiscordMessage message) => GetChannelMentions(message.Content); - - internal static IEnumerable GetChannelMentions(string messageContent) - { - Regex regex = ChannelMentionRegex(); - MatchCollection matches = regex.Matches(messageContent); - foreach (Match match in matches.Cast()) - { - yield return ulong.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture); - } - } - - internal static IEnumerable GetEmojis(DiscordMessage message) - { - Regex regex = EmojiMentionRegex(); - MatchCollection matches = regex.Matches(message.Content); - foreach (Match match in matches.Cast()) - { - yield return ulong.Parse(match.Groups[2].Value, CultureInfo.InvariantCulture); - } - } - - internal static bool IsValidSlashCommandName(string name) - { - Regex regex = SlashCommandNameRegex(); - return regex.IsMatch(name); - } - - internal static bool HasMessageIntents(DiscordIntents intents) - => (intents.HasIntent(DiscordIntents.GuildMessages) && intents.HasIntent(DiscordIntents.MessageContents)) || intents.HasIntent(DiscordIntents.DirectMessages); - - internal static bool HasReactionIntents(DiscordIntents intents) - => intents.HasIntent(DiscordIntents.GuildMessageReactions) || intents.HasIntent(DiscordIntents.DirectMessageReactions); - - internal static bool HasTypingIntents(DiscordIntents intents) - => intents.HasIntent(DiscordIntents.GuildMessageTyping) || intents.HasIntent(DiscordIntents.DirectMessageTyping); - - internal static bool IsTextableChannel(DiscordChannel channel) - => channel.Type switch - { - DiscordChannelType.Text => true, - DiscordChannelType.Voice => true, - DiscordChannelType.Group => true, - DiscordChannelType.Private => true, - DiscordChannelType.PublicThread => true, - DiscordChannelType.PrivateThread => true, - DiscordChannelType.NewsThread => true, - DiscordChannelType.News => true, - DiscordChannelType.Stage => true, - _ => false, - }; - - // https://discord.com/developers/docs/topics/gateway#sharding-sharding-formula - /// - /// Gets a shard id from a guild id and total shard count. - /// - /// The guild id the shard is on. - /// The total amount of shards. - /// The shard id. - public static int GetShardId(ulong guildId, int shardCount) - => (int)((guildId >> 22) % (ulong)shardCount); - - /// - /// Helper method to create a from Unix time seconds for targets that do not support this natively. - /// - /// Unix time seconds to convert. - /// Whether the method should throw on failure. Defaults to true. - /// Calculated . - public static DateTimeOffset GetDateTimeOffset(long unixTime, bool shouldThrow = true) - { - try - { - return DateTimeOffset.FromUnixTimeSeconds(unixTime); - } - catch (Exception) - { - if (shouldThrow) - { - throw; - } - - return DateTimeOffset.MinValue; - } - } - - /// - /// Helper method to create a from Unix time milliseconds for targets that do not support this natively. - /// - /// Unix time milliseconds to convert. - /// Whether the method should throw on failure. Defaults to true. - /// Calculated . - public static DateTimeOffset GetDateTimeOffsetFromMilliseconds(long unixTime, bool shouldThrow = true) - { - try - { - return DateTimeOffset.FromUnixTimeMilliseconds(unixTime); - } - catch (Exception) - { - if (shouldThrow) - { - throw; - } - - return DateTimeOffset.MinValue; - } - } - - /// - /// Helper method to calculate Unix time seconds from a for targets that do not support this natively. - /// - /// to calculate Unix time for. - /// Calculated Unix time. - public static long GetUnixTime(DateTimeOffset dto) - => dto.ToUnixTimeMilliseconds(); - - /// - /// Computes a timestamp from a given snowflake. - /// - /// Snowflake to compute a timestamp from. - /// Computed timestamp. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static DateTimeOffset GetSnowflakeTime(this ulong snowflake) - => DiscordClient.discordEpoch.AddMilliseconds(snowflake >> 22); - - /// - /// Checks whether this string contains given characters. - /// - /// String to check. - /// Characters to check for. - /// Whether the string contained these characters. - public static bool Contains(this string str, params char[] characters) - { - foreach (char xc in str) - { - if (characters.Contains(xc)) - { - return true; - } - } - - return false; - } - - internal static void LogTaskFault(this Task task, ILogger logger, LogLevel level, EventId eventId, string message) - { - ArgumentNullException.ThrowIfNull(task); - if (logger == null) - { - return; - } - - task.ContinueWith(t => logger.Log(level, eventId, t.Exception, "{Message}", message), TaskContinuationOptions.OnlyOnFaulted); - } - - internal static void Deconstruct(this KeyValuePair kvp, out TKey key, out TValue value) - { - key = kvp.Key; - value = kvp.Value; - } - - /// - /// Creates a snowflake from a given . This can be used to provide "timestamps" for methods - /// like . - /// - /// DateTimeOffset to create a snowflake from. - /// Returns a snowflake representing the given date and time. - public static ulong CreateSnowflake(DateTimeOffset dateTimeOffset) - { - long diff = dateTimeOffset.ToUnixTimeMilliseconds() - DiscordClient.discordEpoch.ToUnixTimeMilliseconds(); - return (ulong)diff << 22; - } - - [GeneratedRegex("<@(\\d+)>", RegexOptions.ECMAScript)] - private static partial Regex UserMentionRegex(); - - [GeneratedRegex("<@!(\\d+)>", RegexOptions.ECMAScript)] - private static partial Regex NicknameMentionRegex(); - - [GeneratedRegex("<#(\\d+)>", RegexOptions.ECMAScript)] - private static partial Regex ChannelMentionRegex(); - - [GeneratedRegex("<@&(\\d+)>", RegexOptions.ECMAScript)] - private static partial Regex RoleMentionRegex(); - - [GeneratedRegex("", RegexOptions.ECMAScript)] - private static partial Regex EmojiMentionRegex(); - - [GeneratedRegex("^[\\w-]{1,32}$")] - private static partial Regex SlashCommandNameRegex(); -} +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using DSharpPlus.Entities; +using DSharpPlus.Net; +using Microsoft.Extensions.Logging; + +namespace DSharpPlus; + +/// +/// Various Discord-related utilities. +/// +public static partial class Utilities +{ + /// + /// Gets the version of the library + /// + private static string VersionHeader { get; set; } + + internal static UTF8Encoding UTF8 { get; } = new UTF8Encoding(false); + + static Utilities() + { + Assembly a = typeof(DiscordClient).GetTypeInfo().Assembly; + + string vs = ""; + AssemblyInformationalVersionAttribute? iv = a.GetCustomAttribute(); + if (iv != null) + { + vs = iv.InformationalVersion; + } + else + { + Version? v = a.GetName().Version; + vs = v.ToString(3); + } + + VersionHeader = $"DiscordBot (https://github.com/DSharpPlus/DSharpPlus, v{vs})"; + } + + internal static string GetApiBaseUri() + => Endpoints.BASE_URI; + + internal static Uri GetApiUriFor(string path) + => new($"{GetApiBaseUri()}{path}"); + + internal static Uri GetApiUriFor(string path, string queryString) + => new($"{GetApiBaseUri()}{path}{queryString}"); + + internal static QueryUriBuilder GetApiUriBuilderFor(string path) + => new($"{GetApiBaseUri()}{path}"); + + internal static string GetUserAgent() + => VersionHeader; + + internal static bool ContainsUserMentions(string message) + { + Regex regex = UserMentionRegex(); + return regex.IsMatch(message); + } + + internal static bool ContainsNicknameMentions(string message) + { + Regex regex = NicknameMentionRegex(); + return regex.IsMatch(message); + } + + internal static bool ContainsChannelMentions(string message) + { + Regex regex = ChannelMentionRegex(); + return regex.IsMatch(message); + } + + internal static bool ContainsRoleMentions(string message) + { + Regex regex = RoleMentionRegex(); + return regex.IsMatch(message); + } + + internal static bool ContainsEmojis(string message) + { + Regex regex = EmojiMentionRegex(); + return regex.IsMatch(message); + } + + internal static IEnumerable GetUserMentions(DiscordMessage message) + { + Regex regex = UserMentionRegex(); + MatchCollection matches = regex.Matches(message.Content); + foreach (Match match in matches.Cast()) + { + yield return ulong.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture); + } + } + + internal static IEnumerable GetRoleMentions(DiscordMessage message) + { + Regex regex = RoleMentionRegex(); + MatchCollection matches = regex.Matches(message.Content); + foreach (Match match in matches.Cast()) + { + yield return ulong.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture); + } + } + + internal static IEnumerable GetChannelMentions(DiscordMessage message) => GetChannelMentions(message.Content); + + internal static IEnumerable GetChannelMentions(string messageContent) + { + Regex regex = ChannelMentionRegex(); + MatchCollection matches = regex.Matches(messageContent); + foreach (Match match in matches.Cast()) + { + yield return ulong.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture); + } + } + + internal static IEnumerable GetEmojis(DiscordMessage message) + { + Regex regex = EmojiMentionRegex(); + MatchCollection matches = regex.Matches(message.Content); + foreach (Match match in matches.Cast()) + { + yield return ulong.Parse(match.Groups[2].Value, CultureInfo.InvariantCulture); + } + } + + internal static bool IsValidSlashCommandName(string name) + { + Regex regex = SlashCommandNameRegex(); + return regex.IsMatch(name); + } + + internal static bool HasMessageIntents(DiscordIntents intents) + => (intents.HasIntent(DiscordIntents.GuildMessages) && intents.HasIntent(DiscordIntents.MessageContents)) || intents.HasIntent(DiscordIntents.DirectMessages); + + internal static bool HasReactionIntents(DiscordIntents intents) + => intents.HasIntent(DiscordIntents.GuildMessageReactions) || intents.HasIntent(DiscordIntents.DirectMessageReactions); + + internal static bool HasTypingIntents(DiscordIntents intents) + => intents.HasIntent(DiscordIntents.GuildMessageTyping) || intents.HasIntent(DiscordIntents.DirectMessageTyping); + + internal static bool IsTextableChannel(DiscordChannel channel) + => channel.Type switch + { + DiscordChannelType.Text => true, + DiscordChannelType.Voice => true, + DiscordChannelType.Group => true, + DiscordChannelType.Private => true, + DiscordChannelType.PublicThread => true, + DiscordChannelType.PrivateThread => true, + DiscordChannelType.NewsThread => true, + DiscordChannelType.News => true, + DiscordChannelType.Stage => true, + _ => false, + }; + + // https://discord.com/developers/docs/topics/gateway#sharding-sharding-formula + /// + /// Gets a shard id from a guild id and total shard count. + /// + /// The guild id the shard is on. + /// The total amount of shards. + /// The shard id. + public static int GetShardId(ulong guildId, int shardCount) + => (int)((guildId >> 22) % (ulong)shardCount); + + /// + /// Helper method to create a from Unix time seconds for targets that do not support this natively. + /// + /// Unix time seconds to convert. + /// Whether the method should throw on failure. Defaults to true. + /// Calculated . + public static DateTimeOffset GetDateTimeOffset(long unixTime, bool shouldThrow = true) + { + try + { + return DateTimeOffset.FromUnixTimeSeconds(unixTime); + } + catch (Exception) + { + if (shouldThrow) + { + throw; + } + + return DateTimeOffset.MinValue; + } + } + + /// + /// Helper method to create a from Unix time milliseconds for targets that do not support this natively. + /// + /// Unix time milliseconds to convert. + /// Whether the method should throw on failure. Defaults to true. + /// Calculated . + public static DateTimeOffset GetDateTimeOffsetFromMilliseconds(long unixTime, bool shouldThrow = true) + { + try + { + return DateTimeOffset.FromUnixTimeMilliseconds(unixTime); + } + catch (Exception) + { + if (shouldThrow) + { + throw; + } + + return DateTimeOffset.MinValue; + } + } + + /// + /// Helper method to calculate Unix time seconds from a for targets that do not support this natively. + /// + /// to calculate Unix time for. + /// Calculated Unix time. + public static long GetUnixTime(DateTimeOffset dto) + => dto.ToUnixTimeMilliseconds(); + + /// + /// Computes a timestamp from a given snowflake. + /// + /// Snowflake to compute a timestamp from. + /// Computed timestamp. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static DateTimeOffset GetSnowflakeTime(this ulong snowflake) + => DiscordClient.discordEpoch.AddMilliseconds(snowflake >> 22); + + /// + /// Checks whether this string contains given characters. + /// + /// String to check. + /// Characters to check for. + /// Whether the string contained these characters. + public static bool Contains(this string str, params char[] characters) + { + foreach (char xc in str) + { + if (characters.Contains(xc)) + { + return true; + } + } + + return false; + } + + internal static void LogTaskFault(this Task task, ILogger logger, LogLevel level, EventId eventId, string message) + { + ArgumentNullException.ThrowIfNull(task); + if (logger == null) + { + return; + } + + task.ContinueWith(t => logger.Log(level, eventId, t.Exception, "{Message}", message), TaskContinuationOptions.OnlyOnFaulted); + } + + internal static void Deconstruct(this KeyValuePair kvp, out TKey key, out TValue value) + { + key = kvp.Key; + value = kvp.Value; + } + + /// + /// Creates a snowflake from a given . This can be used to provide "timestamps" for methods + /// like . + /// + /// DateTimeOffset to create a snowflake from. + /// Returns a snowflake representing the given date and time. + public static ulong CreateSnowflake(DateTimeOffset dateTimeOffset) + { + long diff = dateTimeOffset.ToUnixTimeMilliseconds() - DiscordClient.discordEpoch.ToUnixTimeMilliseconds(); + return (ulong)diff << 22; + } + + [GeneratedRegex("<@(\\d+)>", RegexOptions.ECMAScript)] + private static partial Regex UserMentionRegex(); + + [GeneratedRegex("<@!(\\d+)>", RegexOptions.ECMAScript)] + private static partial Regex NicknameMentionRegex(); + + [GeneratedRegex("<#(\\d+)>", RegexOptions.ECMAScript)] + private static partial Regex ChannelMentionRegex(); + + [GeneratedRegex("<@&(\\d+)>", RegexOptions.ECMAScript)] + private static partial Regex RoleMentionRegex(); + + [GeneratedRegex("", RegexOptions.ECMAScript)] + private static partial Regex EmojiMentionRegex(); + + [GeneratedRegex("^[\\w-]{1,32}$")] + private static partial Regex SlashCommandNameRegex(); +} diff --git a/Directory.Build.props b/Directory.Build.props index a18ac6ab04..e9ee6bbe2f 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,54 +1,54 @@ - - - - - $(CoreCompileDependsOn);_DisableAnalyzers - true - false - Latest - true - enable - Library - $([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildProjectDirectory), "DSharpPlus.sln")) - true - net9.0 - true - $(NoWarn);DSP0001;DSP0002;DSP0005 - CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8618;CS8619;CS8620;CS8625;CS8629;CS8633;CS8714;CS8764;CS8765;CS8767;NETSDK1188 - 5.0.0 - $(Version)-nightly-$(Nightly) - $(Version)-pr.$(PR) - $(Version)-alpha.$(Alpha) - - - - - DSharpPlus - DSharpPlus contributors - dsharpplus.png - LICENSE - https://github.com/DSharpPlus/DSharpPlus - README.md - discord, discord-api, bots, discord-bots, chat, dsharp, dsharpplus, csharp, dotnet, vb-net, fsharp - Git - https://github.com/DSharpPlus/DSharpPlus - true - snupkg - - - - - - - - - - - - - - - - - + + + + + $(CoreCompileDependsOn);_DisableAnalyzers + true + false + Latest + true + enable + Library + $([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildProjectDirectory), "DSharpPlus.sln")) + true + net9.0 + true + $(NoWarn);DSP0001;DSP0002;DSP0005 + CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8618;CS8619;CS8620;CS8625;CS8629;CS8633;CS8714;CS8764;CS8765;CS8767;NETSDK1188; NU5104 + 5.0.0 + $(Version)-nightly-$(Nightly) + $(Version)-pr.$(PR) + $(Version)-alpha.$(Alpha) + + + + + DSharpPlus + DSharpPlus contributors + dsharpplus.png + LICENSE + https://github.com/DSharpPlus/DSharpPlus + README.md + discord, discord-api, bots, discord-bots, chat, dsharp, dsharpplus, csharp, dotnet, vb-net, fsharp + Git + https://github.com/DSharpPlus/DSharpPlus + true + snupkg + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/articles/beyond_basics/permissions.md b/docs/articles/beyond_basics/permissions.md index b63b88ee93..fbc8b84b17 100644 --- a/docs/articles/beyond_basics/permissions.md +++ b/docs/articles/beyond_basics/permissions.md @@ -1,54 +1,54 @@ ---- -uid: articles.beyond_basics.permissions -title: Permissions ---- - -# Permissions - -DSharpPlus implements permissions with two types, `enum DiscordPermission` and `struct DiscordPermissions`, as well as three implementation types we'll talk about in this article. This serves to allow permissions to scale indefinitely, at no impact to you, the user. - -## The Difference between `DiscordPermission` and `DiscordPermissions` - -`DiscordPermission` is an enum expressing names for specific permissions. It is used for attributes, which generally take an array of `DiscordPermission`, as well as to communicate permissions back to you. Each `DiscordPermission` can represent exactly one permission, and only its name is guaranteed to be constant - you should not rely on their underlying values and should treat them as opaque. - -> [!WARNING] -> Since the underlying values may change at any time, performing any sort of math of them should be considered unsafe. - -`DiscordPermissions`, on the other hand, expresses a set of permissions wherein each permission is either granted or not granted. It is possible, safe, and encouraged to perform math on this type and this type only. It exposes a number of methods that account for all special behaviour involved with permissions: `HasPermission` will not only check for the specified permission, but also for Administrator. - -## Querying and Manipulating Permissions - -`DiscordPermissions` exposes three methods to query whether a permission is set: `HasPermission` if you want to find out about a single permission, `HasAnyPermission` and `HasAllPermissions` for querying groups. All of these methods will account for special permissions. If you wish to merely find out whether a specific flag is set, `HasFlag` is provided for advanced purposes. - -For editing what permissions are set, `Add`, `Remove` and `Toggle` are provided. Both of them provide overloads for both single permissions and groups of permissions, and additionally `Add` and `Remove` are also provided as operators `+` and `-`. These methods do not account for special behaviour, and as such, revoking a permission may not revoke an administrator's permissions to perform the associated action. - -While `Add` and `Remove` merely ensure that at the end of the operation, the specified permissions are added or removed from the set, `Toggle` will always modify the set by flipping the permission. If a permission was previously not granted, this operation will grant it and vice versa. - -Furthermore, DSharpPlus provides the bitwise operators `AND`, `OR`, `XOR` and `NOT` on permission sets. For the intents and purposes of these operators, each permission is a bit whose position is not guaranteed. It is not advisable to manually handle these operations instead of the above named, well-defined methods outside of advanced scenarios. - -## Enumerating and Printing Permissions - -DSharpPlus supports numerous formats for printing permissions. By default, and if no other understood format is specified, DSharpPlus will print an opaque integer representation of permissions that can be round-tripped using `BigInteger.Parse` and the constructor overload accepting a `BigInteger`. It is also the same integer representation used in bot invite links and similar, and also the same representation received from Discord. - -If `raw` is passed as a format specifier, DSharpPlus will print the underlying representation. This is mainly intended for debug purposes and not assumed to be useful to users. - -If `name` is passed as a format specifier, DSharpPlus will pretty-print the permissions according to no stable order by their English name, separated by commata. Undocumented but set flags will be replaced with their internal number. As a variant of this, format specifiers of the shape `name:custom` will pretty-print permissions by their English name according to the format defined as `custom`, where the provided format string will be copied verbatim and `{permission}` will be replaced by the english name. For example, the following code may result in the following: - -~~~cs -permissions.ToString("name: - {permission}\n"); - -// - Administrator -// - Send Messages -// - Send Text-to-speech Messages -// - 47 -~~~ - -Note that `{permission}` must be contained as a literal in the string and cannot be interpolated. - -If that does not suffice for your intents, you may also wish to build your own pretty-printer, or do something else entirely. To that end, the method `EnumeratePermissions` is provided, which provides an `IEnumerable` containing all set permissions for the given input set. You can use this as a building block for anything further. - -## Other Utilities - -Two permission sets may be compared using the `==` and `!=` operators, and permissions have a proper implementation of `GetHashCode` that makes them suitable for use in Dictionary keys and the likes. - -The `AllBitsSet` property returns a permission set with all possibly representible, which may be more than Discord supports, whereas `None` returns a permission set with no flags set. `All` returns a set with all permissions documented by Discord and implemented by DSharpPlus set. The values of `AllBitsSet` and `All` may change at any point in time: `AllBitsSet` whenever DSharpPlus changes the size or format of the underlying representation, and `All` whenever Discord adds new permissions that are subsequently implemented by DSharpPlus. They cannot be assumed to be constant. +--- +uid: articles.beyond_basics.permissions +title: Permissions +--- + +# Permissions + +DSharpPlus implements permissions with two types, `enum DiscordPermission` and `struct DiscordPermissions`, as well as three implementation types we'll talk about in this article. This serves to allow permissions to scale indefinitely, at no impact to you, the user. + +## The Difference between `DiscordPermission` and `DiscordPermissions` + +`DiscordPermission` is an enum expressing names for specific permissions. It is used for attributes, which generally take an array of `DiscordPermission`, as well as to communicate permissions back to you. Each `DiscordPermission` can represent exactly one permission, and only its name is guaranteed to be constant - you should not rely on their underlying values and should treat them as opaque. + +> [!WARNING] +> Since the underlying values may change at any time, performing any sort of math of them should be considered unsafe. + +`DiscordPermissions`, on the other hand, expresses a set of permissions wherein each permission is either granted or not granted. It is possible, safe, and encouraged to perform math on this type and this type only. It exposes a number of methods that account for all special behaviour involved with permissions: `HasPermission` will not only check for the specified permission, but also for Administrator. + +## Querying and Manipulating Permissions + +`DiscordPermissions` exposes three methods to query whether a permission is set: `HasPermission` if you want to find out about a single permission, `HasAnyPermission` and `HasAllPermissions` for querying groups. All of these methods will account for special permissions. If you wish to merely find out whether a specific flag is set, `HasFlag` is provided for advanced purposes. + +For editing what permissions are set, `Add`, `Remove` and `Toggle` are provided. Both of them provide overloads for both single permissions and groups of permissions, and additionally `Add` and `Remove` are also provided as operators `+` and `-`. These methods do not account for special behaviour, and as such, revoking a permission may not revoke an administrator's permissions to perform the associated action. + +While `Add` and `Remove` merely ensure that at the end of the operation, the specified permissions are added or removed from the set, `Toggle` will always modify the set by flipping the permission. If a permission was previously not granted, this operation will grant it and vice versa. + +Furthermore, DSharpPlus provides the bitwise operators `AND`, `OR`, `XOR` and `NOT` on permission sets. For the intents and purposes of these operators, each permission is a bit whose position is not guaranteed. It is not advisable to manually handle these operations instead of the above named, well-defined methods outside of advanced scenarios. + +## Enumerating and Printing Permissions + +DSharpPlus supports numerous formats for printing permissions. By default, and if no other understood format is specified, DSharpPlus will print an opaque integer representation of permissions that can be round-tripped using `BigInteger.Parse` and the constructor overload accepting a `BigInteger`. It is also the same integer representation used in bot invite links and similar, and also the same representation received from Discord. + +If `raw` is passed as a format specifier, DSharpPlus will print the underlying representation. This is mainly intended for debug purposes and not assumed to be useful to users. + +If `name` is passed as a format specifier, DSharpPlus will pretty-print the permissions according to no stable order by their English name, separated by commata. Undocumented but set flags will be replaced with their internal number. As a variant of this, format specifiers of the shape `name:custom` will pretty-print permissions by their English name according to the format defined as `custom`, where the provided format string will be copied verbatim and `{permission}` will be replaced by the english name. For example, the following code may result in the following: + +~~~cs +permissions.ToString("name: - {permission}\n"); + +// - Administrator +// - Send Messages +// - Send Text-to-speech Messages +// - 47 +~~~ + +Note that `{permission}` must be contained as a literal in the string and cannot be interpolated. + +If that does not suffice for your intents, you may also wish to build your own pretty-printer, or do something else entirely. To that end, the method `EnumeratePermissions` is provided, which provides an `IEnumerable` containing all set permissions for the given input set. You can use this as a building block for anything further. + +## Other Utilities + +Two permission sets may be compared using the `==` and `!=` operators, and permissions have a proper implementation of `GetHashCode` that makes them suitable for use in Dictionary keys and the likes. + +The `AllBitsSet` property returns a permission set with all possibly representible, which may be more than Discord supports, whereas `None` returns a permission set with no flags set. `All` returns a set with all permissions documented by Discord and implemented by DSharpPlus set. The values of `AllBitsSet` and `All` may change at any point in time: `AllBitsSet` whenever DSharpPlus changes the size or format of the underlying representation, and `All` whenever Discord adds new permissions that are subsequently implemented by DSharpPlus. They cannot be assumed to be constant. diff --git a/docs/articles/commands/custom_context_checks.md b/docs/articles/commands/custom_context_checks.md index 7d89ec632a..c837619496 100644 --- a/docs/articles/commands/custom_context_checks.md +++ b/docs/articles/commands/custom_context_checks.md @@ -1,160 +1,160 @@ ---- -uid: articles.commands.custom_context_checks -title: Custom Context Checks ---- - -# Custom Context Checks -Context checks are safeguards to a command that will help it to execute successfully. Context checks like `RequireGuild` or `RequirePermissions` will cause the command not to execute if the user runs the command in a DM or if the user/bot does not have the required permissions. Occasionally, you may want to create your own context checks to ensure that a command can only be executed under certain conditions. - -A context check contains two important pieces: -- The attribute that will be applied to the command. This contains parameters that will be passed to the executing check. -- The check itself. This is the method that determines if the command can be executed. - -## Implementing a context check attribute -Any context check needs an attribute associated with it. This attribute will be applied to your command methods and needs to inherit from `ContextCheckAttribute`. It should contain the necessary metadata your check needs to determine whether or not to execute the command. For the purposes of this article, we'll create the following attribute: - -```cs -public class DirectMessageUsageAttribute : ContextCheckAttribute -{ - public DirectMessageUsage Usage { get; init; } - - public DirectMessageUsageAttribute(DirectMessageUsage usage = DirectMessageUsage.Allow) => Usage = usage; -} -``` - -## Implementing the context check -Now we're going to implement the logic which checks if the command is allowed to be executed. The `IContextCheck` interface is used to define the check method. The `T` in `IContextCheck` is the attribute that was applied to the command. In this case, it's the `DirectMessageUsageAttribute`, but it can be any check attribute - if desired, the same attribute can invoke multiple different context checks at once by implementing multiple `IContextCheck` interfaces. - -If the check was successful, the method should return `null`. If it was unsuccessful, the method should return a string that will then be provided to `CommandsExtension.CommandErrored`. - -```cs -public class DirectMessageUsageCheck : IContextCheck -{ - public ValueTask ExecuteCheckAsync(DirectMessageUsageAttribute attribute, CommandContext context) - { - // When the command is sent via DM and the attribute allows DMs, allow the command to be executed. - if (context.Channel.IsPrivate && attribute.Usage is not DirectMessageUsage.DenyDMs) - { - return ValueTask.FromResult(null); - } - // When the command is sent outside of DM and the attribute allows non-DMs, allow the command to be executed. - else if (!context.Channel.IsPrivate && attribute.Usage is not DirectMessageUsage.RequireDMs) - { - return ValueTask.FromResult(null); - } - // The command was sent via DM but the attribute denies DMs - // The command was sent outside of DM but the attribute requires DMs. - else - { - string dmStatus = context.Channel.IsPrivate ? "inside a DM" : "outside a DM"; - string requirement = attribute.Usage switch - { - DirectMessageUsage.DenyDMs => "denies DM usage", - DirectMessageUsage.RequireDMs => "requires DM usage", - _ => throw new NotImplementedException($"A new DirectMessageUsage value was added and not implemented in the {nameof(DirectMessageUsageCheck)}: {attribute.Usage}"), - }; - - return ValueTask.FromResult($"The executed command {requirement} but was executed {dmStatus}."); - } - } -} -``` - -> [!WARNING] -> Your check may inspect the command context to get more information, but you should be careful making any API calls, especially such that may alter state such as `RespondAsync`. This is an easy source of bugs, and you should be aware of the three-second limit for initial responses to interactions. - -Now, for the most important part, we need to register the check: - -```cs -commandsExtension.AddCheck(); -``` - -Then we use the check like such: - -```cs -[Command("dm")] -[DirectMessageUsage(DirectMessageUsage.RequireDMs)] -public async ValueTask RequireDMs(CommandContext commandContext) => - await commandContext.RespondAsync("This command was executed in a DM!"); -``` - -## Parameter Checks - -DSharpPlus.Commands also supports checks that target specifically one parameter. They are supplied with the present value and the metadata the extension has about the parameter, such as its default value or attributes. To implement a parameter check for your own parameter: -- Create an attribute that inherits from `ParameterCheckAttribute`. -- Have it implement `IParameterCheck`. -- Register your parameter check using `CommandsExtension.AddParameterCheck()`. -- Apply the attribute to your parameter. - -> [!NOTE] -> You will be supplied an `object` for the parameter value. It is your responsibility to ensure the type matches what your check expects, and to either ignore or error on incorrect types. - -For example, we can make a check that ensures a string is no longer than X characters. First, we create our attribute, as above: - -```cs -using DSharpPlus.Commands.ContextChecks.ParameterChecks; - -public sealed class MaximumStringLengthAttribute : ParameterCheckAttribute -{ - public int MaximumLength { get; private set; } - - public MaximumStringLengthAttribute(int length) => MaximumLength = length; -} -``` - -Then, we will be creating our check: - -```cs -using DSharpPlus.Commands.ContextChecks.ParameterChecks; - -public sealed class MaximumStringLengthCheck : IParameterCheck -{ - public ValueTask ExecuteCheckAsync(MaximumStringLengthAttribute attribute, ParameterCheckInfo info, CommandContext context) - { - if (info.Value is not string str) - { - return ValueTask.FromResult("The provided parameter was not a string."); - } - else if (str.Length >= attribute.MaximumLength) - { - return ValueTask.FromResult("The string exceeded the length limit."); - } - - return ValueTask.FromResult(null); - } -} -``` - -We then register it like so: - -```cs -commandsExtension.AddParameterCheck(); -``` - -And then apply it to our parameter: - -```cs -[Command("say")] -public static async ValueTask SayAsync(CommandContext commandContext, [MaximumStringLength(2000)] string text) => - await commandContext.RespondAsync(text); -``` - -## Advanced Features - -The classes you use to implement checks participate in dependency injection, and you can request any type you previously supplied to the service provider in a public constructor. Useful applications include, but are not limited to, logging or tracking how often a command executes. - -A single check class can also implement multiple checks, like so: - -```cs -public class Check : IContextCheck, IContextCheck; -``` - -or even multiple different kinds of checks, like so: - -```cs -public class Check : IContextCheck, IParameterCheck; -``` - -This means that all other code in that class can be shared between the two check methods, but this should be used with caution - since checks are registered per type, you lose granularity over which checks should be executed; and it means the same construction ceremony will run for both checks. - +--- +uid: articles.commands.custom_context_checks +title: Custom Context Checks +--- + +# Custom Context Checks +Context checks are safeguards to a command that will help it to execute successfully. Context checks like `RequireGuild` or `RequirePermissions` will cause the command not to execute if the user runs the command in a DM or if the user/bot does not have the required permissions. Occasionally, you may want to create your own context checks to ensure that a command can only be executed under certain conditions. + +A context check contains two important pieces: +- The attribute that will be applied to the command. This contains parameters that will be passed to the executing check. +- The check itself. This is the method that determines if the command can be executed. + +## Implementing a context check attribute +Any context check needs an attribute associated with it. This attribute will be applied to your command methods and needs to inherit from `ContextCheckAttribute`. It should contain the necessary metadata your check needs to determine whether or not to execute the command. For the purposes of this article, we'll create the following attribute: + +```cs +public class DirectMessageUsageAttribute : ContextCheckAttribute +{ + public DirectMessageUsage Usage { get; init; } + + public DirectMessageUsageAttribute(DirectMessageUsage usage = DirectMessageUsage.Allow) => Usage = usage; +} +``` + +## Implementing the context check +Now we're going to implement the logic which checks if the command is allowed to be executed. The `IContextCheck` interface is used to define the check method. The `T` in `IContextCheck` is the attribute that was applied to the command. In this case, it's the `DirectMessageUsageAttribute`, but it can be any check attribute - if desired, the same attribute can invoke multiple different context checks at once by implementing multiple `IContextCheck` interfaces. + +If the check was successful, the method should return `null`. If it was unsuccessful, the method should return a string that will then be provided to `CommandsExtension.CommandErrored`. + +```cs +public class DirectMessageUsageCheck : IContextCheck +{ + public ValueTask ExecuteCheckAsync(DirectMessageUsageAttribute attribute, CommandContext context) + { + // When the command is sent via DM and the attribute allows DMs, allow the command to be executed. + if (context.Channel.IsPrivate && attribute.Usage is not DirectMessageUsage.DenyDMs) + { + return ValueTask.FromResult(null); + } + // When the command is sent outside of DM and the attribute allows non-DMs, allow the command to be executed. + else if (!context.Channel.IsPrivate && attribute.Usage is not DirectMessageUsage.RequireDMs) + { + return ValueTask.FromResult(null); + } + // The command was sent via DM but the attribute denies DMs + // The command was sent outside of DM but the attribute requires DMs. + else + { + string dmStatus = context.Channel.IsPrivate ? "inside a DM" : "outside a DM"; + string requirement = attribute.Usage switch + { + DirectMessageUsage.DenyDMs => "denies DM usage", + DirectMessageUsage.RequireDMs => "requires DM usage", + _ => throw new NotImplementedException($"A new DirectMessageUsage value was added and not implemented in the {nameof(DirectMessageUsageCheck)}: {attribute.Usage}"), + }; + + return ValueTask.FromResult($"The executed command {requirement} but was executed {dmStatus}."); + } + } +} +``` + +> [!WARNING] +> Your check may inspect the command context to get more information, but you should be careful making any API calls, especially such that may alter state such as `RespondAsync`. This is an easy source of bugs, and you should be aware of the three-second limit for initial responses to interactions. + +Now, for the most important part, we need to register the check: + +```cs +commandsExtension.AddCheck(); +``` + +Then we use the check like such: + +```cs +[Command("dm")] +[DirectMessageUsage(DirectMessageUsage.RequireDMs)] +public async ValueTask RequireDMs(CommandContext commandContext) => + await commandContext.RespondAsync("This command was executed in a DM!"); +``` + +## Parameter Checks + +DSharpPlus.Commands also supports checks that target specifically one parameter. They are supplied with the present value and the metadata the extension has about the parameter, such as its default value or attributes. To implement a parameter check for your own parameter: +- Create an attribute that inherits from `ParameterCheckAttribute`. +- Have it implement `IParameterCheck`. +- Register your parameter check using `CommandsExtension.AddParameterCheck()`. +- Apply the attribute to your parameter. + +> [!NOTE] +> You will be supplied an `object` for the parameter value. It is your responsibility to ensure the type matches what your check expects, and to either ignore or error on incorrect types. + +For example, we can make a check that ensures a string is no longer than X characters. First, we create our attribute, as above: + +```cs +using DSharpPlus.Commands.ContextChecks.ParameterChecks; + +public sealed class MaximumStringLengthAttribute : ParameterCheckAttribute +{ + public int MaximumLength { get; private set; } + + public MaximumStringLengthAttribute(int length) => MaximumLength = length; +} +``` + +Then, we will be creating our check: + +```cs +using DSharpPlus.Commands.ContextChecks.ParameterChecks; + +public sealed class MaximumStringLengthCheck : IParameterCheck +{ + public ValueTask ExecuteCheckAsync(MaximumStringLengthAttribute attribute, ParameterCheckInfo info, CommandContext context) + { + if (info.Value is not string str) + { + return ValueTask.FromResult("The provided parameter was not a string."); + } + else if (str.Length >= attribute.MaximumLength) + { + return ValueTask.FromResult("The string exceeded the length limit."); + } + + return ValueTask.FromResult(null); + } +} +``` + +We then register it like so: + +```cs +commandsExtension.AddParameterCheck(); +``` + +And then apply it to our parameter: + +```cs +[Command("say")] +public static async ValueTask SayAsync(CommandContext commandContext, [MaximumStringLength(2000)] string text) => + await commandContext.RespondAsync(text); +``` + +## Advanced Features + +The classes you use to implement checks participate in dependency injection, and you can request any type you previously supplied to the service provider in a public constructor. Useful applications include, but are not limited to, logging or tracking how often a command executes. + +A single check class can also implement multiple checks, like so: + +```cs +public class Check : IContextCheck, IContextCheck; +``` + +or even multiple different kinds of checks, like so: + +```cs +public class Check : IContextCheck, IParameterCheck; +``` + +This means that all other code in that class can be shared between the two check methods, but this should be used with caution - since checks are registered per type, you lose granularity over which checks should be executed; and it means the same construction ceremony will run for both checks. + There is no limit on how many different checks can reference the same attribute, they will all be supplied with that attribute. Checks targeting `UnconditionalCheckAttribute` will always be executed, regardless of whether the attribute is applied or not. Unconditional context checks are not available for parameter checks. \ No newline at end of file diff --git a/docs/articles/commands/processors/slash_commands/choice_provider_vs_autocomplete.md b/docs/articles/commands/processors/slash_commands/choice_provider_vs_autocomplete.md index a79f6e8c6f..82f3544a42 100644 --- a/docs/articles/commands/processors/slash_commands/choice_provider_vs_autocomplete.md +++ b/docs/articles/commands/processors/slash_commands/choice_provider_vs_autocomplete.md @@ -1,145 +1,145 @@ ---- -uid: articles.commands.command_processors.slash_commands.choice_provider_vs_autocomplete -title: Choice Provider vs Auto-complete ---- - -# Choice Provider vs Auto-complete - -What's a choice provider? How is it different from auto-complete? When should you use one over the other? - -## Choice Providers -Discord provides a special feature to slash command options called "choices." Choices are a list of options that the user can select from. The user can select **only** from these choices - as in only those choices are valid - which differs from auto-complete. These choices must be known and provided on startup as they're used when registering the slash command. This means that you can't dynamically change the choices at runtime. - -![A Discord screenshot of the `lock` command providing only two choices. The first choice is `Send Messages`, while the second choice is `View Channel`.](../../../../images/commands_choice_provider_example.png) - -> [!NOTE] -> The user **must** choose between **only** those two options. If the user tries to select something else, Discord will prevent the command from running. - -> [!WARNING] -> A choice provider may only provide 25 choices. If you have more than 25 choices, you should use auto-complete. - -## Auto-complete -Auto-complete, on the other hand, is a feature that allows the user to type in a value and Discord will return a list of suggestions retrieved from your bot. The user can select from the list of suggestions or continue typing. This is useful when you have a large number of options or when the options are dynamic and can change at runtime. - -![A Discord screenshot of the `tag get` command. As the user types, the list of tags changes.](../../../../images/commands_autocomplete_example.png) - -As the user types in the text, Discord will send a request to your bot to get the list of auto-complete suggestions. The user can then select from the list of suggestions or continue typing whatever they want. - -> [!WARNING] -> The user **is not required** to choose from the the suggestions provided. They can send any value they want, and it's up to your bot to handle the value. - -## Which one should I use? -If you have a small, fixed list of options, use a choice provider. If you have a large list of options or the list of options can change at runtime, use auto-complete. - -Some valid use-cases for choice providers include: -- Small Enums (Built-In support!) -- Media types (e.g. `image`, `video`, `audio`) -- The day of the week - -Some valid use-cases for auto-complete include: -- Tag names -- A Google search -- Very large enums (e.g. all the countries in the world. Also built-in support!) - -Both choice providers and auto-complete support dependency injection through the constructor. - -## Implementing a Choice Provider - -Our class will implement from the `IChoiceProvider` interface. This interface has a single method: `ValueTask> ProvideAsync(CommandParameter parameter)`. This method is only called once per command parameter on startup. - -```cs -public class DaysOfTheWeekProvider : IChoiceProvider -{ - private static readonly IReadOnlyList daysOfTheWeek = - [ - new DiscordApplicationCommandOptionChoice("Sunday", 0), - new DiscordApplicationCommandOptionChoice("Monday", 1), - new DiscordApplicationCommandOptionChoice("Tuesday", 2), - new DiscordApplicationCommandOptionChoice("Wednesday", 3), - new DiscordApplicationCommandOptionChoice("Thursday", 4), - new DiscordApplicationCommandOptionChoice("Friday", 5), - new DiscordApplicationCommandOptionChoice("Saturday", 6), - ]; - - public ValueTask> ProvideAsync(CommandParameter parameter) => - ValueTask.FromResult(daysOfTheWeek); -} -``` - -And now we apply this choice provider to a command parameter: - -```cs -public class ScheduleCommand -{ - public async ValueTask ExecuteAsync(CommandContext context, [SlashChoiceProvider] int day) - { - // ... - } -} -``` - -## Implementing Auto-Complete - -Auto-complete is very similar in design to choice providers. Our class will implement the `IAutoCompleteProvider` interface. This interface has a single method: `ValueTask> AutoCompleteAsync(AutoCompleteContext context)`. This method will be called everytime the `DiscordClient.InteractionCreated` is invoked with a `ApplicationCommandType` of `AutoCompleteRequest`. - -```cs -public class TagNameAutoCompleteProvider : IAutoCompleteProvider -{ - private readonly ITagService tagService; - - public TagNameAutoCompleteProvider(ITagService tagService) => tagService = tagService; - - public ValueTask> AutoCompleteAsync(AutoCompleteContext context); - { - var tags = tagService - .GetTags() - .Where(x => x.Name.StartsWith(context.UserInput, StringComparison.OrdinalIgnoreCase)) - .ToDictionary(x => x.Name, x => x.Id); - - return ValueTask.FromResult(tags); - } -} -``` - -And now we apply this auto-complete provider to a command parameter: - -```cs -public class TagCommand -{ - public async ValueTask ExecuteAsync(CommandContext context, [SlashAutoCompleteProvider] string tagName) - { - // ... - } -} -``` - -### Simple Auto-Complete -For simple lists of options, the `SimpleAutoCompleteProvder` class can be derived. This simplifies the process by just asking the developer to have a list of all the choices instead of creating the filtered results list directly. - -As an example, you could read a list of supported voice languages from a file and use that to auto-complete the language option of a voice list command. - -First the auto-complete provider: - -```cs -public class LanguageAutoCompleteProvider : SimpleAutoCompleteProvider -{ - static DiscordAutoCompleteChoice[] LanguageList = [ .. File.ReadAllLines("data/languages.txt").Select(l => l.Split(' ', 2)).Select(p => new DiscordAutoCompleteChoice(p[1], p[0])) ]; - protected override IEnumerable Choices => LanguageList; - protected override bool AllowDuplicateValues => false; -} -``` - -And then tag the command parameter in the same way as before: - -```cs -public static class ListCommands -{ - public static async Task VoiceListCommand(SlashCommandContext ctx, [SlashAutoCompleteProvider] string language) - { - // ... - } -} -``` - -> [!NOTE] +--- +uid: articles.commands.command_processors.slash_commands.choice_provider_vs_autocomplete +title: Choice Provider vs Auto-complete +--- + +# Choice Provider vs Auto-complete + +What's a choice provider? How is it different from auto-complete? When should you use one over the other? + +## Choice Providers +Discord provides a special feature to slash command options called "choices." Choices are a list of options that the user can select from. The user can select **only** from these choices - as in only those choices are valid - which differs from auto-complete. These choices must be known and provided on startup as they're used when registering the slash command. This means that you can't dynamically change the choices at runtime. + +![A Discord screenshot of the `lock` command providing only two choices. The first choice is `Send Messages`, while the second choice is `View Channel`.](../../../../images/commands_choice_provider_example.png) + +> [!NOTE] +> The user **must** choose between **only** those two options. If the user tries to select something else, Discord will prevent the command from running. + +> [!WARNING] +> A choice provider may only provide 25 choices. If you have more than 25 choices, you should use auto-complete. + +## Auto-complete +Auto-complete, on the other hand, is a feature that allows the user to type in a value and Discord will return a list of suggestions retrieved from your bot. The user can select from the list of suggestions or continue typing. This is useful when you have a large number of options or when the options are dynamic and can change at runtime. + +![A Discord screenshot of the `tag get` command. As the user types, the list of tags changes.](../../../../images/commands_autocomplete_example.png) + +As the user types in the text, Discord will send a request to your bot to get the list of auto-complete suggestions. The user can then select from the list of suggestions or continue typing whatever they want. + +> [!WARNING] +> The user **is not required** to choose from the the suggestions provided. They can send any value they want, and it's up to your bot to handle the value. + +## Which one should I use? +If you have a small, fixed list of options, use a choice provider. If you have a large list of options or the list of options can change at runtime, use auto-complete. + +Some valid use-cases for choice providers include: +- Small Enums (Built-In support!) +- Media types (e.g. `image`, `video`, `audio`) +- The day of the week + +Some valid use-cases for auto-complete include: +- Tag names +- A Google search +- Very large enums (e.g. all the countries in the world. Also built-in support!) + +Both choice providers and auto-complete support dependency injection through the constructor. + +## Implementing a Choice Provider + +Our class will implement from the `IChoiceProvider` interface. This interface has a single method: `ValueTask> ProvideAsync(CommandParameter parameter)`. This method is only called once per command parameter on startup. + +```cs +public class DaysOfTheWeekProvider : IChoiceProvider +{ + private static readonly IReadOnlyList daysOfTheWeek = + [ + new DiscordApplicationCommandOptionChoice("Sunday", 0), + new DiscordApplicationCommandOptionChoice("Monday", 1), + new DiscordApplicationCommandOptionChoice("Tuesday", 2), + new DiscordApplicationCommandOptionChoice("Wednesday", 3), + new DiscordApplicationCommandOptionChoice("Thursday", 4), + new DiscordApplicationCommandOptionChoice("Friday", 5), + new DiscordApplicationCommandOptionChoice("Saturday", 6), + ]; + + public ValueTask> ProvideAsync(CommandParameter parameter) => + ValueTask.FromResult(daysOfTheWeek); +} +``` + +And now we apply this choice provider to a command parameter: + +```cs +public class ScheduleCommand +{ + public async ValueTask ExecuteAsync(CommandContext context, [SlashChoiceProvider] int day) + { + // ... + } +} +``` + +## Implementing Auto-Complete + +Auto-complete is very similar in design to choice providers. Our class will implement the `IAutoCompleteProvider` interface. This interface has a single method: `ValueTask> AutoCompleteAsync(AutoCompleteContext context)`. This method will be called everytime the `DiscordClient.InteractionCreated` is invoked with a `ApplicationCommandType` of `AutoCompleteRequest`. + +```cs +public class TagNameAutoCompleteProvider : IAutoCompleteProvider +{ + private readonly ITagService tagService; + + public TagNameAutoCompleteProvider(ITagService tagService) => tagService = tagService; + + public ValueTask> AutoCompleteAsync(AutoCompleteContext context); + { + var tags = tagService + .GetTags() + .Where(x => x.Name.StartsWith(context.UserInput, StringComparison.OrdinalIgnoreCase)) + .ToDictionary(x => x.Name, x => x.Id); + + return ValueTask.FromResult(tags); + } +} +``` + +And now we apply this auto-complete provider to a command parameter: + +```cs +public class TagCommand +{ + public async ValueTask ExecuteAsync(CommandContext context, [SlashAutoCompleteProvider] string tagName) + { + // ... + } +} +``` + +### Simple Auto-Complete +For simple lists of options, the `SimpleAutoCompleteProvder` class can be derived. This simplifies the process by just asking the developer to have a list of all the choices instead of creating the filtered results list directly. + +As an example, you could read a list of supported voice languages from a file and use that to auto-complete the language option of a voice list command. + +First the auto-complete provider: + +```cs +public class LanguageAutoCompleteProvider : SimpleAutoCompleteProvider +{ + static DiscordAutoCompleteChoice[] LanguageList = [ .. File.ReadAllLines("data/languages.txt").Select(l => l.Split(' ', 2)).Select(p => new DiscordAutoCompleteChoice(p[1], p[0])) ]; + protected override IEnumerable Choices => LanguageList; + protected override bool AllowDuplicateValues => false; +} +``` + +And then tag the command parameter in the same way as before: + +```cs +public static class ListCommands +{ + public static async Task VoiceListCommand(SlashCommandContext ctx, [SlashAutoCompleteProvider] string language) + { + // ... + } +} +``` + +> [!NOTE] > For performance reasons, consider making `Choices` read from a static array, as in the example above. If `Choices` reads the file directly, that will happen on every auto-complete request. \ No newline at end of file diff --git a/docs/articles/commands/processors/slash_commands/missing_commands.md b/docs/articles/commands/processors/slash_commands/missing_commands.md index cec1c915bf..ef2e3b9bc8 100644 --- a/docs/articles/commands/processors/slash_commands/missing_commands.md +++ b/docs/articles/commands/processors/slash_commands/missing_commands.md @@ -1,16 +1,16 @@ ---- -uid: articles.commands.command_processors.slash_commands.missing_commands -title: Missing Commands ---- - -# Missing Commands - -#### Help! I registered all my slash commands but they aren't showing up! - -When the Discord App (the client you use, not the bot) starts up, it fetches all the commands that are registered with each bot and caches them to the current Discord channel. This means that if you register a command while the Discord App is running, you won't see the command until you restart the Discord App (`Ctrl + R`). - -#### Help! They're still not showing up! - -Some slash commands may be missing if they don't follow the requirements that Discord has set. First and foremost, always check your logs for errors. If a command parameter doesn't have a type converter, has a name/description that's too long or other miscellaneous issues, the Commands framework will avoid registering that specific command and print an error into the console. - +--- +uid: articles.commands.command_processors.slash_commands.missing_commands +title: Missing Commands +--- + +# Missing Commands + +#### Help! I registered all my slash commands but they aren't showing up! + +When the Discord App (the client you use, not the bot) starts up, it fetches all the commands that are registered with each bot and caches them to the current Discord channel. This means that if you register a command while the Discord App is running, you won't see the command until you restart the Discord App (`Ctrl + R`). + +#### Help! They're still not showing up! + +Some slash commands may be missing if they don't follow the requirements that Discord has set. First and foremost, always check your logs for errors. If a command parameter doesn't have a type converter, has a name/description that's too long or other miscellaneous issues, the Commands framework will avoid registering that specific command and print an error into the console. + There should never be a case when a command is silently skipped. If you're experiencing this issue, double check that the command is being registered correctly and that there are no errors in the logs. If you're still having trouble, feel free to open up a GitHub issue or a help post in the [Discord server](https://discord.gg/dsharpplus). \ No newline at end of file diff --git a/docs/articles/toc.yml b/docs/articles/toc.yml index 05f99d85b3..9f7f44b36d 100644 --- a/docs/articles/toc.yml +++ b/docs/articles/toc.yml @@ -1,143 +1,143 @@ -- name: Preamble - topicUid: articles.preamble -- name: The Basics - items: - - name: Creating a Bot Account - topicUid: articles.basics.bot_account - - name: Writing Your First Bot - topicUid: articles.basics.first_bot -- name: Beyond Basics - items: - - name: Events - topicUid: articles.beyond_basics.events - - name: Logging - topicUid: articles.beyond_basics.logging.default - items: - - name: The Default Logger - topicUid: articles.beyond_basics.logging.default - - name: Third Party Loggers - topicUid: articles.beyond_basics.logging.third_party - - name: Intents - topicUid: articles.beyond_basics.intents - - name: Sharding - topicUid: articles.beyond_basics.sharding - - name: Message Builder - topicUid: articles.beyond_basics.messagebuilder - - name: Interactions - topicUid: articles.beyond_basics.interactions - - name: Permissions - topicUid: articles.beyond_basics.permissions -- name: Commands - items: - - name: Introduction - topicUid: articles.commands.introduction - - name: Custom Context Checks - topicUid: articles.commands.custom_context_checks - - name: Custom Error Handler - topicUid: articles.commands.custom_error_handler - - name: Variadic Parameters - topicUid: articles.commands.variadic_parameters - - name: Argument Converters - items: - - name: Built-In Converters - topicUid: articles.commands.converters.built_in_converters - - name: Manually Invoking Converters - topicUid: articles.commands.converters.manually_invoking_converters - - name: Custom Argument Converters - topicUid: articles.commands.converters.custom_argument_converters - - name: Command Processors - items: - - name: Introduction - topicUid: articles.commands.command_processors.introduction - - name: Text Commands - items: - - name: Command Aliases - topicUid: articles.commands.command_processors.text_commands.command_aliases - - name: Custom Prefix Handler - topicUid: articles.commands.command_processors.text_commands.custom_prefix_handler - - name: Remaining Text - topicUid: articles.commands.command_processors.text_commands.remaining_text - - name: Slash Commands - items: - - name: Choice Providers vs AutoComplete - topicUid: articles.commands.command_processors.slash_commands.choice_provider_vs_autocomplete - - name: Missing Commands - topicUid: articles.commands.command_processors.slash_commands.missing_commands - - name: Naming Policies - topicUid: articles.commands.command_processors.slash_commands.naming_policies - - name: Localizing Interactions - topicUid: articles.commands.command_processors.slash_commands.localizing_interactions -- name: CommandsNext - items: - - name: Introduction - topicUid: articles.commands_next.intro - - name: Command Attributes - topicUid: articles.commands_next.command_attributes - - name: Dependency Injection - topicUid: articles.commands_next.dependency_injection - - name: Customization - items: - - name: Help Formatter - topicUid: articles.commands_next.help_formatter - - name: Argument Converters - topicUid: articles.commands_next.argument_converters - - name: Command Handler - topicUid: articles.commands_next.command_handler -- name: Audio - items: - - name: Lavalink - items: - - name: Setup - topicUid: articles.audio.lavalink.setup - - name: Configuration - topicUid: articles.audio.lavalink.configuration - - name: Music Commands - topicUid: articles.audio.lavalink.music_commands - - name: VoiceNext - items: - - name: Prerequisites - topicUid: articles.audio.voicenext.prerequisites - - name: Transmitting - topicUid: articles.audio.voicenext.transmit - - name: Receiving - topicUid: articles.audio.voicenext.receive -- name: Interactivity - topicUid: articles.interactivity -- name: Slash Commands - topicUid: articles.slash_commands -- name: Advanced Topics - items: - - name: Buttons - topicUid: articles.advanced_topics.buttons - - name: Select Menus - topicUid: articles.advanced_topics.selects - - name: Generic Host - topicUid: articles.advanced_topics.generic_host - - name: Metrics and Profiling - topicUid: articles.advanced_topics.metrics_profiling - - name: Trace Logging - topicUid: articles.advanced_topics.trace - - name: Gateway Compression - topicUid: articles.advanced_topics.compression -- name: Hosting - topicUid: articles.hosting -- name: Version Migration - items: - - name: DiscordSharp to DSharpPlus - topicUid: articles.migration.dsharp - - name: 2.x to 3.x - topicUid: articles.migration.2x_to_3x - - name: 3.x to 4.x - topicUid: articles.migration.3x_to_4x - - name: 4.x to 5.x - topicUid: articles.migration.4x_to_5x - - name: DSharpPlus.SlashCommands to DSharpPlus.Commands - topicUid: articles.migration.slashcommands_to_commands -- name: Miscellaneous - items: - - name: Nightly Builds - topicUid: articles.misc.nightly_builds - - name: Debug Symbols - topicUid: articles.misc.debug_symbols - - name: Reporting Issues - topicUid: articles.misc.reporting_issues +- name: Preamble + topicUid: articles.preamble +- name: The Basics + items: + - name: Creating a Bot Account + topicUid: articles.basics.bot_account + - name: Writing Your First Bot + topicUid: articles.basics.first_bot +- name: Beyond Basics + items: + - name: Events + topicUid: articles.beyond_basics.events + - name: Logging + topicUid: articles.beyond_basics.logging.default + items: + - name: The Default Logger + topicUid: articles.beyond_basics.logging.default + - name: Third Party Loggers + topicUid: articles.beyond_basics.logging.third_party + - name: Intents + topicUid: articles.beyond_basics.intents + - name: Sharding + topicUid: articles.beyond_basics.sharding + - name: Message Builder + topicUid: articles.beyond_basics.messagebuilder + - name: Interactions + topicUid: articles.beyond_basics.interactions + - name: Permissions + topicUid: articles.beyond_basics.permissions +- name: Commands + items: + - name: Introduction + topicUid: articles.commands.introduction + - name: Custom Context Checks + topicUid: articles.commands.custom_context_checks + - name: Custom Error Handler + topicUid: articles.commands.custom_error_handler + - name: Variadic Parameters + topicUid: articles.commands.variadic_parameters + - name: Argument Converters + items: + - name: Built-In Converters + topicUid: articles.commands.converters.built_in_converters + - name: Manually Invoking Converters + topicUid: articles.commands.converters.manually_invoking_converters + - name: Custom Argument Converters + topicUid: articles.commands.converters.custom_argument_converters + - name: Command Processors + items: + - name: Introduction + topicUid: articles.commands.command_processors.introduction + - name: Text Commands + items: + - name: Command Aliases + topicUid: articles.commands.command_processors.text_commands.command_aliases + - name: Custom Prefix Handler + topicUid: articles.commands.command_processors.text_commands.custom_prefix_handler + - name: Remaining Text + topicUid: articles.commands.command_processors.text_commands.remaining_text + - name: Slash Commands + items: + - name: Choice Providers vs AutoComplete + topicUid: articles.commands.command_processors.slash_commands.choice_provider_vs_autocomplete + - name: Missing Commands + topicUid: articles.commands.command_processors.slash_commands.missing_commands + - name: Naming Policies + topicUid: articles.commands.command_processors.slash_commands.naming_policies + - name: Localizing Interactions + topicUid: articles.commands.command_processors.slash_commands.localizing_interactions +- name: CommandsNext + items: + - name: Introduction + topicUid: articles.commands_next.intro + - name: Command Attributes + topicUid: articles.commands_next.command_attributes + - name: Dependency Injection + topicUid: articles.commands_next.dependency_injection + - name: Customization + items: + - name: Help Formatter + topicUid: articles.commands_next.help_formatter + - name: Argument Converters + topicUid: articles.commands_next.argument_converters + - name: Command Handler + topicUid: articles.commands_next.command_handler +- name: Audio + items: + - name: Lavalink + items: + - name: Setup + topicUid: articles.audio.lavalink.setup + - name: Configuration + topicUid: articles.audio.lavalink.configuration + - name: Music Commands + topicUid: articles.audio.lavalink.music_commands + - name: VoiceNext + items: + - name: Prerequisites + topicUid: articles.audio.voicenext.prerequisites + - name: Transmitting + topicUid: articles.audio.voicenext.transmit + - name: Receiving + topicUid: articles.audio.voicenext.receive +- name: Interactivity + topicUid: articles.interactivity +- name: Slash Commands + topicUid: articles.slash_commands +- name: Advanced Topics + items: + - name: Buttons + topicUid: articles.advanced_topics.buttons + - name: Select Menus + topicUid: articles.advanced_topics.selects + - name: Generic Host + topicUid: articles.advanced_topics.generic_host + - name: Metrics and Profiling + topicUid: articles.advanced_topics.metrics_profiling + - name: Trace Logging + topicUid: articles.advanced_topics.trace + - name: Gateway Compression + topicUid: articles.advanced_topics.compression +- name: Hosting + topicUid: articles.hosting +- name: Version Migration + items: + - name: DiscordSharp to DSharpPlus + topicUid: articles.migration.dsharp + - name: 2.x to 3.x + topicUid: articles.migration.2x_to_3x + - name: 3.x to 4.x + topicUid: articles.migration.3x_to_4x + - name: 4.x to 5.x + topicUid: articles.migration.4x_to_5x + - name: DSharpPlus.SlashCommands to DSharpPlus.Commands + topicUid: articles.migration.slashcommands_to_commands +- name: Miscellaneous + items: + - name: Nightly Builds + topicUid: articles.misc.nightly_builds + - name: Debug Symbols + topicUid: articles.misc.debug_symbols + - name: Reporting Issues + topicUid: articles.misc.reporting_issues diff --git a/docs/faq.md b/docs/faq.md index 3c34fc5139..c43b263d34 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -1,134 +1,134 @@ ---- -uid: faq -title: Frequently Asked Questions ---- - -# Frequently Asked Questions - -#### I have updated from an old version to the latest version and my project will not build! -Please read the latest [migration article][0] to see a list of changes, as new releases may contain breaking changes. - -#### Code I copied from an article is not compiling or working as expected. Why? -*Please use the code snippets as a reference; don't blindly copy-paste code!* - -The snippets of code in the articles are meant to serve as examples to help you understand how to use a part of the -library. Although most will compile and work at the time of writing, changes to the library over time can make some -snippets obsolete. Many issues can be resolved with Intellisense by searching for similarly named methods and verifying -method parameters. - -#### I am targeting .NET Framework, Mono or Unity and have exceptions, crashes, or other problems. -As mentioned in the [preamble][1], the Mono runtime is inherently unstable and has numerous flaws. Because of this we -do not support Mono in any way, nor will we support any other projects which use it, including Unity. - -.NET Framework is outdated and we are dropping support for it in major version 5.0; and we do not accept bug reports -or issues from .NET Framework. - -Instead, we recommend using the most recent stable version of [.NET][2]. - -#### I see the latest stable version was released quite a while ago, what should I do? -You should consider targeting nightly versions if possible. They're usually about as stable as the stable version and -purely exist to allow us to iterate faster, implementing broader changes and adapting to Discord changes more effectively. -Many newer Discord features will only be implemented in nightly versions. To use them, specify to your nuget client that -you want to enable prereleases, either via CLI flag or a checkbox in your favourite IDE. - -#### Connecting to a voice channel with VoiceNext will either hang or throw an exception. -To troubleshoot, please ensure that: -* You are using the latest version of DSharpPlus. -* You have properly enabled VoiceNext with your instance of @DSharpPlus.DiscordClient. -* You are *not* using VoiceNext in an event handler. -* You have [opus and libsodium][3] available in your target environment. - - -#### Why am I getting *heartbeat skipped* messages in my console? -Check your internet connection and ensure that the machine your bot is hosted on has a stable internet connection. If -your local network has no issues, the problem could be with either Discord or Cloudflare, in which case, it is out of your -control. - -#### Why am I not getting message data? -Verify whether you have the Message Content intent enabled in both the developer dashboard and specified in your -DiscordConfiguration. If your bot is in more than 100 guilds, you will need approval for it from Discord. - -#### Why am I getting a 4XX error and how can I fix it? -HTTP Error Code | Cause | Resolution -:--------------:|:----------------------------|:--------------------- -`400` | Malformed request. | Catch the exception and inspect the `Errors` and `JsonMessage` properties - they will tell you what part of your request was malformed. If you need help figuring out what went wrong or suspect a library bug, feel free to contact us. -`401` | Invalid token. | Verify your token and make sure no errors were made. The client secret found on the 'general information' tab of your application page *is not* your token. -`403` | Not enough permissions. | Verify permissions and ensure your bot account has a role higher than the target user. Administrator permissions *do not* bypass the role hierarchy. -`404` | Requested object not found. | This usually means the entity does not exist. A 404 response from an interaction (slash command, user/message context command, modal, button) generally means the interaction has expired - if that is the case, either defer the interaction or speed up the code that runs before making your response. -`429` | Ratelimit hit. | If you see one-off ratelimit errors, that's fine, you should reattempt or inform the user. If you can consistently reproduce this, you should report this to us with a trace log and as much code as possible. You may need to reduce the amount of requests you make, or you may have found a library issue. - -#### I cannot modify a specific user or role. Why is this? -In order to modify a user, the highest role of your bot account must be higher than the target user. Changing the properties of a role requires that your bot account have a role higher than that role. - -#### Does the command framework I use support dependency injection? -It does! However, they're all slightly different. - -- If you use DSharpPlus.Commands, dependency injection happens through the constructor. One scope is created per command and used for everything contextually related to the command. -- If you use DSharpPlus.SlashCommands, dependency injection also happens through the constructor, but scopes don't always work as you might expect them to. Additionally, context checks do not support dependency injection - you will have to resort to the service locator pattern. -- If you use DSharpPlus.CommandsNext, dependency injection happens through constructors, properties and fields, and scopes don't always work as you might expect them to. You should refrain from using property and field injection and mark your fields private. Any public fields or properties you might need should be annotated as `[DontInject]` to prevent issues. Additionally, context checks and argument converters do not support dependency injection - as with SlashCommands, you will have to resort to the service locator pattern. - -Furthermore, you should note that SlashCommands and CommandsNext will be deprecated soon and removed in a future release. - -#### Can I use a user token? -Automating a user account is against Discord's Terms of Service and is not supported by DSharpPlus. - -#### How can I set a custom status? -You can use either of the following methods (prefer the first one if possible, since it does not require a special API call). - -- The overload for @DSharpPlus.DiscordClient.ConnectAsync(DSharpPlus.Entities.DiscordActivity,System.Nullable{DSharpPlus.Entities.UserStatus},System.Nullable{System.DateTimeOffset}) which accepts a @DSharpPlus.Entities.DiscordActivity. -- The overload for @DSharpPlus.DiscordClient.UpdateStatusAsync(DSharpPlus.Entities.DiscordActivity,System.Nullable{DSharpPlus.Entities.UserStatus},System.Nullable{System.DateTimeOffset}) which accepts a @DSharpPlus.Entities.DiscordActivity. -- The overload for @DSharpPlus.DiscordClient.UpdateStatusAsync(DSharpPlus.Entities.DiscordActivity,System.Nullable{DSharpPlus.Entities.UserStatus},System.Nullable{System.DateTimeOffset}) OR @DSharpPlus.DiscordShardedClient.UpdateStatusAsync(DSharpPlus.Entities.DiscordActivity,System.Nullable{DSharpPlus.Entities.UserStatus},System.Nullable{System.DateTimeOffset}) (for the sharded client) at any point after the connection has been established. - -#### Am I able to retrieve an entity by name? -Yes, if you have the parent object. For example, if you are searching for a role in a guild, use LINQ on the `Roles` property and filter by -`DiscordRole.Name`. If you do not have the parent object or the property is not populated, you will either have to request it or find an -alternative way of solving your problem. - -#### Why are you using Newtonsoft.Json when System.Text.Json is available? -Newtonsoft.Json is grandfathered in from the times before System.Text.Json was available, and migrating our codebase is a monumental task. -We are taking steps in that direction, it just takes a long time. - -#### Why the hell are my events not firing? -This is because since version 8 of the Discord API, @DSharpPlus.DiscordIntents are required to be enabled on -@DSharpPlus.DiscordConfiguration and the Discord Application Portal. We have an [article][4] that covers all that has to -be done to set this up. - -#### Speaking of events, where is my ready event? -You should avoid using the ready event in most cases; it does not indicate that your bot is ready to operate. For this reason, we have changed -the name in nightly builds to `SessionCreated`, which you can hook if you must, but generally you should prefer `GuildDownloadCompleted`, which -is fired when the bot is ready to operate. - -#### And where are my errors? / My command just silently fails without an error! -The library catches exceptions and dispatches them to an event. DSharpPlus.Commands contains a default error handler that will inform you of any -errors, but we don't yet do this in all places (we're planning to). See the following useful table for what events to hook: - -| Library | Error Location | Event -|:--------|:---------------|:----- -| DSharpPlus | any event handler | `DiscordClient.ClientErrored` -| DSharpPlus.CommandsNext | any command | `CommandsNextExtension.CommandErrored` -| DSharpPlus.SlashCommands | any command | `SlashCommandsExtension.SlashCommandErrored` -| DSharpPlus.SlashCommands | any autocomplete handler | `SlashCommandsExtension.AutocompleteErrored` -| DSharpPlus.Commands | anywhere | `CommandsExtension.CommandErrored` - -This is also where you can retrieve the results of any pre-execution checks you may have registered. - -#### Why does everything explode when I try to serialize entities or push them to a database? -Our entities are tightly bound to each other and their associated `DiscordClient` and cannot be serialized or deserialized without significant -involvement of library internals. If you need to store them, you should create your own serialization models that contain the data you need. - -#### Why is everything sealed? Why can't I extend anything? -Because of how these library internals mentioned above work, inheriting from our entities rarely if ever does anything useful for you. If you -added another field, it couldn't be used, if you changed some method, you would risk the library breaking. There are some exceptions where an -abstract base type exists, and potentially some more where it may not - feel free to let us know - but in general, you should prefer extension -methods and custom helper methods. - -#### Where are my pictures of spiderman? -![GOD DAMN IT PETER][5] - - -[0]: xref:articles.migration.3x_to_4x -[1]: xref:articles.preamble -[2]: https://dotnet.microsoft.com/download -[3]: xref:articles.audio.voicenext.prerequisites -[4]: xref:articles.beyond_basics.intents -[5]: ./images/faq_spiderman.png +--- +uid: faq +title: Frequently Asked Questions +--- + +# Frequently Asked Questions + +#### I have updated from an old version to the latest version and my project will not build! +Please read the latest [migration article][0] to see a list of changes, as new releases may contain breaking changes. + +#### Code I copied from an article is not compiling or working as expected. Why? +*Please use the code snippets as a reference; don't blindly copy-paste code!* + +The snippets of code in the articles are meant to serve as examples to help you understand how to use a part of the +library. Although most will compile and work at the time of writing, changes to the library over time can make some +snippets obsolete. Many issues can be resolved with Intellisense by searching for similarly named methods and verifying +method parameters. + +#### I am targeting .NET Framework, Mono or Unity and have exceptions, crashes, or other problems. +As mentioned in the [preamble][1], the Mono runtime is inherently unstable and has numerous flaws. Because of this we +do not support Mono in any way, nor will we support any other projects which use it, including Unity. + +.NET Framework is outdated and we are dropping support for it in major version 5.0; and we do not accept bug reports +or issues from .NET Framework. + +Instead, we recommend using the most recent stable version of [.NET][2]. + +#### I see the latest stable version was released quite a while ago, what should I do? +You should consider targeting nightly versions if possible. They're usually about as stable as the stable version and +purely exist to allow us to iterate faster, implementing broader changes and adapting to Discord changes more effectively. +Many newer Discord features will only be implemented in nightly versions. To use them, specify to your nuget client that +you want to enable prereleases, either via CLI flag or a checkbox in your favourite IDE. + +#### Connecting to a voice channel with VoiceNext will either hang or throw an exception. +To troubleshoot, please ensure that: +* You are using the latest version of DSharpPlus. +* You have properly enabled VoiceNext with your instance of @DSharpPlus.DiscordClient. +* You are *not* using VoiceNext in an event handler. +* You have [opus and libsodium][3] available in your target environment. + + +#### Why am I getting *heartbeat skipped* messages in my console? +Check your internet connection and ensure that the machine your bot is hosted on has a stable internet connection. If +your local network has no issues, the problem could be with either Discord or Cloudflare, in which case, it is out of your +control. + +#### Why am I not getting message data? +Verify whether you have the Message Content intent enabled in both the developer dashboard and specified in your +DiscordConfiguration. If your bot is in more than 100 guilds, you will need approval for it from Discord. + +#### Why am I getting a 4XX error and how can I fix it? +HTTP Error Code | Cause | Resolution +:--------------:|:----------------------------|:--------------------- +`400` | Malformed request. | Catch the exception and inspect the `Errors` and `JsonMessage` properties - they will tell you what part of your request was malformed. If you need help figuring out what went wrong or suspect a library bug, feel free to contact us. +`401` | Invalid token. | Verify your token and make sure no errors were made. The client secret found on the 'general information' tab of your application page *is not* your token. +`403` | Not enough permissions. | Verify permissions and ensure your bot account has a role higher than the target user. Administrator permissions *do not* bypass the role hierarchy. +`404` | Requested object not found. | This usually means the entity does not exist. A 404 response from an interaction (slash command, user/message context command, modal, button) generally means the interaction has expired - if that is the case, either defer the interaction or speed up the code that runs before making your response. +`429` | Ratelimit hit. | If you see one-off ratelimit errors, that's fine, you should reattempt or inform the user. If you can consistently reproduce this, you should report this to us with a trace log and as much code as possible. You may need to reduce the amount of requests you make, or you may have found a library issue. + +#### I cannot modify a specific user or role. Why is this? +In order to modify a user, the highest role of your bot account must be higher than the target user. Changing the properties of a role requires that your bot account have a role higher than that role. + +#### Does the command framework I use support dependency injection? +It does! However, they're all slightly different. + +- If you use DSharpPlus.Commands, dependency injection happens through the constructor. One scope is created per command and used for everything contextually related to the command. +- If you use DSharpPlus.SlashCommands, dependency injection also happens through the constructor, but scopes don't always work as you might expect them to. Additionally, context checks do not support dependency injection - you will have to resort to the service locator pattern. +- If you use DSharpPlus.CommandsNext, dependency injection happens through constructors, properties and fields, and scopes don't always work as you might expect them to. You should refrain from using property and field injection and mark your fields private. Any public fields or properties you might need should be annotated as `[DontInject]` to prevent issues. Additionally, context checks and argument converters do not support dependency injection - as with SlashCommands, you will have to resort to the service locator pattern. + +Furthermore, you should note that SlashCommands and CommandsNext will be deprecated soon and removed in a future release. + +#### Can I use a user token? +Automating a user account is against Discord's Terms of Service and is not supported by DSharpPlus. + +#### How can I set a custom status? +You can use either of the following methods (prefer the first one if possible, since it does not require a special API call). + +- The overload for @DSharpPlus.DiscordClient.ConnectAsync(DSharpPlus.Entities.DiscordActivity,System.Nullable{DSharpPlus.Entities.UserStatus},System.Nullable{System.DateTimeOffset}) which accepts a @DSharpPlus.Entities.DiscordActivity. +- The overload for @DSharpPlus.DiscordClient.UpdateStatusAsync(DSharpPlus.Entities.DiscordActivity,System.Nullable{DSharpPlus.Entities.UserStatus},System.Nullable{System.DateTimeOffset}) which accepts a @DSharpPlus.Entities.DiscordActivity. +- The overload for @DSharpPlus.DiscordClient.UpdateStatusAsync(DSharpPlus.Entities.DiscordActivity,System.Nullable{DSharpPlus.Entities.UserStatus},System.Nullable{System.DateTimeOffset}) OR @DSharpPlus.DiscordShardedClient.UpdateStatusAsync(DSharpPlus.Entities.DiscordActivity,System.Nullable{DSharpPlus.Entities.UserStatus},System.Nullable{System.DateTimeOffset}) (for the sharded client) at any point after the connection has been established. + +#### Am I able to retrieve an entity by name? +Yes, if you have the parent object. For example, if you are searching for a role in a guild, use LINQ on the `Roles` property and filter by +`DiscordRole.Name`. If you do not have the parent object or the property is not populated, you will either have to request it or find an +alternative way of solving your problem. + +#### Why are you using Newtonsoft.Json when System.Text.Json is available? +Newtonsoft.Json is grandfathered in from the times before System.Text.Json was available, and migrating our codebase is a monumental task. +We are taking steps in that direction, it just takes a long time. + +#### Why the hell are my events not firing? +This is because since version 8 of the Discord API, @DSharpPlus.DiscordIntents are required to be enabled on +@DSharpPlus.DiscordConfiguration and the Discord Application Portal. We have an [article][4] that covers all that has to +be done to set this up. + +#### Speaking of events, where is my ready event? +You should avoid using the ready event in most cases; it does not indicate that your bot is ready to operate. For this reason, we have changed +the name in nightly builds to `SessionCreated`, which you can hook if you must, but generally you should prefer `GuildDownloadCompleted`, which +is fired when the bot is ready to operate. + +#### And where are my errors? / My command just silently fails without an error! +The library catches exceptions and dispatches them to an event. DSharpPlus.Commands contains a default error handler that will inform you of any +errors, but we don't yet do this in all places (we're planning to). See the following useful table for what events to hook: + +| Library | Error Location | Event +|:--------|:---------------|:----- +| DSharpPlus | any event handler | `DiscordClient.ClientErrored` +| DSharpPlus.CommandsNext | any command | `CommandsNextExtension.CommandErrored` +| DSharpPlus.SlashCommands | any command | `SlashCommandsExtension.SlashCommandErrored` +| DSharpPlus.SlashCommands | any autocomplete handler | `SlashCommandsExtension.AutocompleteErrored` +| DSharpPlus.Commands | anywhere | `CommandsExtension.CommandErrored` + +This is also where you can retrieve the results of any pre-execution checks you may have registered. + +#### Why does everything explode when I try to serialize entities or push them to a database? +Our entities are tightly bound to each other and their associated `DiscordClient` and cannot be serialized or deserialized without significant +involvement of library internals. If you need to store them, you should create your own serialization models that contain the data you need. + +#### Why is everything sealed? Why can't I extend anything? +Because of how these library internals mentioned above work, inheriting from our entities rarely if ever does anything useful for you. If you +added another field, it couldn't be used, if you changed some method, you would risk the library breaking. There are some exceptions where an +abstract base type exists, and potentially some more where it may not - feel free to let us know - but in general, you should prefer extension +methods and custom helper methods. + +#### Where are my pictures of spiderman? +![GOD DAMN IT PETER][5] + + +[0]: xref:articles.migration.3x_to_4x +[1]: xref:articles.preamble +[2]: https://dotnet.microsoft.com/download +[3]: xref:articles.audio.voicenext.prerequisites +[4]: xref:articles.beyond_basics.intents +[5]: ./images/faq_spiderman.png diff --git a/obsolete/DSharpPlus.SlashCommands/SlashCommandsExtension.cs b/obsolete/DSharpPlus.SlashCommands/SlashCommandsExtension.cs index e90214ce01..e4a19453e3 100644 --- a/obsolete/DSharpPlus.SlashCommands/SlashCommandsExtension.cs +++ b/obsolete/DSharpPlus.SlashCommands/SlashCommandsExtension.cs @@ -1,2044 +1,2044 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Reflection; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using DSharpPlus.AsyncEvents; -using DSharpPlus.Entities; -using DSharpPlus.EventArgs; -using DSharpPlus.Exceptions; -using DSharpPlus.SlashCommands.EventArgs; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; - -namespace DSharpPlus.SlashCommands; - -/// -/// A class that handles slash commands for a client. -/// -[Obsolete( - "DSharpPlus.SlashCommands is obsolete. Please consider using the new DSharpPlus.Commands extension instead." -)] -public sealed partial class SlashCommandsExtension : IDisposable -{ - //A list of methods for top level commands - private static List commandMethods { get; set; } = []; - - //List of groups - private static List groupCommands { get; set; } = []; - - //List of groups with subgroups - private static List subGroupCommands { get; set; } = []; - - //List of context menus - private static List contextMenuCommands { get; set; } = []; - - //Singleton modules - private static List singletonModules { get; set; } = []; - - //List of modules to register - private List> updateList { get; set; } = []; - - //Set to true if anything fails when registering - private static bool errored { get; set; } = false; - - private readonly IServiceProvider services; - - /// - /// Gets a list of registered commands. The key is the guild id (null if global). - /// - public static IReadOnlyList< - KeyValuePair> - > RegisteredCommands => registeredCommands; - - public DiscordClient Client { get; private set; } - - private static readonly List< - KeyValuePair> - > registeredCommands = []; - - internal SlashCommandsExtension(IServiceProvider serviceProvider) => - this.services = serviceProvider; - - /// - /// Runs setup. DO NOT RUN THIS MANUALLY. DO NOT DO ANYTHING WITH THIS. - /// - /// The client to setup on. - internal void Setup(DiscordClient client) - { - if (this.Client != null) - { - throw new InvalidOperationException("What did I tell you?"); - } - - this.Client = client; - - DefaultClientErrorHandler errorHandler = new(client.Logger); - - this.slashError = new AsyncEvent( - errorHandler - ); - this.slashInvoked = new AsyncEvent( - errorHandler - ); - this.slashExecuted = new AsyncEvent( - errorHandler - ); - this.contextMenuErrored = new AsyncEvent( - errorHandler - ); - this.contextMenuExecuted = new AsyncEvent< - SlashCommandsExtension, - ContextMenuExecutedEventArgs - >(errorHandler); - this.contextMenuInvoked = new AsyncEvent< - SlashCommandsExtension, - ContextMenuInvokedEventArgs - >(errorHandler); - this.autocompleteErrored = new AsyncEvent< - SlashCommandsExtension, - AutocompleteErrorEventArgs - >(errorHandler); - this.autocompleteExecuted = new AsyncEvent< - SlashCommandsExtension, - AutocompleteExecutedEventArgs - >(errorHandler); - } - - /// - /// 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 : ApplicationCommandModule => this.updateList.Add(new(guildId, 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(ApplicationCommandModule).IsAssignableFrom(type)) - { - throw new ArgumentException( - "Command classes have to inherit from ApplicationCommandModule", - nameof(type) - ); - } - - this.updateList.Add(new(guildId, type)); - } - - /// - /// Registers all command classes from a given assembly. - /// - /// Assembly to register command classes from. - /// The guild id to register it on. If you want global commands, leave it null. - public void RegisterCommands(Assembly assembly, ulong? guildId = null) - { - IEnumerable types = assembly.ExportedTypes.Where(xt => - typeof(ApplicationCommandModule).IsAssignableFrom(xt) && !xt.GetTypeInfo().IsNested - ); - - foreach (Type? xt in types) - { - RegisterCommands(xt, guildId); - } - } - - //To be run on ready - internal Task Update(DiscordClient client, SessionCreatedEventArgs e) => Update(); - - //Actual method for registering, used for RegisterCommands and on Ready - internal Task Update() - { - //Groups commands by guild id or global - foreach (ulong? key in this.updateList.Select(x => x.Key).Distinct()) - { - RegisterCommands(this.updateList.Where(x => x.Key == key).Select(x => x.Value), key); - } - - return Task.CompletedTask; - } - - #region Registering - - //Method for registering commands for a target from modules - private void RegisterCommands(IEnumerable types, ulong? guildId) - { - //Initialize empty lists to be added to the global ones at the end - List commandMethodsToAdd = []; - List groupCommandsToAdd = []; - List subGroupCommandsToAdd = []; - List contextMenuCommandsToAdd = []; - List updateList = []; - - _ = Task.Run(async () => - { - //Iterates over all the modules - foreach (Type type in types) - { - try - { - TypeInfo module = type.GetTypeInfo(); - List classes = []; - - //Add module to classes list if it's a group - if (module.GetCustomAttribute() != null) - { - classes.Add(module); - } - else - { - //Otherwise add the nested groups - classes = module - .DeclaredNestedTypes.Where(x => - x.GetCustomAttribute() != null - ) - .ToList(); - } - - //Handles groups - foreach (TypeInfo subclassinfo in classes) - { - //Gets the attribute and methods in the group - - bool allowDMs = - subclassinfo.GetCustomAttribute() is null; - DiscordPermissions? v2Permissions = new(subclassinfo - .GetCustomAttribute() - ?.Permissions ?? []); - - SlashCommandGroupAttribute? groupAttribute = - subclassinfo.GetCustomAttribute(); - IEnumerable submethods = subclassinfo.DeclaredMethods.Where(x => - x.GetCustomAttribute() != null - ); - IEnumerable subclasses = subclassinfo.DeclaredNestedTypes.Where( - x => x.GetCustomAttribute() != null - ); - if (subclasses.Any() && submethods.Any()) - { - throw new ArgumentException( - "Slash command groups cannot have both subcommands and subgroups!" - ); - } - - //Group context menus - IEnumerable contextMethods = subclassinfo.DeclaredMethods.Where( - x => x.GetCustomAttribute() != null - ); - AddContextMenus(contextMethods); - - //Initializes the command - DiscordApplicationCommand payload = - new( - groupAttribute.Name, - groupAttribute.Description, - defaultPermission: groupAttribute.DefaultPermission, - allowDMUsage: allowDMs, - defaultMemberPermissions: v2Permissions, - nsfw: groupAttribute.NSFW - ); - - List> commandmethods = []; - //Handles commands in the group - foreach (MethodInfo? submethod in submethods) - { - SlashCommandAttribute? commandAttribute = - submethod.GetCustomAttribute(); - - //Gets the paramaters and accounts for InteractionContext - ParameterInfo[] parameters = submethod.GetParameters(); - if ( - parameters?.Length is null or 0 - || !ReferenceEquals( - parameters.First().ParameterType, - typeof(InteractionContext) - ) - ) - { - throw new ArgumentException( - $"The first argument must be an InteractionContext!" - ); - } - - parameters = parameters.Skip(1).ToArray(); - - //Check if the ReturnType can be safely casted to a Task later on execution - if (!typeof(Task).IsAssignableFrom(submethod.ReturnType)) - { - throw new InvalidOperationException( - "The method has to return a Task or Task<> value" - ); - } - - List options = - await ParseParametersAsync(parameters, guildId); - - IReadOnlyDictionary nameLocalizations = - GetNameLocalizations(submethod); - IReadOnlyDictionary descriptionLocalizations = - GetDescriptionLocalizations(submethod); - IReadOnlyList? integrationTypes = - GetInteractionCommandInstallTypes(submethod); - IReadOnlyList? contexts = - GetInteractionCommandAllowedContexts(submethod); - - //Creates the subcommand and adds it to the main command - DiscordApplicationCommandOption subpayload = - new( - commandAttribute.Name, - commandAttribute.Description, - DiscordApplicationCommandOptionType.SubCommand, - null, - null, - options, - name_localizations: nameLocalizations, - description_localizations: descriptionLocalizations - ); - payload = new DiscordApplicationCommand( - payload.Name, - payload.Description, - payload.Options?.Append(subpayload) ?? new[] { subpayload }, - payload.DefaultPermission, - allowDMUsage: allowDMs, - defaultMemberPermissions: v2Permissions, - nsfw: payload.NSFW, - integrationTypes: integrationTypes, - contexts: contexts - ); - - //Adds it to the method lists - commandmethods.Add(new(commandAttribute.Name, submethod)); - groupCommandsToAdd.Add( - new() { Name = groupAttribute.Name, Methods = commandmethods } - ); - } - - SubGroupCommand command = new() { Name = groupAttribute.Name }; - //Handles subgroups - foreach (TypeInfo? subclass in subclasses) - { - SlashCommandGroupAttribute? subGroupAttribute = - subclass.GetCustomAttribute(); - //I couldn't think of more creative naming - IEnumerable subsubmethods = subclass.DeclaredMethods.Where( - x => x.GetCustomAttribute() != null - ); - - List options = []; - - List> currentMethods = []; - - //Similar to the one for regular groups - foreach (MethodInfo? subsubmethod in subsubmethods) - { - List suboptions = []; - SlashCommandAttribute? commatt = - subsubmethod.GetCustomAttribute(); - ParameterInfo[] parameters = subsubmethod.GetParameters(); - if ( - parameters?.Length is null or 0 - || !ReferenceEquals( - parameters.First().ParameterType, - typeof(InteractionContext) - ) - ) - { - throw new ArgumentException( - $"The first argument must be an InteractionContext!" - ); - } - - parameters = parameters.Skip(1).ToArray(); - suboptions = - [ - .. suboptions, - .. await ParseParametersAsync(parameters, guildId), - ]; - - IReadOnlyDictionary nameLocalizations = - GetNameLocalizations(subsubmethod); - IReadOnlyDictionary descriptionLocalizations = - GetDescriptionLocalizations(subsubmethod); - - DiscordApplicationCommandOption subsubpayload = - new( - commatt.Name, - commatt.Description, - DiscordApplicationCommandOptionType.SubCommand, - null, - null, - suboptions, - name_localizations: nameLocalizations, - description_localizations: descriptionLocalizations - ); - options.Add(subsubpayload); - - commandmethods.Add(new(commatt.Name, subsubmethod)); - currentMethods.Add(new(commatt.Name, subsubmethod)); - } - - //Subgroups Context Menus - IEnumerable subContextMethods = - subclass.DeclaredMethods.Where(x => - x.GetCustomAttribute() != null - ); - AddContextMenus(subContextMethods); - - //Adds the group to the command and method lists - DiscordApplicationCommandOption subpayload = - new( - subGroupAttribute.Name, - subGroupAttribute.Description, - DiscordApplicationCommandOptionType.SubCommandGroup, - null, - null, - options - ); - command.SubCommands.Add( - new() { Name = subGroupAttribute.Name, Methods = currentMethods } - ); - payload = new DiscordApplicationCommand( - payload.Name, - payload.Description, - payload.Options?.Append(subpayload) ?? new[] { subpayload }, - payload.DefaultPermission, - allowDMUsage: allowDMs, - defaultMemberPermissions: v2Permissions, - nsfw: payload.NSFW - ); - - //Accounts for lifespans for the sub group - if ( - subclass.GetCustomAttribute() - is not null - and { Lifespan: SlashModuleLifespan.Singleton } - ) - { - singletonModules.Add(CreateInstance(subclass, this.services)); - } - } - - if (command.SubCommands.Count != 0) - { - subGroupCommandsToAdd.Add(command); - } - - updateList.Add(payload); - - //Accounts for lifespans - if ( - subclassinfo.GetCustomAttribute() - is not null - and { Lifespan: SlashModuleLifespan.Singleton } - ) - { - singletonModules.Add(CreateInstance(subclassinfo, this.services)); - } - } - - //Handles methods, only if the module isn't a group itself - if (module.GetCustomAttribute() is null) - { - //Slash commands (again, similar to the one for groups) - IEnumerable methods = module.DeclaredMethods.Where(x => - x.GetCustomAttribute() != null - ); - - foreach (MethodInfo? method in methods) - { - SlashCommandAttribute? commandattribute = - method.GetCustomAttribute(); - - ParameterInfo[] parameters = method.GetParameters(); - if ( - parameters?.Length is null or 0 - || !ReferenceEquals( - parameters.FirstOrDefault()?.ParameterType, - typeof(InteractionContext) - ) - ) - { - throw new ArgumentException( - $"The first argument must be an InteractionContext!" - ); - } - - parameters = parameters.Skip(1).ToArray(); - List options = - await ParseParametersAsync(parameters, guildId); - - commandMethodsToAdd.Add( - new() { Method = method, Name = commandattribute.Name } - ); - - IReadOnlyDictionary nameLocalizations = - GetNameLocalizations(method); - IReadOnlyDictionary descriptionLocalizations = - GetDescriptionLocalizations(method); - IReadOnlyList? integrationTypes = - GetInteractionCommandInstallTypes(method); - IReadOnlyList? contexts = - GetInteractionCommandAllowedContexts(method); - - bool allowDMs = - ( - method.GetCustomAttribute() - ?? method.DeclaringType.GetCustomAttribute() - ) - is null; - DiscordPermissions? v2Permissions = new(( - method.GetCustomAttribute() - ?? method.DeclaringType.GetCustomAttribute() - )?.Permissions ?? []); - - DiscordApplicationCommand payload = - new( - commandattribute.Name, - commandattribute.Description, - options, - commandattribute.DefaultPermission, - name_localizations: nameLocalizations, - description_localizations: descriptionLocalizations, - allowDMUsage: allowDMs, - defaultMemberPermissions: v2Permissions, - nsfw: commandattribute.NSFW, - integrationTypes: integrationTypes, - contexts: contexts - ); - updateList.Add(payload); - } - - //Context Menus - IEnumerable contextMethods = module.DeclaredMethods.Where(x => - x.GetCustomAttribute() != null - ); - AddContextMenus(contextMethods); - - //Accounts for lifespans - if ( - module.GetCustomAttribute() - is not null - and { Lifespan: SlashModuleLifespan.Singleton } - ) - { - singletonModules.Add(CreateInstance(module, this.services)); - } - } - - void AddContextMenus(IEnumerable contextMethods) - { - foreach (MethodInfo contextMethod in contextMethods) - { - ContextMenuAttribute? contextAttribute = - contextMethod.GetCustomAttribute(); - bool allowDMUsage = - ( - contextMethod.GetCustomAttribute() - ?? contextMethod.DeclaringType.GetCustomAttribute() - ) - is null; - DiscordPermissions? permissions = new(( - contextMethod.GetCustomAttribute() - ?? contextMethod.DeclaringType.GetCustomAttribute() - )?.Permissions ?? []); - IReadOnlyList? integrationTypes = - GetInteractionCommandInstallTypes(contextMethod); - IReadOnlyList? contexts = - GetInteractionCommandAllowedContexts(contextMethod); - DiscordApplicationCommand command = - new( - contextAttribute.Name, - null, - type: contextAttribute.Type, - defaultPermission: contextAttribute.DefaultPermission, - allowDMUsage: allowDMUsage, - defaultMemberPermissions: permissions, - nsfw: contextAttribute.NSFW, - integrationTypes: integrationTypes, - contexts: contexts - ); - - ParameterInfo[] parameters = contextMethod.GetParameters(); - if ( - parameters?.Length is null or 0 - || !ReferenceEquals( - parameters.FirstOrDefault()?.ParameterType, - typeof(ContextMenuContext) - ) - ) - { - throw new ArgumentException( - $"The first argument must be a ContextMenuContext!" - ); - } - - if (parameters.Length > 1) - { - throw new ArgumentException( - $"A context menu cannot have parameters!" - ); - } - - contextMenuCommandsToAdd.Add( - new ContextMenuCommand - { - Method = contextMethod, - Name = contextAttribute.Name, - } - ); - - updateList.Add(command); - } - } - } - catch (Exception ex) - { - //This isn't really much more descriptive but I added a separate case for it anyway - if (ex is BadRequestException brex) - { - this.Client.Logger.LogCritical( - brex, - "There was an error registering application commands: {JsonError}", - brex.JsonMessage - ); - } - else - { - this.Client.Logger.LogCritical( - ex, - $"There was an error registering application commands" - ); - } - - errored = true; - } - } - - if (!errored) - { - try - { - IEnumerable commands; - //Creates a guild command if a guild id is specified, otherwise global - commands = guildId is null - ? await this.Client.BulkOverwriteGlobalApplicationCommandsAsync(updateList) - : await this.Client.BulkOverwriteGuildApplicationCommandsAsync( - guildId.Value, - updateList - ); - - //Checks against the ids and adds them to the command method lists - foreach (DiscordApplicationCommand command in commands) - { - if (commandMethodsToAdd.Any(x => x.Name == command.Name)) - { - commandMethodsToAdd.First(x => x.Name == command.Name).CommandId = - command.Id; - } - else if (groupCommandsToAdd.Any(x => x.Name == command.Name)) - { - groupCommandsToAdd.First(x => x.Name == command.Name).CommandId = - command.Id; - } - else if (subGroupCommandsToAdd.Any(x => x.Name == command.Name)) - { - subGroupCommandsToAdd.First(x => x.Name == command.Name).CommandId = - command.Id; - } - else if (contextMenuCommandsToAdd.Any(x => x.Name == command.Name)) - { - contextMenuCommandsToAdd.First(x => x.Name == command.Name).CommandId = - command.Id; - } - } - //Adds to the global lists finally - commandMethods.AddRange(commandMethodsToAdd); - groupCommands.AddRange(groupCommandsToAdd); - subGroupCommands.AddRange(subGroupCommandsToAdd); - contextMenuCommands.AddRange(contextMenuCommandsToAdd); - - registeredCommands.Add(new(guildId, commands.ToList())); - } - catch (Exception ex) - { - if (ex is BadRequestException brex) - { - this.Client.Logger.LogCritical( - brex, - "There was an error registering application commands: {JsonMessage}", - brex.JsonMessage - ); - } - else - { - this.Client.Logger.LogCritical( - ex, - $"There was an error registering application commands" - ); - } - - errored = true; - } - } - }); - } - - //Handles the parameters for a slash command - private async Task> ParseParametersAsync( - ParameterInfo[] parameters, - ulong? guildId - ) - { - List options = []; - foreach (ParameterInfo parameter in parameters) - { - //Gets the attribute - OptionAttribute? optionattribute = - parameter.GetCustomAttribute() - ?? throw new ArgumentException("Arguments must have the Option attribute!"); - - //Sets the type - Type type = parameter.ParameterType; - string commandName = - parameter.Member.GetCustomAttribute()?.Name - ?? parameter.Member.GetCustomAttribute().Name; - DiscordApplicationCommandOptionType parametertype = GetParameterType(commandName, type); - - //Handles choices - //From attributes - List choices = GetChoiceAttributesFromParameter( - parameter.GetCustomAttributes() - ); - //From enums - if ( - parameter.ParameterType.IsEnum - || Nullable.GetUnderlyingType(parameter.ParameterType)?.IsEnum == true - ) - { - choices = GetChoiceAttributesFromEnumParameter(parameter.ParameterType); - } - //From choice provider - IEnumerable choiceProviders = - parameter.GetCustomAttributes(); - if (choiceProviders.Any()) - { - choices = await GetChoiceAttributesFromProviderAsync(choiceProviders, guildId); - } - - IEnumerable? channelTypes = - parameter.GetCustomAttribute()?.ChannelTypes ?? null; - - object? minimumValue = parameter.GetCustomAttribute()?.Value ?? null; - object? maximumValue = parameter.GetCustomAttribute()?.Value ?? null; - - int? minimumLength = - parameter.GetCustomAttribute()?.Value ?? null; - int? maximumLength = - parameter.GetCustomAttribute()?.Value ?? null; - - IReadOnlyDictionary nameLocalizations = GetNameLocalizations(parameter); - IReadOnlyDictionary descriptionLocalizations = - GetDescriptionLocalizations(parameter); - - AutocompleteAttribute? autocompleteAttribute = - parameter.GetCustomAttribute(); - if ( - autocompleteAttribute != null - && autocompleteAttribute.Provider.GetMethod(nameof(IAutocompleteProvider.Provider)) - == null - ) - { - throw new ArgumentException( - "Autocomplete providers must inherit from IAutocompleteProvider." - ); - } - - options.Add( - new DiscordApplicationCommandOption( - optionattribute.Name, - optionattribute.Description, - parametertype, - !parameter.IsOptional, - choices, - null, - channelTypes, - autocompleteAttribute != null || optionattribute.Autocomplete, - minimumValue, - maximumValue, - nameLocalizations, - descriptionLocalizations, - minimumLength, - maximumLength - ) - ); - } - - return options; - } - - private static IReadOnlyList? GetInteractionCommandInstallTypes( - ICustomAttributeProvider method - ) - { - InteractionCommandInstallTypeAttribute[] attributes = - (InteractionCommandInstallTypeAttribute[]) - method.GetCustomAttributes(typeof(InteractionCommandInstallTypeAttribute), false); - return attributes.FirstOrDefault()?.InstallTypes; - } - - private static IReadOnlyList? GetInteractionCommandAllowedContexts( - ICustomAttributeProvider method - ) - { - InteractionCommandAllowedContextsAttribute[] attributes = - (InteractionCommandAllowedContextsAttribute[]) - method.GetCustomAttributes( - typeof(InteractionCommandAllowedContextsAttribute), - false - ); - return attributes.FirstOrDefault()?.AllowedContexts; - } - - private static IReadOnlyDictionary GetNameLocalizations( - ICustomAttributeProvider method - ) - { - NameLocalizationAttribute[] nameAttributes = (NameLocalizationAttribute[]) - method.GetCustomAttributes(typeof(NameLocalizationAttribute), false); - return nameAttributes.ToDictionary( - nameAttribute => nameAttribute.Locale, - nameAttribute => nameAttribute.Name - ); - } - - private static IReadOnlyDictionary GetDescriptionLocalizations( - ICustomAttributeProvider method - ) - { - DescriptionLocalizationAttribute[] descriptionAttributes = - (DescriptionLocalizationAttribute[]) - method.GetCustomAttributes(typeof(DescriptionLocalizationAttribute), false); - return descriptionAttributes.ToDictionary( - descriptionAttribute => descriptionAttribute.Locale, - descriptionAttribute => descriptionAttribute.Description - ); - } - - //Gets the choices from a choice provider - private async Task< - List - > GetChoiceAttributesFromProviderAsync( - IEnumerable customAttributes, - ulong? guildId - ) - { - List choices = []; - foreach (ChoiceProviderAttribute choiceProviderAttribute in customAttributes) - { - MethodInfo? method = choiceProviderAttribute.ProviderType.GetMethod( - nameof(IChoiceProvider.Provider) - ); - - if (method == null) - { - throw new ArgumentException("ChoiceProviders must inherit from IChoiceProvider."); - } - else - { - object? 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, this.services); - } - - //Gets the choices from the method - IEnumerable result = await (Task< - IEnumerable - >) - method.Invoke(instance, null); - - if (result.Any()) - { - choices.AddRange(result); - } - } - } - - return choices; - } - - //Gets choices from an enum - private static List GetChoiceAttributesFromEnumParameter( - Type enumParam - ) - { - List choices = []; - if (enumParam.IsGenericType && enumParam.GetGenericTypeDefinition() == typeof(Nullable<>)) - { - enumParam = Nullable.GetUnderlyingType(enumParam); - } - foreach (Enum enumValue in Enum.GetValues(enumParam)) - { - choices.Add( - new DiscordApplicationCommandOptionChoice(enumValue.GetName(), enumValue.ToString()) - ); - } - return choices; - } - - //Small method to get the parameter's type from its type - private static DiscordApplicationCommandOptionType GetParameterType( - string commandName, - Type type - ) => - type == typeof(string) ? DiscordApplicationCommandOptionType.String - : type == typeof(long) || type == typeof(long?) - ? DiscordApplicationCommandOptionType.Integer - : type == typeof(bool) || type == typeof(bool?) - ? DiscordApplicationCommandOptionType.Boolean - : type == typeof(double) || type == typeof(double?) - ? DiscordApplicationCommandOptionType.Number - : type == typeof(DiscordChannel) ? DiscordApplicationCommandOptionType.Channel - : type == typeof(DiscordUser) ? DiscordApplicationCommandOptionType.User - : type == typeof(DiscordRole) ? DiscordApplicationCommandOptionType.Role - : type == typeof(DiscordEmoji) ? DiscordApplicationCommandOptionType.String - : type == typeof(TimeSpan?) ? DiscordApplicationCommandOptionType.String - : type == typeof(SnowflakeObject) ? DiscordApplicationCommandOptionType.Mentionable - : type.IsEnum || Nullable.GetUnderlyingType(type)?.IsEnum == true - ? DiscordApplicationCommandOptionType.String - : type == typeof(DiscordAttachment) ? DiscordApplicationCommandOptionType.Attachment - : throw new ArgumentException( - $"Cannot convert type! (Command: {commandName}) Argument types must be string, long, bool, double, TimeSpan?, DiscordChannel, DiscordUser, DiscordRole, DiscordEmoji, DiscordAttachment, SnowflakeObject, or an Enum." - ); - - //Gets choices from choice attributes - private static List GetChoiceAttributesFromParameter( - IEnumerable choiceattributes - ) => - !choiceattributes.Any() - ? null - : choiceattributes - .Select(att => new DiscordApplicationCommandOptionChoice( - att.Name, - att.Value.ToString() - )) - .ToList(); - - #endregion - - #region Handling - - internal Task InteractionHandler(DiscordClient client, InteractionCreatedEventArgs e) - { - _ = Task.Run(async () => - { - if ( - e.Interaction is - { - Type: DiscordInteractionType.ApplicationCommand, - Data.Type: DiscordApplicationCommandType.SlashCommand - } - ) - { - StringBuilder qualifiedName = new(e.Interaction.Data.Name); - DiscordInteractionDataOption[] options = - e.Interaction.Data.Options?.ToArray() ?? []; - while (options.Length != 0) - { - DiscordInteractionDataOption firstOption = options[0]; - if ( - firstOption.Type - is not DiscordApplicationCommandOptionType.SubCommandGroup - and not DiscordApplicationCommandOptionType.SubCommand - ) - { - break; - } - - _ = qualifiedName.AppendFormat(" {0}", firstOption.Name); - options = firstOption.Options?.ToArray() ?? []; - } - - //Creates the context - InteractionContext context = - new() - { - Interaction = e.Interaction, - Channel = e.Interaction.Channel, - Guild = e.Interaction.Guild, - User = e.Interaction.User, - Client = client, - SlashCommandsExtension = this, - CommandName = e.Interaction.Data.Name, - QualifiedName = qualifiedName.ToString(), - InteractionId = e.Interaction.Id, - Token = e.Interaction.Token, - Services = this.services, - ResolvedUserMentions = e.Interaction.Data.Resolved?.Users?.Values.ToList(), - ResolvedRoleMentions = e.Interaction.Data.Resolved?.Roles?.Values.ToList(), - ResolvedChannelMentions = - e.Interaction.Data.Resolved?.Channels?.Values.ToList(), - Type = DiscordApplicationCommandType.SlashCommand, - }; - - try - { - if (errored) - { - throw new InvalidOperationException( - "Slash commands failed to register properly on startup." - ); - } - - //Gets the method for the command - IEnumerable methods = commandMethods.Where(x => - x.CommandId == e.Interaction.Data.Id - ); - IEnumerable groups = groupCommands.Where(x => - x.CommandId == e.Interaction.Data.Id - ); - IEnumerable subgroups = subGroupCommands.Where(x => - x.CommandId == e.Interaction.Data.Id - ); - if (!methods.Any() && !groups.Any() && !subgroups.Any()) - { - throw new InvalidOperationException( - "A slash command was executed, but no command was registered for it." - ); - } - - //Just read the code you'll get it - if (methods.Any()) - { - MethodInfo method = methods.First().Method; - - List args = await ResolveInteractionCommandParametersAsync( - e, - context, - method, - e.Interaction.Data.Options - ); - - await RunCommandAsync(context, method, args); - } - else if (groups.Any()) - { - DiscordInteractionDataOption command = e.Interaction.Data.Options.First(); - MethodInfo method = groups - .First() - .Methods.First(x => x.Key == command.Name) - .Value; - - List args = await ResolveInteractionCommandParametersAsync( - e, - context, - method, - e.Interaction.Data.Options.First().Options - ); - - await RunCommandAsync(context, method, args); - } - else if (subgroups.Any()) - { - DiscordInteractionDataOption command = e.Interaction.Data.Options.First(); - GroupCommand group = subgroups - .First() - .SubCommands.First(x => x.Name == command.Name); - MethodInfo method = group - .Methods.First(x => x.Key == command.Options.First().Name) - .Value; - - List args = await ResolveInteractionCommandParametersAsync( - e, - context, - method, - e.Interaction.Data.Options.First().Options.First().Options - ); - - await RunCommandAsync(context, method, args); - } - - await this.slashExecuted.InvokeAsync( - this, - new SlashCommandExecutedEventArgs { Context = context } - ); - } - catch (Exception ex) - { - await this.slashError.InvokeAsync( - this, - new SlashCommandErrorEventArgs { Context = context, Exception = ex } - ); - } - } - - //Handles autcomplete interactions - if (e.Interaction.Type == DiscordInteractionType.AutoComplete) - { - if (errored) - { - throw new InvalidOperationException( - "Slash commands failed to register properly on startup." - ); - } - - //Gets the method for the command - IEnumerable methods = commandMethods.Where(x => - x.CommandId == e.Interaction.Data.Id - ); - IEnumerable groups = groupCommands.Where(x => - x.CommandId == e.Interaction.Data.Id - ); - IEnumerable subgroups = subGroupCommands.Where(x => - x.CommandId == e.Interaction.Data.Id - ); - if (!methods.Any() && !groups.Any() && !subgroups.Any()) - { - throw new InvalidOperationException( - "An autocomplete interaction was created, but no command was registered for it." - ); - } - - if (methods.Any()) - { - MethodInfo method = methods.First().Method; - - IEnumerable? options = e.Interaction.Data.Options; - //Gets the focused option - DiscordInteractionDataOption focusedOption = options.First(o => o.Focused); - ParameterInfo parameter = method - .GetParameters() - .Skip(1) - .First(p => - p.GetCustomAttribute().Name == focusedOption.Name - ); - await RunAutocompleteAsync(e.Interaction, parameter, options, focusedOption); - } - - if (groups.Any()) - { - DiscordInteractionDataOption command = e.Interaction.Data.Options.First(); - MethodInfo method = groups - .First() - .Methods.First(x => x.Key == command.Name) - .Value; - - IEnumerable options = command.Options; - DiscordInteractionDataOption focusedOption = options.First(o => o.Focused); - ParameterInfo parameter = method - .GetParameters() - .Skip(1) - .First(p => - p.GetCustomAttribute().Name == focusedOption.Name - ); - await RunAutocompleteAsync(e.Interaction, parameter, options, focusedOption); - } - - if (subgroups.Any()) - { - DiscordInteractionDataOption command = e.Interaction.Data.Options.First(); - GroupCommand group = subgroups - .First() - .SubCommands.First(x => x.Name == command.Name); - MethodInfo method = group - .Methods.First(x => x.Key == command.Options.First().Name) - .Value; - - IEnumerable options = command - .Options.First() - .Options; - DiscordInteractionDataOption focusedOption = options.First(o => o.Focused); - ParameterInfo parameter = method - .GetParameters() - .Skip(1) - .First(p => - p.GetCustomAttribute().Name == focusedOption.Name - ); - await RunAutocompleteAsync(e.Interaction, parameter, options, focusedOption); - } - } - }); - return Task.CompletedTask; - } - - internal Task ContextMenuHandler(DiscordClient client, ContextMenuInteractionCreatedEventArgs e) - { - _ = Task.Run(async () => - { - //Creates the context - ContextMenuContext context = - new() - { - Interaction = e.Interaction, - Channel = e.Interaction.Channel, - Client = client, - Services = this.services, - CommandName = e.Interaction.Data.Name, - SlashCommandsExtension = 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, - }; - - if ( - e.Interaction.Guild != null - && e.TargetUser != null - && e.Interaction.Guild.Members.TryGetValue( - e.TargetUser.Id, - out DiscordMember? member - ) - ) - { - context.TargetMember = member; - } - - try - { - if (errored) - { - throw new InvalidOperationException( - "Context menus failed to register properly on startup." - ); - } - - //Gets the method for the command - ContextMenuCommand? method = - contextMenuCommands.FirstOrDefault(x => x.CommandId == e.Interaction.Data.Id) - ?? throw new InvalidOperationException( - "A context menu was executed, but no command was registered for it." - ); - await RunCommandAsync(context, method.Method, new[] { context }); - - await this.contextMenuExecuted.InvokeAsync( - this, - new ContextMenuExecutedEventArgs { Context = context } - ); - } - catch (Exception ex) - { - await this.contextMenuErrored.InvokeAsync( - this, - new ContextMenuErrorEventArgs { Context = context, Exception = ex } - ); - } - }); - - return Task.CompletedTask; - } - - internal async Task RunCommandAsync( - BaseContext context, - MethodInfo method, - IEnumerable args - ) - { - //Accounts for lifespans - SlashModuleLifespan moduleLifespan = - ( - method.DeclaringType.GetCustomAttribute() != null - ? method - .DeclaringType.GetCustomAttribute() - ?.Lifespan - : SlashModuleLifespan.Transient - ) ?? SlashModuleLifespan.Transient; - object classInstance = moduleLifespan switch //Accounts for static methods and adds DI - { - // Accounts for static methods and adds DI - SlashModuleLifespan.Scoped => method.IsStatic - ? ActivatorUtilities.CreateInstance( - this.services.CreateScope().ServiceProvider, - method.DeclaringType - ) - : CreateInstance(method.DeclaringType, this.services.CreateScope().ServiceProvider), - // Accounts for static methods and adds DI - SlashModuleLifespan.Transient => method.IsStatic - ? ActivatorUtilities.CreateInstance(this.services, method.DeclaringType) - : CreateInstance(method.DeclaringType, this.services), - // If singleton, gets it from the singleton list - SlashModuleLifespan.Singleton => singletonModules.First(x => - ReferenceEquals(x.GetType(), method.DeclaringType) - ), - // TODO: Use a more specific exception type - _ => throw new Exception( - $"An unknown {nameof(SlashModuleLifespanAttribute)} scope was specified on command {context.CommandName}" - ), - }; - - ApplicationCommandModule module = null; - if (classInstance is ApplicationCommandModule mod) - { - module = mod; - } - - //Slash commands - if (context is InteractionContext slashContext) - { - await this.slashInvoked.InvokeAsync( - this, - new SlashCommandInvokedEventArgs { Context = slashContext } - ); - - await RunPreexecutionChecksAsync(method, slashContext); - - //Runs BeforeExecution and accounts for groups that don't inherit from ApplicationCommandModule - bool shouldExecute = await ( - module?.BeforeSlashExecutionAsync(slashContext) ?? Task.FromResult(true) - ); - - if (shouldExecute) - { - await (Task)method.Invoke(classInstance, args.ToArray()); - - //Runs AfterExecution and accounts for groups that don't inherit from ApplicationCommandModule - await (module?.AfterSlashExecutionAsync(slashContext) ?? Task.CompletedTask); - } - } - //Context menus - if (context is ContextMenuContext CMContext) - { - await this.contextMenuInvoked.InvokeAsync( - this, - new ContextMenuInvokedEventArgs() { Context = CMContext } - ); - - await RunPreexecutionChecksAsync(method, CMContext); - - //This null check actually shouldn't be necessary for context menus but I'll keep it in just in case - bool shouldExecute = await ( - module?.BeforeContextMenuExecutionAsync(CMContext) ?? Task.FromResult(true) - ); - - if (shouldExecute) - { - await (Task)method.Invoke(classInstance, args.ToArray()); - - await (module?.AfterContextMenuExecutionAsync(CMContext) ?? Task.CompletedTask); - } - } - } - - //Property injection copied over from CommandsNext - internal static object CreateInstance(Type t, IServiceProvider services) - { - TypeInfo ti = t.GetTypeInfo(); - ConstructorInfo[] 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." - ); - } - - ConstructorInfo constructor = constructors[0]; - ParameterInfo[] constructorArgs = constructor.GetParameters(); - object[] 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 (int i = 0; i < args.Length; i++) - { - args[i] = services.GetRequiredService(constructorArgs[i].ParameterType); - } - } - - object? moduleInstance = Activator.CreateInstance(t, args); - - // inject into properties - IEnumerable props = t.GetRuntimeProperties() - .Where(xp => - xp.CanWrite - && xp.SetMethod != null - && !xp.SetMethod.IsStatic - && xp.SetMethod.IsPublic - ); - foreach (PropertyInfo? prop in props) - { - if (prop.GetCustomAttribute() != null) - { - continue; - } - - object? service = services.GetService(prop.PropertyType); - if (service == null) - { - continue; - } - - prop.SetValue(moduleInstance, service); - } - - // inject into fields - IEnumerable fields = t.GetRuntimeFields() - .Where(xf => !xf.IsInitOnly && !xf.IsStatic && xf.IsPublic); - foreach (FieldInfo? field in fields) - { - if (field.GetCustomAttribute() != null) - { - continue; - } - - object? service = services.GetService(field.FieldType); - if (service == null) - { - continue; - } - - field.SetValue(moduleInstance, service); - } - - return moduleInstance; - } - - //Parses slash command parameters - private async Task> ResolveInteractionCommandParametersAsync( - InteractionCreatedEventArgs e, - InteractionContext context, - MethodInfo method, - IEnumerable options - ) - { - List args = [context]; - IEnumerable parameters = method.GetParameters().Skip(1); - - for (int i = 0; i < parameters.Count(); i++) - { - ParameterInfo parameter = parameters.ElementAt(i); - - //Accounts for optional arguments without values given - if ( - parameter.IsOptional - && ( - !options?.Any(x => - x.Name.Equals( - parameter.GetCustomAttribute().Name, - StringComparison.InvariantCultureIgnoreCase - ) - ) ?? true - ) - ) - { - args.Add(parameter.DefaultValue); - } - else - { - DiscordInteractionDataOption option = options.Single(x => - x.Name.Equals( - parameter.GetCustomAttribute().Name, - StringComparison.InvariantCultureIgnoreCase - ) - ); - - //Checks the type and casts/references resolved and adds the value to the list - //This can probably reference the slash command's type property that didn't exist when I wrote this and it could use a cleaner switch instead, but if it works it works - 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 (Nullable.GetUnderlyingType(parameter.ParameterType)?.IsEnum == true) - { - args.Add( - Enum.Parse( - Nullable.GetUnderlyingType(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(TimeSpan?)) - { - string? value = option.Value.ToString(); - if (value == "0") - { - args.Add(TimeSpan.Zero); - continue; - } - if ( - int.TryParse( - value, - NumberStyles.Number, - CultureInfo.InvariantCulture, - out _ - ) - ) - { - args.Add(null); - continue; - } - value = value.ToLowerInvariant(); - - if (TimeSpan.TryParse(value, CultureInfo.InvariantCulture, out TimeSpan result)) - { - args.Add(result); - continue; - } - string[] gps = ["days", "hours", "minutes", "seconds"]; - Match mtc = GetTimeSpanRegex().Match(value); - if (!mtc.Success) - { - args.Add(null); - continue; - } - - int d = 0; - int h = 0; - int m = 0; - int s = 0; - foreach (string gp in gps) - { - string gpc = mtc.Groups[gp].Value; - if (string.IsNullOrWhiteSpace(gpc)) - { - continue; - } - - gpc = gpc.Trim(); - - char gpt = gpc[^1]; - int.TryParse( - gpc[..^1], - NumberStyles.Integer, - CultureInfo.InvariantCulture, - out int val - ); - switch (gpt) - { - case 'd': - d = val; - break; - - case 'h': - h = val; - break; - - case 'm': - m = val; - break; - - case 's': - s = val; - break; - } - } - result = new TimeSpan(d, h, m, s); - args.Add(result); - } - 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 DiscordMember? member - ) - ) - { - args.Add(member); - } - else if ( - e.Interaction.Data.Resolved.Users != null - && e.Interaction.Data.Resolved.Users.TryGetValue( - (ulong)option.Value, - out DiscordUser? 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 DiscordChannel? 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 DiscordRole? role - ) - ) - { - args.Add(role); - } - else - { - args.Add(e.Interaction.Guild.Roles.GetValueOrDefault((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 DiscordRole? role - ) - ) - { - args.Add(role); - } - else if ( - e.Interaction.Data.Resolved.Members != null - && e.Interaction.Data.Resolved.Members.TryGetValue( - (ulong)option.Value, - out DiscordMember? member - ) - ) - { - args.Add(member); - } - else if ( - e.Interaction.Data.Resolved.Users != null - && e.Interaction.Data.Resolved.Users.TryGetValue( - (ulong)option.Value, - out DiscordUser? user - ) - ) - { - args.Add(user); - } - else - { - throw new ArgumentException("Error resolving mentionable option."); - } - } - else if (parameter.ParameterType == typeof(DiscordEmoji)) - { - string? value = option.Value.ToString(); - - if ( - DiscordEmoji.TryFromUnicode(this.Client, value, out DiscordEmoji? emoji) - || DiscordEmoji.TryFromName(this.Client, value, out emoji) - ) - { - args.Add(emoji); - } - else - { - throw new ArgumentException("Error parsing emoji parameter."); - } - } - else if (parameter.ParameterType == typeof(DiscordAttachment)) - { - if ( - e.Interaction.Data.Resolved.Attachments?.ContainsKey((ulong)option.Value) - ?? false - ) - { - DiscordAttachment attachment = e.Interaction.Data.Resolved.Attachments[ - (ulong)option.Value - ]; - args.Add(attachment); - } - else - { - this.Client.Logger.LogError( - "Missing attachment in resolved data. This is an issue with Discord." - ); - } - } - else - { - throw new ArgumentException("Error resolving interaction."); - } - } - } - - return args; - } - - //Runs pre-execution checks - private static async Task RunPreexecutionChecksAsync(MethodInfo method, BaseContext context) - { - if (context is InteractionContext ctx) - { - //Gets all attributes from parent classes as well and stuff - List attributes = - [ - .. method.GetCustomAttributes(true), - .. 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() - ); - } - } - - Dictionary dict = []; - foreach (SlashCheckBaseAttribute att in attributes) - { - //Runs the check and adds the result to a list - bool 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) - { - List attributes = - [ - .. method.GetCustomAttributes(true), - .. 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() - ); - } - } - - Dictionary dict = []; - foreach (ContextMenuCheckBaseAttribute att in attributes) - { - //Runs the check and adds the result to a list - bool 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(), - }; - } - } - } - - //Actually handles autocomplete interactions - private async Task RunAutocompleteAsync( - DiscordInteraction interaction, - ParameterInfo parameter, - IEnumerable options, - DiscordInteractionDataOption focusedOption - ) - { - AutocompleteContext context = - new() - { - Interaction = interaction, - Client = this.Client, - Services = this.services, - SlashCommandsExtension = this, - Guild = interaction.Guild, - Channel = interaction.Channel, - User = interaction.User, - Options = options.ToList(), - FocusedOption = focusedOption, - }; - - try - { - //Gets the provider - Type? provider = parameter.GetCustomAttribute()?.Provider; - if (provider == null) - { - return; - } - - MethodInfo? providerMethod = provider.GetMethod(nameof(IAutocompleteProvider.Provider)); - object providerInstance = ActivatorUtilities.CreateInstance(this.services, provider); - - IEnumerable choices = await (Task< - IEnumerable - >) - providerMethod.Invoke(providerInstance, new[] { context }); - - if (choices.Count() > 25) - { - choices = choices.Take(25); - this.Client.Logger.LogWarning( - """Autocomplete provider "{provider}" returned more than 25 choices. Only the first 25 are passed to Discord.""", - nameof(provider) - ); - } - - await interaction.CreateResponseAsync( - DiscordInteractionResponseType.AutoCompleteResult, - new DiscordInteractionResponseBuilder().AddAutoCompleteChoices(choices) - ); - - await this.autocompleteExecuted.InvokeAsync( - this, - new() { Context = context, ProviderType = provider } - ); - } - catch (Exception ex) - { - await this.autocompleteErrored.InvokeAsync( - this, - new AutocompleteErrorEventArgs() - { - Exception = ex, - Context = context, - ProviderType = parameter.GetCustomAttribute()?.Provider, - } - ); - } - } - - #endregion - - /// - /// Refreshes your commands, used for refreshing choice providers or applying commands registered after the ready event on the discord client. - /// Should only be run on the slash command extension linked to shard 0 if sharding. - /// Not recommended and should be avoided since it can make slash commands be unresponsive for a while. - /// - public async Task RefreshCommandsAsync() - { - commandMethods.Clear(); - groupCommands.Clear(); - subGroupCommands.Clear(); - registeredCommands.Clear(); - - await Update(); - } - - /// - /// Fires when the execution of a slash command fails. - /// - public event AsyncEventHandler< - SlashCommandsExtension, - SlashCommandErrorEventArgs - > SlashCommandErrored - { - add => this.slashError.Register(value); - remove => this.slashError.Unregister(value); - } - private AsyncEvent slashError; - - /// - /// Fired when a slash command has been received and is to be executed - /// - public event AsyncEventHandler< - SlashCommandsExtension, - SlashCommandInvokedEventArgs - > SlashCommandInvoked - { - add => this.slashInvoked.Register(value); - remove => this.slashInvoked.Unregister(value); - } - private AsyncEvent slashInvoked; - - /// - /// Fires when the execution of a slash command is successful. - /// - public event AsyncEventHandler< - SlashCommandsExtension, - SlashCommandExecutedEventArgs - > 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< - SlashCommandsExtension, - ContextMenuErrorEventArgs - > ContextMenuErrored - { - add => this.contextMenuErrored.Register(value); - remove => this.contextMenuErrored.Unregister(value); - } - private AsyncEvent contextMenuErrored; - - /// - /// Fired when a context menu has been received and is to be executed - /// - public event AsyncEventHandler< - SlashCommandsExtension, - ContextMenuInvokedEventArgs - > ContextMenuInvoked - { - add => this.contextMenuInvoked.Register(value); - remove => this.contextMenuInvoked.Unregister(value); - } - private AsyncEvent contextMenuInvoked; - - /// - /// Fire when the execution of a context menu is successful. - /// - public event AsyncEventHandler< - SlashCommandsExtension, - ContextMenuExecutedEventArgs - > ContextMenuExecuted - { - add => this.contextMenuExecuted.Register(value); - remove => this.contextMenuExecuted.Unregister(value); - } - private AsyncEvent contextMenuExecuted; - - public event AsyncEventHandler< - SlashCommandsExtension, - AutocompleteErrorEventArgs - > AutocompleteErrored - { - add => this.autocompleteErrored.Register(value); - remove => this.autocompleteErrored.Register(value); - } - private AsyncEvent autocompleteErrored; - - public event AsyncEventHandler< - SlashCommandsExtension, - AutocompleteExecutedEventArgs - > AutocompleteExecuted - { - add => this.autocompleteExecuted.Register(value); - remove => this.autocompleteExecuted.Register(value); - } - private AsyncEvent autocompleteExecuted; - - public void Dispose() - { - this.slashError?.UnregisterAll(); - this.slashInvoked?.UnregisterAll(); - this.slashExecuted?.UnregisterAll(); - this.contextMenuErrored?.UnregisterAll(); - this.contextMenuExecuted?.UnregisterAll(); - this.contextMenuInvoked?.UnregisterAll(); - this.autocompleteErrored?.UnregisterAll(); - this.autocompleteExecuted?.UnregisterAll(); - - // Satisfy rule CA1816. Can be removed if this class is sealed. - GC.SuppressFinalize(this); - } - - [GeneratedRegex( - @"^(?\d+d\s*)?(?\d{1,2}h\s*)?(?\d{1,2}m\s*)?(?\d{1,2}s\s*)?$", - RegexOptions.ECMAScript - )] - private static partial Regex GetTimeSpanRegex(); -} - -//I'm not sure if creating separate classes is the cleanest thing here but I can't think of anything else so these stay - -internal class CommandMethod -{ - public ulong CommandId { get; set; } - public string Name { get; set; } - public MethodInfo Method { get; set; } -} - -internal class GroupCommand -{ - public ulong CommandId { get; set; } - public string Name { get; set; } - public List> Methods { get; set; } = null; -} - -internal class SubGroupCommand -{ - public ulong CommandId { get; set; } - public string Name { get; set; } - public List SubCommands { get; set; } = []; -} - -internal class ContextMenuCommand -{ - public ulong CommandId { get; set; } - public string Name { get; set; } - public MethodInfo Method { get; set; } -} +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using DSharpPlus.AsyncEvents; +using DSharpPlus.Entities; +using DSharpPlus.EventArgs; +using DSharpPlus.Exceptions; +using DSharpPlus.SlashCommands.EventArgs; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace DSharpPlus.SlashCommands; + +/// +/// A class that handles slash commands for a client. +/// +[Obsolete( + "DSharpPlus.SlashCommands is obsolete. Please consider using the new DSharpPlus.Commands extension instead." +)] +public sealed partial class SlashCommandsExtension : IDisposable +{ + //A list of methods for top level commands + private static List commandMethods { get; set; } = []; + + //List of groups + private static List groupCommands { get; set; } = []; + + //List of groups with subgroups + private static List subGroupCommands { get; set; } = []; + + //List of context menus + private static List contextMenuCommands { get; set; } = []; + + //Singleton modules + private static List singletonModules { get; set; } = []; + + //List of modules to register + private List> updateList { get; set; } = []; + + //Set to true if anything fails when registering + private static bool errored { get; set; } = false; + + private readonly IServiceProvider services; + + /// + /// Gets a list of registered commands. The key is the guild id (null if global). + /// + public static IReadOnlyList< + KeyValuePair> + > RegisteredCommands => registeredCommands; + + public DiscordClient Client { get; private set; } + + private static readonly List< + KeyValuePair> + > registeredCommands = []; + + internal SlashCommandsExtension(IServiceProvider serviceProvider) => + this.services = serviceProvider; + + /// + /// Runs setup. DO NOT RUN THIS MANUALLY. DO NOT DO ANYTHING WITH THIS. + /// + /// The client to setup on. + internal void Setup(DiscordClient client) + { + if (this.Client != null) + { + throw new InvalidOperationException("What did I tell you?"); + } + + this.Client = client; + + DefaultClientErrorHandler errorHandler = new(client.Logger); + + this.slashError = new AsyncEvent( + errorHandler + ); + this.slashInvoked = new AsyncEvent( + errorHandler + ); + this.slashExecuted = new AsyncEvent( + errorHandler + ); + this.contextMenuErrored = new AsyncEvent( + errorHandler + ); + this.contextMenuExecuted = new AsyncEvent< + SlashCommandsExtension, + ContextMenuExecutedEventArgs + >(errorHandler); + this.contextMenuInvoked = new AsyncEvent< + SlashCommandsExtension, + ContextMenuInvokedEventArgs + >(errorHandler); + this.autocompleteErrored = new AsyncEvent< + SlashCommandsExtension, + AutocompleteErrorEventArgs + >(errorHandler); + this.autocompleteExecuted = new AsyncEvent< + SlashCommandsExtension, + AutocompleteExecutedEventArgs + >(errorHandler); + } + + /// + /// 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 : ApplicationCommandModule => this.updateList.Add(new(guildId, 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(ApplicationCommandModule).IsAssignableFrom(type)) + { + throw new ArgumentException( + "Command classes have to inherit from ApplicationCommandModule", + nameof(type) + ); + } + + this.updateList.Add(new(guildId, type)); + } + + /// + /// Registers all command classes from a given assembly. + /// + /// Assembly to register command classes from. + /// The guild id to register it on. If you want global commands, leave it null. + public void RegisterCommands(Assembly assembly, ulong? guildId = null) + { + IEnumerable types = assembly.ExportedTypes.Where(xt => + typeof(ApplicationCommandModule).IsAssignableFrom(xt) && !xt.GetTypeInfo().IsNested + ); + + foreach (Type? xt in types) + { + RegisterCommands(xt, guildId); + } + } + + //To be run on ready + internal Task Update(DiscordClient client, SessionCreatedEventArgs e) => Update(); + + //Actual method for registering, used for RegisterCommands and on Ready + internal Task Update() + { + //Groups commands by guild id or global + foreach (ulong? key in this.updateList.Select(x => x.Key).Distinct()) + { + RegisterCommands(this.updateList.Where(x => x.Key == key).Select(x => x.Value), key); + } + + return Task.CompletedTask; + } + + #region Registering + + //Method for registering commands for a target from modules + private void RegisterCommands(IEnumerable types, ulong? guildId) + { + //Initialize empty lists to be added to the global ones at the end + List commandMethodsToAdd = []; + List groupCommandsToAdd = []; + List subGroupCommandsToAdd = []; + List contextMenuCommandsToAdd = []; + List updateList = []; + + _ = Task.Run(async () => + { + //Iterates over all the modules + foreach (Type type in types) + { + try + { + TypeInfo module = type.GetTypeInfo(); + List classes = []; + + //Add module to classes list if it's a group + if (module.GetCustomAttribute() != null) + { + classes.Add(module); + } + else + { + //Otherwise add the nested groups + classes = module + .DeclaredNestedTypes.Where(x => + x.GetCustomAttribute() != null + ) + .ToList(); + } + + //Handles groups + foreach (TypeInfo subclassinfo in classes) + { + //Gets the attribute and methods in the group + + bool allowDMs = + subclassinfo.GetCustomAttribute() is null; + DiscordPermissions? v2Permissions = new(subclassinfo + .GetCustomAttribute() + ?.Permissions ?? []); + + SlashCommandGroupAttribute? groupAttribute = + subclassinfo.GetCustomAttribute(); + IEnumerable submethods = subclassinfo.DeclaredMethods.Where(x => + x.GetCustomAttribute() != null + ); + IEnumerable subclasses = subclassinfo.DeclaredNestedTypes.Where( + x => x.GetCustomAttribute() != null + ); + if (subclasses.Any() && submethods.Any()) + { + throw new ArgumentException( + "Slash command groups cannot have both subcommands and subgroups!" + ); + } + + //Group context menus + IEnumerable contextMethods = subclassinfo.DeclaredMethods.Where( + x => x.GetCustomAttribute() != null + ); + AddContextMenus(contextMethods); + + //Initializes the command + DiscordApplicationCommand payload = + new( + groupAttribute.Name, + groupAttribute.Description, + defaultPermission: groupAttribute.DefaultPermission, + allowDMUsage: allowDMs, + defaultMemberPermissions: v2Permissions, + nsfw: groupAttribute.NSFW + ); + + List> commandmethods = []; + //Handles commands in the group + foreach (MethodInfo? submethod in submethods) + { + SlashCommandAttribute? commandAttribute = + submethod.GetCustomAttribute(); + + //Gets the paramaters and accounts for InteractionContext + ParameterInfo[] parameters = submethod.GetParameters(); + if ( + parameters?.Length is null or 0 + || !ReferenceEquals( + parameters.First().ParameterType, + typeof(InteractionContext) + ) + ) + { + throw new ArgumentException( + $"The first argument must be an InteractionContext!" + ); + } + + parameters = parameters.Skip(1).ToArray(); + + //Check if the ReturnType can be safely casted to a Task later on execution + if (!typeof(Task).IsAssignableFrom(submethod.ReturnType)) + { + throw new InvalidOperationException( + "The method has to return a Task or Task<> value" + ); + } + + List options = + await ParseParametersAsync(parameters, guildId); + + IReadOnlyDictionary nameLocalizations = + GetNameLocalizations(submethod); + IReadOnlyDictionary descriptionLocalizations = + GetDescriptionLocalizations(submethod); + IReadOnlyList? integrationTypes = + GetInteractionCommandInstallTypes(submethod); + IReadOnlyList? contexts = + GetInteractionCommandAllowedContexts(submethod); + + //Creates the subcommand and adds it to the main command + DiscordApplicationCommandOption subpayload = + new( + commandAttribute.Name, + commandAttribute.Description, + DiscordApplicationCommandOptionType.SubCommand, + null, + null, + options, + name_localizations: nameLocalizations, + description_localizations: descriptionLocalizations + ); + payload = new DiscordApplicationCommand( + payload.Name, + payload.Description, + payload.Options?.Append(subpayload) ?? new[] { subpayload }, + payload.DefaultPermission, + allowDMUsage: allowDMs, + defaultMemberPermissions: v2Permissions, + nsfw: payload.NSFW, + integrationTypes: integrationTypes, + contexts: contexts + ); + + //Adds it to the method lists + commandmethods.Add(new(commandAttribute.Name, submethod)); + groupCommandsToAdd.Add( + new() { Name = groupAttribute.Name, Methods = commandmethods } + ); + } + + SubGroupCommand command = new() { Name = groupAttribute.Name }; + //Handles subgroups + foreach (TypeInfo? subclass in subclasses) + { + SlashCommandGroupAttribute? subGroupAttribute = + subclass.GetCustomAttribute(); + //I couldn't think of more creative naming + IEnumerable subsubmethods = subclass.DeclaredMethods.Where( + x => x.GetCustomAttribute() != null + ); + + List options = []; + + List> currentMethods = []; + + //Similar to the one for regular groups + foreach (MethodInfo? subsubmethod in subsubmethods) + { + List suboptions = []; + SlashCommandAttribute? commatt = + subsubmethod.GetCustomAttribute(); + ParameterInfo[] parameters = subsubmethod.GetParameters(); + if ( + parameters?.Length is null or 0 + || !ReferenceEquals( + parameters.First().ParameterType, + typeof(InteractionContext) + ) + ) + { + throw new ArgumentException( + $"The first argument must be an InteractionContext!" + ); + } + + parameters = parameters.Skip(1).ToArray(); + suboptions = + [ + .. suboptions, + .. await ParseParametersAsync(parameters, guildId), + ]; + + IReadOnlyDictionary nameLocalizations = + GetNameLocalizations(subsubmethod); + IReadOnlyDictionary descriptionLocalizations = + GetDescriptionLocalizations(subsubmethod); + + DiscordApplicationCommandOption subsubpayload = + new( + commatt.Name, + commatt.Description, + DiscordApplicationCommandOptionType.SubCommand, + null, + null, + suboptions, + name_localizations: nameLocalizations, + description_localizations: descriptionLocalizations + ); + options.Add(subsubpayload); + + commandmethods.Add(new(commatt.Name, subsubmethod)); + currentMethods.Add(new(commatt.Name, subsubmethod)); + } + + //Subgroups Context Menus + IEnumerable subContextMethods = + subclass.DeclaredMethods.Where(x => + x.GetCustomAttribute() != null + ); + AddContextMenus(subContextMethods); + + //Adds the group to the command and method lists + DiscordApplicationCommandOption subpayload = + new( + subGroupAttribute.Name, + subGroupAttribute.Description, + DiscordApplicationCommandOptionType.SubCommandGroup, + null, + null, + options + ); + command.SubCommands.Add( + new() { Name = subGroupAttribute.Name, Methods = currentMethods } + ); + payload = new DiscordApplicationCommand( + payload.Name, + payload.Description, + payload.Options?.Append(subpayload) ?? new[] { subpayload }, + payload.DefaultPermission, + allowDMUsage: allowDMs, + defaultMemberPermissions: v2Permissions, + nsfw: payload.NSFW + ); + + //Accounts for lifespans for the sub group + if ( + subclass.GetCustomAttribute() + is not null + and { Lifespan: SlashModuleLifespan.Singleton } + ) + { + singletonModules.Add(CreateInstance(subclass, this.services)); + } + } + + if (command.SubCommands.Count != 0) + { + subGroupCommandsToAdd.Add(command); + } + + updateList.Add(payload); + + //Accounts for lifespans + if ( + subclassinfo.GetCustomAttribute() + is not null + and { Lifespan: SlashModuleLifespan.Singleton } + ) + { + singletonModules.Add(CreateInstance(subclassinfo, this.services)); + } + } + + //Handles methods, only if the module isn't a group itself + if (module.GetCustomAttribute() is null) + { + //Slash commands (again, similar to the one for groups) + IEnumerable methods = module.DeclaredMethods.Where(x => + x.GetCustomAttribute() != null + ); + + foreach (MethodInfo? method in methods) + { + SlashCommandAttribute? commandattribute = + method.GetCustomAttribute(); + + ParameterInfo[] parameters = method.GetParameters(); + if ( + parameters?.Length is null or 0 + || !ReferenceEquals( + parameters.FirstOrDefault()?.ParameterType, + typeof(InteractionContext) + ) + ) + { + throw new ArgumentException( + $"The first argument must be an InteractionContext!" + ); + } + + parameters = parameters.Skip(1).ToArray(); + List options = + await ParseParametersAsync(parameters, guildId); + + commandMethodsToAdd.Add( + new() { Method = method, Name = commandattribute.Name } + ); + + IReadOnlyDictionary nameLocalizations = + GetNameLocalizations(method); + IReadOnlyDictionary descriptionLocalizations = + GetDescriptionLocalizations(method); + IReadOnlyList? integrationTypes = + GetInteractionCommandInstallTypes(method); + IReadOnlyList? contexts = + GetInteractionCommandAllowedContexts(method); + + bool allowDMs = + ( + method.GetCustomAttribute() + ?? method.DeclaringType.GetCustomAttribute() + ) + is null; + DiscordPermissions? v2Permissions = new(( + method.GetCustomAttribute() + ?? method.DeclaringType.GetCustomAttribute() + )?.Permissions ?? []); + + DiscordApplicationCommand payload = + new( + commandattribute.Name, + commandattribute.Description, + options, + commandattribute.DefaultPermission, + name_localizations: nameLocalizations, + description_localizations: descriptionLocalizations, + allowDMUsage: allowDMs, + defaultMemberPermissions: v2Permissions, + nsfw: commandattribute.NSFW, + integrationTypes: integrationTypes, + contexts: contexts + ); + updateList.Add(payload); + } + + //Context Menus + IEnumerable contextMethods = module.DeclaredMethods.Where(x => + x.GetCustomAttribute() != null + ); + AddContextMenus(contextMethods); + + //Accounts for lifespans + if ( + module.GetCustomAttribute() + is not null + and { Lifespan: SlashModuleLifespan.Singleton } + ) + { + singletonModules.Add(CreateInstance(module, this.services)); + } + } + + void AddContextMenus(IEnumerable contextMethods) + { + foreach (MethodInfo contextMethod in contextMethods) + { + ContextMenuAttribute? contextAttribute = + contextMethod.GetCustomAttribute(); + bool allowDMUsage = + ( + contextMethod.GetCustomAttribute() + ?? contextMethod.DeclaringType.GetCustomAttribute() + ) + is null; + DiscordPermissions? permissions = new(( + contextMethod.GetCustomAttribute() + ?? contextMethod.DeclaringType.GetCustomAttribute() + )?.Permissions ?? []); + IReadOnlyList? integrationTypes = + GetInteractionCommandInstallTypes(contextMethod); + IReadOnlyList? contexts = + GetInteractionCommandAllowedContexts(contextMethod); + DiscordApplicationCommand command = + new( + contextAttribute.Name, + null, + type: contextAttribute.Type, + defaultPermission: contextAttribute.DefaultPermission, + allowDMUsage: allowDMUsage, + defaultMemberPermissions: permissions, + nsfw: contextAttribute.NSFW, + integrationTypes: integrationTypes, + contexts: contexts + ); + + ParameterInfo[] parameters = contextMethod.GetParameters(); + if ( + parameters?.Length is null or 0 + || !ReferenceEquals( + parameters.FirstOrDefault()?.ParameterType, + typeof(ContextMenuContext) + ) + ) + { + throw new ArgumentException( + $"The first argument must be a ContextMenuContext!" + ); + } + + if (parameters.Length > 1) + { + throw new ArgumentException( + $"A context menu cannot have parameters!" + ); + } + + contextMenuCommandsToAdd.Add( + new ContextMenuCommand + { + Method = contextMethod, + Name = contextAttribute.Name, + } + ); + + updateList.Add(command); + } + } + } + catch (Exception ex) + { + //This isn't really much more descriptive but I added a separate case for it anyway + if (ex is BadRequestException brex) + { + this.Client.Logger.LogCritical( + brex, + "There was an error registering application commands: {JsonError}", + brex.JsonMessage + ); + } + else + { + this.Client.Logger.LogCritical( + ex, + $"There was an error registering application commands" + ); + } + + errored = true; + } + } + + if (!errored) + { + try + { + IEnumerable commands; + //Creates a guild command if a guild id is specified, otherwise global + commands = guildId is null + ? await this.Client.BulkOverwriteGlobalApplicationCommandsAsync(updateList) + : await this.Client.BulkOverwriteGuildApplicationCommandsAsync( + guildId.Value, + updateList + ); + + //Checks against the ids and adds them to the command method lists + foreach (DiscordApplicationCommand command in commands) + { + if (commandMethodsToAdd.Any(x => x.Name == command.Name)) + { + commandMethodsToAdd.First(x => x.Name == command.Name).CommandId = + command.Id; + } + else if (groupCommandsToAdd.Any(x => x.Name == command.Name)) + { + groupCommandsToAdd.First(x => x.Name == command.Name).CommandId = + command.Id; + } + else if (subGroupCommandsToAdd.Any(x => x.Name == command.Name)) + { + subGroupCommandsToAdd.First(x => x.Name == command.Name).CommandId = + command.Id; + } + else if (contextMenuCommandsToAdd.Any(x => x.Name == command.Name)) + { + contextMenuCommandsToAdd.First(x => x.Name == command.Name).CommandId = + command.Id; + } + } + //Adds to the global lists finally + commandMethods.AddRange(commandMethodsToAdd); + groupCommands.AddRange(groupCommandsToAdd); + subGroupCommands.AddRange(subGroupCommandsToAdd); + contextMenuCommands.AddRange(contextMenuCommandsToAdd); + + registeredCommands.Add(new(guildId, commands.ToList())); + } + catch (Exception ex) + { + if (ex is BadRequestException brex) + { + this.Client.Logger.LogCritical( + brex, + "There was an error registering application commands: {JsonMessage}", + brex.JsonMessage + ); + } + else + { + this.Client.Logger.LogCritical( + ex, + $"There was an error registering application commands" + ); + } + + errored = true; + } + } + }); + } + + //Handles the parameters for a slash command + private async Task> ParseParametersAsync( + ParameterInfo[] parameters, + ulong? guildId + ) + { + List options = []; + foreach (ParameterInfo parameter in parameters) + { + //Gets the attribute + OptionAttribute? optionattribute = + parameter.GetCustomAttribute() + ?? throw new ArgumentException("Arguments must have the Option attribute!"); + + //Sets the type + Type type = parameter.ParameterType; + string commandName = + parameter.Member.GetCustomAttribute()?.Name + ?? parameter.Member.GetCustomAttribute().Name; + DiscordApplicationCommandOptionType parametertype = GetParameterType(commandName, type); + + //Handles choices + //From attributes + List choices = GetChoiceAttributesFromParameter( + parameter.GetCustomAttributes() + ); + //From enums + if ( + parameter.ParameterType.IsEnum + || Nullable.GetUnderlyingType(parameter.ParameterType)?.IsEnum == true + ) + { + choices = GetChoiceAttributesFromEnumParameter(parameter.ParameterType); + } + //From choice provider + IEnumerable choiceProviders = + parameter.GetCustomAttributes(); + if (choiceProviders.Any()) + { + choices = await GetChoiceAttributesFromProviderAsync(choiceProviders, guildId); + } + + IEnumerable? channelTypes = + parameter.GetCustomAttribute()?.ChannelTypes ?? null; + + object? minimumValue = parameter.GetCustomAttribute()?.Value ?? null; + object? maximumValue = parameter.GetCustomAttribute()?.Value ?? null; + + int? minimumLength = + parameter.GetCustomAttribute()?.Value ?? null; + int? maximumLength = + parameter.GetCustomAttribute()?.Value ?? null; + + IReadOnlyDictionary nameLocalizations = GetNameLocalizations(parameter); + IReadOnlyDictionary descriptionLocalizations = + GetDescriptionLocalizations(parameter); + + AutocompleteAttribute? autocompleteAttribute = + parameter.GetCustomAttribute(); + if ( + autocompleteAttribute != null + && autocompleteAttribute.Provider.GetMethod(nameof(IAutocompleteProvider.Provider)) + == null + ) + { + throw new ArgumentException( + "Autocomplete providers must inherit from IAutocompleteProvider." + ); + } + + options.Add( + new DiscordApplicationCommandOption( + optionattribute.Name, + optionattribute.Description, + parametertype, + !parameter.IsOptional, + choices, + null, + channelTypes, + autocompleteAttribute != null || optionattribute.Autocomplete, + minimumValue, + maximumValue, + nameLocalizations, + descriptionLocalizations, + minimumLength, + maximumLength + ) + ); + } + + return options; + } + + private static IReadOnlyList? GetInteractionCommandInstallTypes( + ICustomAttributeProvider method + ) + { + InteractionCommandInstallTypeAttribute[] attributes = + (InteractionCommandInstallTypeAttribute[]) + method.GetCustomAttributes(typeof(InteractionCommandInstallTypeAttribute), false); + return attributes.FirstOrDefault()?.InstallTypes; + } + + private static IReadOnlyList? GetInteractionCommandAllowedContexts( + ICustomAttributeProvider method + ) + { + InteractionCommandAllowedContextsAttribute[] attributes = + (InteractionCommandAllowedContextsAttribute[]) + method.GetCustomAttributes( + typeof(InteractionCommandAllowedContextsAttribute), + false + ); + return attributes.FirstOrDefault()?.AllowedContexts; + } + + private static IReadOnlyDictionary GetNameLocalizations( + ICustomAttributeProvider method + ) + { + NameLocalizationAttribute[] nameAttributes = (NameLocalizationAttribute[]) + method.GetCustomAttributes(typeof(NameLocalizationAttribute), false); + return nameAttributes.ToDictionary( + nameAttribute => nameAttribute.Locale, + nameAttribute => nameAttribute.Name + ); + } + + private static IReadOnlyDictionary GetDescriptionLocalizations( + ICustomAttributeProvider method + ) + { + DescriptionLocalizationAttribute[] descriptionAttributes = + (DescriptionLocalizationAttribute[]) + method.GetCustomAttributes(typeof(DescriptionLocalizationAttribute), false); + return descriptionAttributes.ToDictionary( + descriptionAttribute => descriptionAttribute.Locale, + descriptionAttribute => descriptionAttribute.Description + ); + } + + //Gets the choices from a choice provider + private async Task< + List + > GetChoiceAttributesFromProviderAsync( + IEnumerable customAttributes, + ulong? guildId + ) + { + List choices = []; + foreach (ChoiceProviderAttribute choiceProviderAttribute in customAttributes) + { + MethodInfo? method = choiceProviderAttribute.ProviderType.GetMethod( + nameof(IChoiceProvider.Provider) + ); + + if (method == null) + { + throw new ArgumentException("ChoiceProviders must inherit from IChoiceProvider."); + } + else + { + object? 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, this.services); + } + + //Gets the choices from the method + IEnumerable result = await (Task< + IEnumerable + >) + method.Invoke(instance, null); + + if (result.Any()) + { + choices.AddRange(result); + } + } + } + + return choices; + } + + //Gets choices from an enum + private static List GetChoiceAttributesFromEnumParameter( + Type enumParam + ) + { + List choices = []; + if (enumParam.IsGenericType && enumParam.GetGenericTypeDefinition() == typeof(Nullable<>)) + { + enumParam = Nullable.GetUnderlyingType(enumParam); + } + foreach (Enum enumValue in Enum.GetValues(enumParam)) + { + choices.Add( + new DiscordApplicationCommandOptionChoice(enumValue.GetName(), enumValue.ToString()) + ); + } + return choices; + } + + //Small method to get the parameter's type from its type + private static DiscordApplicationCommandOptionType GetParameterType( + string commandName, + Type type + ) => + type == typeof(string) ? DiscordApplicationCommandOptionType.String + : type == typeof(long) || type == typeof(long?) + ? DiscordApplicationCommandOptionType.Integer + : type == typeof(bool) || type == typeof(bool?) + ? DiscordApplicationCommandOptionType.Boolean + : type == typeof(double) || type == typeof(double?) + ? DiscordApplicationCommandOptionType.Number + : type == typeof(DiscordChannel) ? DiscordApplicationCommandOptionType.Channel + : type == typeof(DiscordUser) ? DiscordApplicationCommandOptionType.User + : type == typeof(DiscordRole) ? DiscordApplicationCommandOptionType.Role + : type == typeof(DiscordEmoji) ? DiscordApplicationCommandOptionType.String + : type == typeof(TimeSpan?) ? DiscordApplicationCommandOptionType.String + : type == typeof(SnowflakeObject) ? DiscordApplicationCommandOptionType.Mentionable + : type.IsEnum || Nullable.GetUnderlyingType(type)?.IsEnum == true + ? DiscordApplicationCommandOptionType.String + : type == typeof(DiscordAttachment) ? DiscordApplicationCommandOptionType.Attachment + : throw new ArgumentException( + $"Cannot convert type! (Command: {commandName}) Argument types must be string, long, bool, double, TimeSpan?, DiscordChannel, DiscordUser, DiscordRole, DiscordEmoji, DiscordAttachment, SnowflakeObject, or an Enum." + ); + + //Gets choices from choice attributes + private static List GetChoiceAttributesFromParameter( + IEnumerable choiceattributes + ) => + !choiceattributes.Any() + ? null + : choiceattributes + .Select(att => new DiscordApplicationCommandOptionChoice( + att.Name, + att.Value.ToString() + )) + .ToList(); + + #endregion + + #region Handling + + internal Task InteractionHandler(DiscordClient client, InteractionCreatedEventArgs e) + { + _ = Task.Run(async () => + { + if ( + e.Interaction is + { + Type: DiscordInteractionType.ApplicationCommand, + Data.Type: DiscordApplicationCommandType.SlashCommand + } + ) + { + StringBuilder qualifiedName = new(e.Interaction.Data.Name); + DiscordInteractionDataOption[] options = + e.Interaction.Data.Options?.ToArray() ?? []; + while (options.Length != 0) + { + DiscordInteractionDataOption firstOption = options[0]; + if ( + firstOption.Type + is not DiscordApplicationCommandOptionType.SubCommandGroup + and not DiscordApplicationCommandOptionType.SubCommand + ) + { + break; + } + + _ = qualifiedName.AppendFormat(" {0}", firstOption.Name); + options = firstOption.Options?.ToArray() ?? []; + } + + //Creates the context + InteractionContext context = + new() + { + Interaction = e.Interaction, + Channel = e.Interaction.Channel, + Guild = e.Interaction.Guild, + User = e.Interaction.User, + Client = client, + SlashCommandsExtension = this, + CommandName = e.Interaction.Data.Name, + QualifiedName = qualifiedName.ToString(), + InteractionId = e.Interaction.Id, + Token = e.Interaction.Token, + Services = this.services, + ResolvedUserMentions = e.Interaction.Data.Resolved?.Users?.Values.ToList(), + ResolvedRoleMentions = e.Interaction.Data.Resolved?.Roles?.Values.ToList(), + ResolvedChannelMentions = + e.Interaction.Data.Resolved?.Channels?.Values.ToList(), + Type = DiscordApplicationCommandType.SlashCommand, + }; + + try + { + if (errored) + { + throw new InvalidOperationException( + "Slash commands failed to register properly on startup." + ); + } + + //Gets the method for the command + IEnumerable methods = commandMethods.Where(x => + x.CommandId == e.Interaction.Data.Id + ); + IEnumerable groups = groupCommands.Where(x => + x.CommandId == e.Interaction.Data.Id + ); + IEnumerable subgroups = subGroupCommands.Where(x => + x.CommandId == e.Interaction.Data.Id + ); + if (!methods.Any() && !groups.Any() && !subgroups.Any()) + { + throw new InvalidOperationException( + "A slash command was executed, but no command was registered for it." + ); + } + + //Just read the code you'll get it + if (methods.Any()) + { + MethodInfo method = methods.First().Method; + + List args = await ResolveInteractionCommandParametersAsync( + e, + context, + method, + e.Interaction.Data.Options + ); + + await RunCommandAsync(context, method, args); + } + else if (groups.Any()) + { + DiscordInteractionDataOption command = e.Interaction.Data.Options.First(); + MethodInfo method = groups + .First() + .Methods.First(x => x.Key == command.Name) + .Value; + + List args = await ResolveInteractionCommandParametersAsync( + e, + context, + method, + e.Interaction.Data.Options.First().Options + ); + + await RunCommandAsync(context, method, args); + } + else if (subgroups.Any()) + { + DiscordInteractionDataOption command = e.Interaction.Data.Options.First(); + GroupCommand group = subgroups + .First() + .SubCommands.First(x => x.Name == command.Name); + MethodInfo method = group + .Methods.First(x => x.Key == command.Options.First().Name) + .Value; + + List args = await ResolveInteractionCommandParametersAsync( + e, + context, + method, + e.Interaction.Data.Options.First().Options.First().Options + ); + + await RunCommandAsync(context, method, args); + } + + await this.slashExecuted.InvokeAsync( + this, + new SlashCommandExecutedEventArgs { Context = context } + ); + } + catch (Exception ex) + { + await this.slashError.InvokeAsync( + this, + new SlashCommandErrorEventArgs { Context = context, Exception = ex } + ); + } + } + + //Handles autcomplete interactions + if (e.Interaction.Type == DiscordInteractionType.AutoComplete) + { + if (errored) + { + throw new InvalidOperationException( + "Slash commands failed to register properly on startup." + ); + } + + //Gets the method for the command + IEnumerable methods = commandMethods.Where(x => + x.CommandId == e.Interaction.Data.Id + ); + IEnumerable groups = groupCommands.Where(x => + x.CommandId == e.Interaction.Data.Id + ); + IEnumerable subgroups = subGroupCommands.Where(x => + x.CommandId == e.Interaction.Data.Id + ); + if (!methods.Any() && !groups.Any() && !subgroups.Any()) + { + throw new InvalidOperationException( + "An autocomplete interaction was created, but no command was registered for it." + ); + } + + if (methods.Any()) + { + MethodInfo method = methods.First().Method; + + IEnumerable? options = e.Interaction.Data.Options; + //Gets the focused option + DiscordInteractionDataOption focusedOption = options.First(o => o.Focused); + ParameterInfo parameter = method + .GetParameters() + .Skip(1) + .First(p => + p.GetCustomAttribute().Name == focusedOption.Name + ); + await RunAutocompleteAsync(e.Interaction, parameter, options, focusedOption); + } + + if (groups.Any()) + { + DiscordInteractionDataOption command = e.Interaction.Data.Options.First(); + MethodInfo method = groups + .First() + .Methods.First(x => x.Key == command.Name) + .Value; + + IEnumerable options = command.Options; + DiscordInteractionDataOption focusedOption = options.First(o => o.Focused); + ParameterInfo parameter = method + .GetParameters() + .Skip(1) + .First(p => + p.GetCustomAttribute().Name == focusedOption.Name + ); + await RunAutocompleteAsync(e.Interaction, parameter, options, focusedOption); + } + + if (subgroups.Any()) + { + DiscordInteractionDataOption command = e.Interaction.Data.Options.First(); + GroupCommand group = subgroups + .First() + .SubCommands.First(x => x.Name == command.Name); + MethodInfo method = group + .Methods.First(x => x.Key == command.Options.First().Name) + .Value; + + IEnumerable options = command + .Options.First() + .Options; + DiscordInteractionDataOption focusedOption = options.First(o => o.Focused); + ParameterInfo parameter = method + .GetParameters() + .Skip(1) + .First(p => + p.GetCustomAttribute().Name == focusedOption.Name + ); + await RunAutocompleteAsync(e.Interaction, parameter, options, focusedOption); + } + } + }); + return Task.CompletedTask; + } + + internal Task ContextMenuHandler(DiscordClient client, ContextMenuInteractionCreatedEventArgs e) + { + _ = Task.Run(async () => + { + //Creates the context + ContextMenuContext context = + new() + { + Interaction = e.Interaction, + Channel = e.Interaction.Channel, + Client = client, + Services = this.services, + CommandName = e.Interaction.Data.Name, + SlashCommandsExtension = 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, + }; + + if ( + e.Interaction.Guild != null + && e.TargetUser != null + && e.Interaction.Guild.Members.TryGetValue( + e.TargetUser.Id, + out DiscordMember? member + ) + ) + { + context.TargetMember = member; + } + + try + { + if (errored) + { + throw new InvalidOperationException( + "Context menus failed to register properly on startup." + ); + } + + //Gets the method for the command + ContextMenuCommand? method = + contextMenuCommands.FirstOrDefault(x => x.CommandId == e.Interaction.Data.Id) + ?? throw new InvalidOperationException( + "A context menu was executed, but no command was registered for it." + ); + await RunCommandAsync(context, method.Method, new[] { context }); + + await this.contextMenuExecuted.InvokeAsync( + this, + new ContextMenuExecutedEventArgs { Context = context } + ); + } + catch (Exception ex) + { + await this.contextMenuErrored.InvokeAsync( + this, + new ContextMenuErrorEventArgs { Context = context, Exception = ex } + ); + } + }); + + return Task.CompletedTask; + } + + internal async Task RunCommandAsync( + BaseContext context, + MethodInfo method, + IEnumerable args + ) + { + //Accounts for lifespans + SlashModuleLifespan moduleLifespan = + ( + method.DeclaringType.GetCustomAttribute() != null + ? method + .DeclaringType.GetCustomAttribute() + ?.Lifespan + : SlashModuleLifespan.Transient + ) ?? SlashModuleLifespan.Transient; + object classInstance = moduleLifespan switch //Accounts for static methods and adds DI + { + // Accounts for static methods and adds DI + SlashModuleLifespan.Scoped => method.IsStatic + ? ActivatorUtilities.CreateInstance( + this.services.CreateScope().ServiceProvider, + method.DeclaringType + ) + : CreateInstance(method.DeclaringType, this.services.CreateScope().ServiceProvider), + // Accounts for static methods and adds DI + SlashModuleLifespan.Transient => method.IsStatic + ? ActivatorUtilities.CreateInstance(this.services, method.DeclaringType) + : CreateInstance(method.DeclaringType, this.services), + // If singleton, gets it from the singleton list + SlashModuleLifespan.Singleton => singletonModules.First(x => + ReferenceEquals(x.GetType(), method.DeclaringType) + ), + // TODO: Use a more specific exception type + _ => throw new Exception( + $"An unknown {nameof(SlashModuleLifespanAttribute)} scope was specified on command {context.CommandName}" + ), + }; + + ApplicationCommandModule module = null; + if (classInstance is ApplicationCommandModule mod) + { + module = mod; + } + + //Slash commands + if (context is InteractionContext slashContext) + { + await this.slashInvoked.InvokeAsync( + this, + new SlashCommandInvokedEventArgs { Context = slashContext } + ); + + await RunPreexecutionChecksAsync(method, slashContext); + + //Runs BeforeExecution and accounts for groups that don't inherit from ApplicationCommandModule + bool shouldExecute = await ( + module?.BeforeSlashExecutionAsync(slashContext) ?? Task.FromResult(true) + ); + + if (shouldExecute) + { + await (Task)method.Invoke(classInstance, args.ToArray()); + + //Runs AfterExecution and accounts for groups that don't inherit from ApplicationCommandModule + await (module?.AfterSlashExecutionAsync(slashContext) ?? Task.CompletedTask); + } + } + //Context menus + if (context is ContextMenuContext CMContext) + { + await this.contextMenuInvoked.InvokeAsync( + this, + new ContextMenuInvokedEventArgs() { Context = CMContext } + ); + + await RunPreexecutionChecksAsync(method, CMContext); + + //This null check actually shouldn't be necessary for context menus but I'll keep it in just in case + bool shouldExecute = await ( + module?.BeforeContextMenuExecutionAsync(CMContext) ?? Task.FromResult(true) + ); + + if (shouldExecute) + { + await (Task)method.Invoke(classInstance, args.ToArray()); + + await (module?.AfterContextMenuExecutionAsync(CMContext) ?? Task.CompletedTask); + } + } + } + + //Property injection copied over from CommandsNext + internal static object CreateInstance(Type t, IServiceProvider services) + { + TypeInfo ti = t.GetTypeInfo(); + ConstructorInfo[] 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." + ); + } + + ConstructorInfo constructor = constructors[0]; + ParameterInfo[] constructorArgs = constructor.GetParameters(); + object[] 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 (int i = 0; i < args.Length; i++) + { + args[i] = services.GetRequiredService(constructorArgs[i].ParameterType); + } + } + + object? moduleInstance = Activator.CreateInstance(t, args); + + // inject into properties + IEnumerable props = t.GetRuntimeProperties() + .Where(xp => + xp.CanWrite + && xp.SetMethod != null + && !xp.SetMethod.IsStatic + && xp.SetMethod.IsPublic + ); + foreach (PropertyInfo? prop in props) + { + if (prop.GetCustomAttribute() != null) + { + continue; + } + + object? service = services.GetService(prop.PropertyType); + if (service == null) + { + continue; + } + + prop.SetValue(moduleInstance, service); + } + + // inject into fields + IEnumerable fields = t.GetRuntimeFields() + .Where(xf => !xf.IsInitOnly && !xf.IsStatic && xf.IsPublic); + foreach (FieldInfo? field in fields) + { + if (field.GetCustomAttribute() != null) + { + continue; + } + + object? service = services.GetService(field.FieldType); + if (service == null) + { + continue; + } + + field.SetValue(moduleInstance, service); + } + + return moduleInstance; + } + + //Parses slash command parameters + private async Task> ResolveInteractionCommandParametersAsync( + InteractionCreatedEventArgs e, + InteractionContext context, + MethodInfo method, + IEnumerable options + ) + { + List args = [context]; + IEnumerable parameters = method.GetParameters().Skip(1); + + for (int i = 0; i < parameters.Count(); i++) + { + ParameterInfo parameter = parameters.ElementAt(i); + + //Accounts for optional arguments without values given + if ( + parameter.IsOptional + && ( + !options?.Any(x => + x.Name.Equals( + parameter.GetCustomAttribute().Name, + StringComparison.InvariantCultureIgnoreCase + ) + ) ?? true + ) + ) + { + args.Add(parameter.DefaultValue); + } + else + { + DiscordInteractionDataOption option = options.Single(x => + x.Name.Equals( + parameter.GetCustomAttribute().Name, + StringComparison.InvariantCultureIgnoreCase + ) + ); + + //Checks the type and casts/references resolved and adds the value to the list + //This can probably reference the slash command's type property that didn't exist when I wrote this and it could use a cleaner switch instead, but if it works it works + 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 (Nullable.GetUnderlyingType(parameter.ParameterType)?.IsEnum == true) + { + args.Add( + Enum.Parse( + Nullable.GetUnderlyingType(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(TimeSpan?)) + { + string? value = option.Value.ToString(); + if (value == "0") + { + args.Add(TimeSpan.Zero); + continue; + } + if ( + int.TryParse( + value, + NumberStyles.Number, + CultureInfo.InvariantCulture, + out _ + ) + ) + { + args.Add(null); + continue; + } + value = value.ToLowerInvariant(); + + if (TimeSpan.TryParse(value, CultureInfo.InvariantCulture, out TimeSpan result)) + { + args.Add(result); + continue; + } + string[] gps = ["days", "hours", "minutes", "seconds"]; + Match mtc = GetTimeSpanRegex().Match(value); + if (!mtc.Success) + { + args.Add(null); + continue; + } + + int d = 0; + int h = 0; + int m = 0; + int s = 0; + foreach (string gp in gps) + { + string gpc = mtc.Groups[gp].Value; + if (string.IsNullOrWhiteSpace(gpc)) + { + continue; + } + + gpc = gpc.Trim(); + + char gpt = gpc[^1]; + int.TryParse( + gpc[..^1], + NumberStyles.Integer, + CultureInfo.InvariantCulture, + out int val + ); + switch (gpt) + { + case 'd': + d = val; + break; + + case 'h': + h = val; + break; + + case 'm': + m = val; + break; + + case 's': + s = val; + break; + } + } + result = new TimeSpan(d, h, m, s); + args.Add(result); + } + 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 DiscordMember? member + ) + ) + { + args.Add(member); + } + else if ( + e.Interaction.Data.Resolved.Users != null + && e.Interaction.Data.Resolved.Users.TryGetValue( + (ulong)option.Value, + out DiscordUser? 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 DiscordChannel? 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 DiscordRole? role + ) + ) + { + args.Add(role); + } + else + { + args.Add(e.Interaction.Guild.Roles.GetValueOrDefault((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 DiscordRole? role + ) + ) + { + args.Add(role); + } + else if ( + e.Interaction.Data.Resolved.Members != null + && e.Interaction.Data.Resolved.Members.TryGetValue( + (ulong)option.Value, + out DiscordMember? member + ) + ) + { + args.Add(member); + } + else if ( + e.Interaction.Data.Resolved.Users != null + && e.Interaction.Data.Resolved.Users.TryGetValue( + (ulong)option.Value, + out DiscordUser? user + ) + ) + { + args.Add(user); + } + else + { + throw new ArgumentException("Error resolving mentionable option."); + } + } + else if (parameter.ParameterType == typeof(DiscordEmoji)) + { + string? value = option.Value.ToString(); + + if ( + DiscordEmoji.TryFromUnicode(this.Client, value, out DiscordEmoji? emoji) + || DiscordEmoji.TryFromName(this.Client, value, out emoji) + ) + { + args.Add(emoji); + } + else + { + throw new ArgumentException("Error parsing emoji parameter."); + } + } + else if (parameter.ParameterType == typeof(DiscordAttachment)) + { + if ( + e.Interaction.Data.Resolved.Attachments?.ContainsKey((ulong)option.Value) + ?? false + ) + { + DiscordAttachment attachment = e.Interaction.Data.Resolved.Attachments[ + (ulong)option.Value + ]; + args.Add(attachment); + } + else + { + this.Client.Logger.LogError( + "Missing attachment in resolved data. This is an issue with Discord." + ); + } + } + else + { + throw new ArgumentException("Error resolving interaction."); + } + } + } + + return args; + } + + //Runs pre-execution checks + private static async Task RunPreexecutionChecksAsync(MethodInfo method, BaseContext context) + { + if (context is InteractionContext ctx) + { + //Gets all attributes from parent classes as well and stuff + List attributes = + [ + .. method.GetCustomAttributes(true), + .. 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() + ); + } + } + + Dictionary dict = []; + foreach (SlashCheckBaseAttribute att in attributes) + { + //Runs the check and adds the result to a list + bool 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) + { + List attributes = + [ + .. method.GetCustomAttributes(true), + .. 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() + ); + } + } + + Dictionary dict = []; + foreach (ContextMenuCheckBaseAttribute att in attributes) + { + //Runs the check and adds the result to a list + bool 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(), + }; + } + } + } + + //Actually handles autocomplete interactions + private async Task RunAutocompleteAsync( + DiscordInteraction interaction, + ParameterInfo parameter, + IEnumerable options, + DiscordInteractionDataOption focusedOption + ) + { + AutocompleteContext context = + new() + { + Interaction = interaction, + Client = this.Client, + Services = this.services, + SlashCommandsExtension = this, + Guild = interaction.Guild, + Channel = interaction.Channel, + User = interaction.User, + Options = options.ToList(), + FocusedOption = focusedOption, + }; + + try + { + //Gets the provider + Type? provider = parameter.GetCustomAttribute()?.Provider; + if (provider == null) + { + return; + } + + MethodInfo? providerMethod = provider.GetMethod(nameof(IAutocompleteProvider.Provider)); + object providerInstance = ActivatorUtilities.CreateInstance(this.services, provider); + + IEnumerable choices = await (Task< + IEnumerable + >) + providerMethod.Invoke(providerInstance, new[] { context }); + + if (choices.Count() > 25) + { + choices = choices.Take(25); + this.Client.Logger.LogWarning( + """Autocomplete provider "{provider}" returned more than 25 choices. Only the first 25 are passed to Discord.""", + nameof(provider) + ); + } + + await interaction.CreateResponseAsync( + DiscordInteractionResponseType.AutoCompleteResult, + new DiscordInteractionResponseBuilder().AddAutoCompleteChoices(choices) + ); + + await this.autocompleteExecuted.InvokeAsync( + this, + new() { Context = context, ProviderType = provider } + ); + } + catch (Exception ex) + { + await this.autocompleteErrored.InvokeAsync( + this, + new AutocompleteErrorEventArgs() + { + Exception = ex, + Context = context, + ProviderType = parameter.GetCustomAttribute()?.Provider, + } + ); + } + } + + #endregion + + /// + /// Refreshes your commands, used for refreshing choice providers or applying commands registered after the ready event on the discord client. + /// Should only be run on the slash command extension linked to shard 0 if sharding. + /// Not recommended and should be avoided since it can make slash commands be unresponsive for a while. + /// + public async Task RefreshCommandsAsync() + { + commandMethods.Clear(); + groupCommands.Clear(); + subGroupCommands.Clear(); + registeredCommands.Clear(); + + await Update(); + } + + /// + /// Fires when the execution of a slash command fails. + /// + public event AsyncEventHandler< + SlashCommandsExtension, + SlashCommandErrorEventArgs + > SlashCommandErrored + { + add => this.slashError.Register(value); + remove => this.slashError.Unregister(value); + } + private AsyncEvent slashError; + + /// + /// Fired when a slash command has been received and is to be executed + /// + public event AsyncEventHandler< + SlashCommandsExtension, + SlashCommandInvokedEventArgs + > SlashCommandInvoked + { + add => this.slashInvoked.Register(value); + remove => this.slashInvoked.Unregister(value); + } + private AsyncEvent slashInvoked; + + /// + /// Fires when the execution of a slash command is successful. + /// + public event AsyncEventHandler< + SlashCommandsExtension, + SlashCommandExecutedEventArgs + > 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< + SlashCommandsExtension, + ContextMenuErrorEventArgs + > ContextMenuErrored + { + add => this.contextMenuErrored.Register(value); + remove => this.contextMenuErrored.Unregister(value); + } + private AsyncEvent contextMenuErrored; + + /// + /// Fired when a context menu has been received and is to be executed + /// + public event AsyncEventHandler< + SlashCommandsExtension, + ContextMenuInvokedEventArgs + > ContextMenuInvoked + { + add => this.contextMenuInvoked.Register(value); + remove => this.contextMenuInvoked.Unregister(value); + } + private AsyncEvent contextMenuInvoked; + + /// + /// Fire when the execution of a context menu is successful. + /// + public event AsyncEventHandler< + SlashCommandsExtension, + ContextMenuExecutedEventArgs + > ContextMenuExecuted + { + add => this.contextMenuExecuted.Register(value); + remove => this.contextMenuExecuted.Unregister(value); + } + private AsyncEvent contextMenuExecuted; + + public event AsyncEventHandler< + SlashCommandsExtension, + AutocompleteErrorEventArgs + > AutocompleteErrored + { + add => this.autocompleteErrored.Register(value); + remove => this.autocompleteErrored.Register(value); + } + private AsyncEvent autocompleteErrored; + + public event AsyncEventHandler< + SlashCommandsExtension, + AutocompleteExecutedEventArgs + > AutocompleteExecuted + { + add => this.autocompleteExecuted.Register(value); + remove => this.autocompleteExecuted.Register(value); + } + private AsyncEvent autocompleteExecuted; + + public void Dispose() + { + this.slashError?.UnregisterAll(); + this.slashInvoked?.UnregisterAll(); + this.slashExecuted?.UnregisterAll(); + this.contextMenuErrored?.UnregisterAll(); + this.contextMenuExecuted?.UnregisterAll(); + this.contextMenuInvoked?.UnregisterAll(); + this.autocompleteErrored?.UnregisterAll(); + this.autocompleteExecuted?.UnregisterAll(); + + // Satisfy rule CA1816. Can be removed if this class is sealed. + GC.SuppressFinalize(this); + } + + [GeneratedRegex( + @"^(?\d+d\s*)?(?\d{1,2}h\s*)?(?\d{1,2}m\s*)?(?\d{1,2}s\s*)?$", + RegexOptions.ECMAScript + )] + private static partial Regex GetTimeSpanRegex(); +} + +//I'm not sure if creating separate classes is the cleanest thing here but I can't think of anything else so these stay + +internal class CommandMethod +{ + public ulong CommandId { get; set; } + public string Name { get; set; } + public MethodInfo Method { get; set; } +} + +internal class GroupCommand +{ + public ulong CommandId { get; set; } + public string Name { get; set; } + public List> Methods { get; set; } = null; +} + +internal class SubGroupCommand +{ + public ulong CommandId { get; set; } + public string Name { get; set; } + public List SubCommands { get; set; } = []; +} + +internal class ContextMenuCommand +{ + public ulong CommandId { get; set; } + public string Name { get; set; } + public MethodInfo Method { get; set; } +} diff --git a/tools/DSharpPlus.Tools.CodeBlockLanguageListGen/Program.cs b/tools/DSharpPlus.Tools.CodeBlockLanguageListGen/Program.cs index dd78f4be81..96454b5b7a 100644 --- a/tools/DSharpPlus.Tools.CodeBlockLanguageListGen/Program.cs +++ b/tools/DSharpPlus.Tools.CodeBlockLanguageListGen/Program.cs @@ -1,60 +1,60 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace DSharpPlus.Tools.CodeBlockLanguageListGen; - -public static class Program -{ - private static readonly string TemplateFile = new StreamReader(typeof(Program) - .Assembly - .GetManifestResourceStream("DSharpPlus.Tools.CodeBlockLanguageListGen.FromCodeAttribute.LanguageList.template") - ?? throw new InvalidOperationException("Failed to load the template file.") - ).ReadToEnd(); - - public static async Task Main() - { - // Grab the list of languages and their aliases - IReadOnlyList languages = await GetLanguagesAsync(); - StringBuilder languageListNode = new("new List()\n"); - languageListNode.Append(" {\n"); - for (int i = 0; i < languages.Count; i++) - { - languageListNode.Append($" \"{languages[i]}\""); - if (i != languages.Count - 1) - { - languageListNode.Append(','); - } - - languageListNode.Append('\n'); - } - languageListNode.Append(" }.ToFrozenSet()"); - - File.WriteAllText($"./DSharpPlus.Commands/ParameterModifiers/FromCode/FromCodeAttribute.LanguageList.cs", TemplateFile - .Replace("{{Date}}", DateTimeOffset.UtcNow.ToString("F", CultureInfo.InvariantCulture)) - .Replace("{{CodeBlockLanguages}}", languageListNode.ToString() - )); - } - - public static async ValueTask> GetLanguagesAsync() - { - ProcessStartInfo psi = new("node", "tools/get-highlight-languages") - { - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - CreateNoWindow = true - }; - - Process process = Process.Start(psi) ?? throw new InvalidOperationException("Failed to start the node process."); - await process.WaitForExitAsync(); - - string output = await process.StandardOutput.ReadToEndAsync(); - return output.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList(); - } -} +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace DSharpPlus.Tools.CodeBlockLanguageListGen; + +public static class Program +{ + private static readonly string TemplateFile = new StreamReader(typeof(Program) + .Assembly + .GetManifestResourceStream("DSharpPlus.Tools.CodeBlockLanguageListGen.FromCodeAttribute.LanguageList.template") + ?? throw new InvalidOperationException("Failed to load the template file.") + ).ReadToEnd(); + + public static async Task Main() + { + // Grab the list of languages and their aliases + IReadOnlyList languages = await GetLanguagesAsync(); + StringBuilder languageListNode = new("new List()\n"); + languageListNode.Append(" {\n"); + for (int i = 0; i < languages.Count; i++) + { + languageListNode.Append($" \"{languages[i]}\""); + if (i != languages.Count - 1) + { + languageListNode.Append(','); + } + + languageListNode.Append('\n'); + } + languageListNode.Append(" }.ToFrozenSet()"); + + File.WriteAllText($"./DSharpPlus.Commands/ParameterModifiers/FromCode/FromCodeAttribute.LanguageList.cs", TemplateFile + .Replace("{{Date}}", DateTimeOffset.UtcNow.ToString("F", CultureInfo.InvariantCulture)) + .Replace("{{CodeBlockLanguages}}", languageListNode.ToString() + )); + } + + public static async ValueTask> GetLanguagesAsync() + { + ProcessStartInfo psi = new("node", "tools/get-highlight-languages") + { + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + Process process = Process.Start(psi) ?? throw new InvalidOperationException("Failed to start the node process."); + await process.WaitForExitAsync(); + + string output = await process.StandardOutput.ReadToEndAsync(); + return output.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList(); + } +}